diff --git a/package-lock.json b/package-lock.json index 4c3a64bd..173a00d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@breejs/later": "^4.2.0", "@fastify/autoload": "^5.8.0", "@fastify/rate-limit": "^9.1.0", - "@fastify/reply-from": "^9.7.0", + "@fastify/reply-from": "^9.8.0", "@fastify/sensible": "^5.5.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", @@ -47,7 +47,7 @@ "mkdirp": "^3.0.1", "moment": "^2.30.1", "moment-precise-range-plugin": "^1.3.0", - "mqtt": "^5.5.2", + "mqtt": "^5.5.3", "ms-teams-wrapper": "^1.0.2", "nodemailer": "^6.9.13", "nodemailer-express-handlebars": "^6.1.2", @@ -63,7 +63,7 @@ "winston": "^3.13.0", "winston-daily-rotate-file": "^5.0.0", "ws": "^8.16.0", - "xstate": "^5.10.0" + "xstate": "^5.11.0" }, "devDependencies": { "@babel/eslint-parser": "^7.24.1", @@ -71,7 +71,7 @@ "eslint-plugin-import": "^2.29.1", "jest": "^29.7.0", "prettier": "^3.2.5", - "snyk": "^1.1288.0" + "snyk": "^1.1290.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1282,17 +1282,16 @@ } }, "node_modules/@fastify/reply-from": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.7.0.tgz", - "integrity": "sha512-/F1QBl3FGlTqStjmiuoLRDchVxP967TZh6FZPwQteWhdLsDec8mqSACE+cRzw6qHUj3v9hfdd7JNgmb++fyFhQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz", + "integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==", "dependencies": { "@fastify/error": "^3.0.0", "end-of-stream": "^1.4.4", "fast-content-type-parse": "^1.1.0", "fast-querystring": "^1.0.0", "fastify-plugin": "^4.0.0", - "pump": "^3.0.0", - "tiny-lru": "^11.0.0", + "toad-cache": "^3.7.0", "undici": "^5.19.1" } }, @@ -6372,9 +6371,9 @@ } }, "node_modules/mqtt": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.2.tgz", - "integrity": "sha512-dlKxINBrrorgMp1A5UHQVf5GAkn1m/dY12W2Sp6LAY794RxQ0OPo0Q9N2S3qrNRjjC1WETA/9oYR6yadhR3siw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.3.tgz", + "integrity": "sha512-R5fTibItlB5kvikTrU29ZgImvAch2ihKMyuvN3CJqd6nsZuearCSv3IGqxEdsSIXxflK6lGDgFmqnsnyJqzYtQ==", "dependencies": { "@types/readable-stream": "^4.0.5", "@types/ws": "^8.5.9", @@ -7047,15 +7046,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -7517,9 +7507,9 @@ } }, "node_modules/snyk": { - "version": "1.1288.0", - "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1288.0.tgz", - "integrity": "sha512-IsfjXWVffhuB/UIefM7iqCGVBiLnULv08ax4YBTO/SF/RzIlm8Q92+I2sSwEva8f7kHYNE85Cjn9fg+LlmKUCQ==", + "version": "1.1290.0", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1290.0.tgz", + "integrity": "sha512-AD72kAeGZ9f5AguB4S4LnUFrpkCg3fww9pykAiLRkkBT4ueGWsN2QyAwH4tpdxUVs3R1SUnY1OornrBXRFkdNg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -7846,14 +7836,6 @@ "real-require": "^0.2.0" } }, - "node_modules/tiny-lru": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz", - "integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==", - "engines": { - "node": ">=12" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8438,9 +8420,9 @@ } }, "node_modules/xstate": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.10.0.tgz", - "integrity": "sha512-rn3AbPFHngLqtfFTe9KDXJ1wQX4ACDKnQyQ2ShjuKor3iVnxDSKyDL9llFLiOpaBQu+cFMKcZYlpXo5nb/q5rw==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.11.0.tgz", + "integrity": "sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" diff --git a/package.json b/package.json index 3c0046a9..446c8877 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@breejs/later": "^4.2.0", "@fastify/autoload": "^5.8.0", "@fastify/rate-limit": "^9.1.0", - "@fastify/reply-from": "^9.7.0", + "@fastify/reply-from": "^9.8.0", "@fastify/sensible": "^5.5.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", @@ -87,7 +87,7 @@ "mkdirp": "^3.0.1", "moment": "^2.30.1", "moment-precise-range-plugin": "^1.3.0", - "mqtt": "^5.5.2", + "mqtt": "^5.5.3", "ms-teams-wrapper": "^1.0.2", "nodemailer": "^6.9.13", "nodemailer-express-handlebars": "^6.1.2", @@ -103,7 +103,7 @@ "winston": "^3.13.0", "winston-daily-rotate-file": "^5.0.0", "ws": "^8.16.0", - "xstate": "^5.10.0" + "xstate": "^5.11.0" }, "devDependencies": { "@babel/eslint-parser": "^7.24.1", @@ -111,7 +111,7 @@ "eslint-plugin-import": "^2.29.1", "jest": "^29.7.0", "prettier": "^3.2.5", - "snyk": "^1.1288.0" + "snyk": "^1.1290.0" }, "pkg": { "assets": [ diff --git a/src/config/config-gen-api-docs.yaml b/src/config/config-gen-api-docs.yaml index f0333045..d95c5914 100644 --- a/src/config/config-gen-api-docs.yaml +++ b/src/config/config-gen-api-docs.yaml @@ -132,31 +132,49 @@ Butler: # Settings for monitoring Qlik Sense licenses qlikSenseLicense: licenseMonitor: - enable: true - frequency: every 5 minutes + enable: false + frequency: every 6 hours # https://bunkat.github.io/later/parsers.html#text destination: - influxDb: # Send service alerts to InfluxDB + influxDb: # Store license data in InfluxDB enable: true tag: - static: # Static attributes/dimensions to attach to the data sent to New Relic. + static: # Static attributes/tags to attach to the data sent to InflixDB # - name: foo # value: bar licenseRelease: - enable: false - # frequency: every 3 hours - frequency: every 5 minutes - neverReleaseUsers: - # - userDir: 'INTERNAL' - # userId: 'sa_repository' - # - userDir: 'INTERNAL' - # userId: 'sa_api' + enable: false # true/false. If true, Butler will release unused licenses according to settings below + dryRun: true # true/false. If true, Butler will not actually release any licenses, just log what it would have done. + frequency: every 6 hours # https://bunkat.github.io/later/parsers.html#text + neverRelease: # Various ways of defining which users should never have their licenses released + user: # Users who should never have their licenses released + # - userDir: 'INTERNAL' + # userId: 'sa_repository' + # - userDir: 'INTERNAL' + # userId: 'sa_api' + # - userDir: 'USERDIR' + # userId: 'qs_admin_account' + tag: # Users with these tags will never have their licenses released + # - License do not release + # - some other tag + customProperty: # Users with these custom properties will never have their licenses released + # - name: LicenseManage + # value: do-not-release + userDirectory: # List of user directories whose users should never have their licenses released + # - INTERNAL + # - ADMIN + inactive: Ignore # Ignore/Yes/No. The value is case insensitive + # No = Don't release licenses for users marked as "Inactive=No" in the QMC + # Yes = Don't release licenses for users marked as "Inactive=Yes" in the QMC + # Ignore = Disregard this setting + blocked: Ignore # Ignore/Yes/No, No = Don't release licenses for users marked as "Blocked=No" in the QMC + removedExternally: ignore # Ignore/Yes/No, No = Don't release licenses for users marked as "Removed externally=No" in the QMC licenseType: # License types to monitor and release analyzer: - enable: true - releaseThresholdDays: 5 + enable: true # Monitor and release Analyzer licenses + releaseThresholdDays: 30 # Number of days a license can be unused before it is released professional: - enable: true - releaseThresholdDays: 5 + enable: true # Monitor and release Professional licenses + releaseThresholdDays: 30 # Number of days a license can be unused before it is released destination: influxDb: # Store info about released licenses in InfluxDB enable: true diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 7a0f258a..5f87c1c4 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -139,7 +139,7 @@ Butler: # Settings for monitoring Qlik Sense licenses qlikSenseLicense: licenseMonitor: - enable: true + enable: false frequency: every 6 hours # https://bunkat.github.io/later/parsers.html#text destination: influxDb: # Store license data in InfluxDB @@ -149,15 +149,32 @@ Butler: - name: foo value: bar licenseRelease: - enable: true + enable: false # true/false. If true, Butler will release unused licenses according to settings below + dryRun: true # true/false. If true, Butler will not actually release any licenses, just log what it would have done. frequency: every 6 hours # https://bunkat.github.io/later/parsers.html#text - neverReleaseUsers: # Users that should never have their license released - - userDir: 'INTERNAL' - userId: 'sa_repository' - - userDir: 'INTERNAL' - userId: 'sa_api' - - userDir: 'USERDIR' - userId: 'qs_admin_account' + neverRelease: # Various ways of defining which users should never have their licenses released + user: # Users who should never have their licenses released + - userDir: 'INTERNAL' + userId: 'sa_repository' + - userDir: 'INTERNAL' + userId: 'sa_api' + - userDir: 'USERDIR' + userId: 'qs_admin_account' + tag: # Users with these tags will never have their licenses released + - License do not release + - some other tag + customProperty: # Users with these custom properties will never have their licenses released + - name: LicenseManage + value: do-not-release + userDirectory: # List of user directories whose users should never have their licenses released + - INTERNAL + - ADMIN + inactive: Ignore # Ignore/Yes/No. The value is case insensitive + # No = Don't release licenses for users marked as "Inactive=No" in the QMC + # Yes = Don't release licenses for users marked as "Inactive=Yes" in the QMC + # Ignore = Disregard this setting + blocked: Ignore # Ignore/Yes/No, No = Don't release licenses for users marked as "Blocked=No" in the QMC + removedExternally: ignore # Ignore/Yes/No, No = Don't release licenses for users marked as "Removed externally=No" in the QMC licenseType: # License types to monitor and release analyzer: enable: true # Monitor and release Analyzer licenses diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js index 53df804a..fcc9161a 100644 --- a/src/lib/assert/assert_config_file.js +++ b/src/lib/assert/assert_config_file.js @@ -1029,51 +1029,57 @@ export const configFileStructureAssert = async (config, logger) => { configFileCorrect = false; } + // License release dry run + if (!config.has('Butler.qlikSenseLicense.licenseRelease.dryRun')) { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.dryRun"'); + configFileCorrect = false; + } + if (!config.has('Butler.qlikSenseLicense.licenseRelease.frequency')) { logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.frequency"'); configFileCorrect = false; } - // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers are objects with the following properties: + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.user are objects with the following properties: // { // userDir: 'string' // userId: 'string', // } - if (config.has('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers')) { - const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers'); + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.user')) { + const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); if (neverReleaseUsers) { if (!Array.isArray(neverReleaseUsers)) { - logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers" is not an array'); + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user" is not an array'); configFileCorrect = false; } else { neverReleaseUsers.forEach((user, index) => { if (typeof user !== 'object') { logger.error( - `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not an object` + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not an object` ); configFileCorrect = false; } else { if (!Object.prototype.hasOwnProperty.call(user, 'userId')) { logger.error( - `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]"` + `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"` ); configFileCorrect = false; } else if (typeof user.userId !== 'string') { logger.error( - `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not a string` + `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string` ); configFileCorrect = false; } if (!Object.prototype.hasOwnProperty.call(user, 'userDir')) { logger.error( - `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]"` + `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"` ); configFileCorrect = false; } else if (typeof user.userDir !== 'string') { logger.error( - `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not a string` + `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string` ); configFileCorrect = false; } @@ -1082,7 +1088,125 @@ export const configFileStructureAssert = async (config, logger) => { } } } else { - logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers"'); + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.user"'); + configFileCorrect = false; + } + + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.tag objects with the following properties: + // - 'string' + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag')) { + const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); + + if (neverReleaseTags) { + if (!Array.isArray(neverReleaseTags)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag" is not an array'); + configFileCorrect = false; + } else { + neverReleaseTags.forEach((tag, index) => { + if (typeof tag !== 'string') { + logger.error(`ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag[${index}]" is not a string`); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag"'); + configFileCorrect = false; + } + + // The custom properties specified in the Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty array + // should meet the following requirements: + // - Each array item should be an object with the following properties: + // { + // name: 'string', + // value: 'string' + // } + + // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory objects with the following properties: + // - 'string' + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory')) { + const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); + + if (neverReleaseUserDirectories) { + if (!Array.isArray(neverReleaseUserDirectories)) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory" is not an array'); + configFileCorrect = false; + } else { + neverReleaseUserDirectories.forEach((userDirectory, index) => { + if (typeof userDirectory !== 'string') { + logger.error( + `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory[${index}]" is not a string` + ); + configFileCorrect = false; + } + }); + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive')) { + const inactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive'); + + if (inactive) { + if (typeof inactive !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(inactive.toLowerCase())) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" must be either "Yes" or "No"'); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked')) { + const blocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked'); + + if (blocked) { + if (typeof blocked !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(blocked.toLowerCase())) { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" must be either "Yes" or "No"'); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked"'); + configFileCorrect = false; + } + + // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally meets the following requirements: + // - Value is either Yes or No + // - Disregard case + if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')) { + const removedExternally = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally'); + + if (removedExternally) { + if (typeof removedExternally !== 'string') { + logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" is not a string'); + configFileCorrect = false; + } else if (!['yes', 'no', 'ignore'].includes(removedExternally.toLowerCase())) { + logger.error( + 'ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" must be either "Yes" or "No"' + ); + configFileCorrect = false; + } + } + } else { + logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally"'); configFileCorrect = false; } diff --git a/src/lib/qliksense_license.js b/src/lib/qliksense_license.js index d7f1ea19..b84842a0 100644 --- a/src/lib/qliksense_license.js +++ b/src/lib/qliksense_license.js @@ -54,26 +54,77 @@ async function checkQlikSenseLicenseStatus(config, logger) { } // Function to release professional licenses -async function licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers) { - const result1 = await qrsInstance.Get(`license/professionalaccesstype/full`); - - // Is status code 200 or body is empty? +async function licenseReleaseProfessional(config, logger, qrsInstance) { + // Build date filter to be used when fetching licenses with old lastUsed date + // Get the current date and time + const currentDate = new Date(); + + // Get the release threshold (days) from the configuration + const releaseThresholdDays = config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays'); + + // Subtract the release threshold (days) from the current date, then round to the last moment of that day + const cutoffDate = new Date(currentDate); + cutoffDate.setDate(cutoffDate.getDate() - releaseThresholdDays); + cutoffDate.setHours(23, 59, 59, 999); + + // verbose log, format dates as yyyy-mm-ddThh:mm:ss.sssZ + logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: currentDate: ${currentDate.toISOString()}`); + logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: releaseThresholdDays: ${releaseThresholdDays}`); + logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: cutoffDate: ${cutoffDate.toISOString()}`); + + // Get all assigned professional licenses + const url = `license/professionalaccesstype/full?filter=lastUsed le '${cutoffDate.toISOString()}'`; + logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Query URL: ${url}`); + const result1 = await qrsInstance.Get(url); + + // Is status code other than 200 or body is empty? if (result1.statusCode !== 200 || !result1.body) { - logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result1.statusCode}`); + logger.error( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Could not get list of assigned professional licenses. HTTP status code ${result1.statusCode}` + ); return false; } // Debug log - logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Allocated: ${JSON.stringify(result1.body)}`); + logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Assigned: ${JSON.stringify(result1.body)}`); // Determnine which allocated licenses to release. // Only release licenses that are NOT quarantined - // Take into account the releaese threshold (days), i.e. days since last use // Loop over all licenses retrived in previous step, add licenses to be released to releaseProfessional array const releaseProfessional = []; + + const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); + const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); + const neverReleaseCustomProperties = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty'); + const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); + // eslint-disable-next-line no-restricted-syntax for (const license of result1.body) { if (!license.quarantined) { + // Get full user info + let currentUser; + try { + // eslint-disable-next-line no-await-in-loop + const res = await qrsInstance.Get(`user/${license.user.id}`); + if (res.statusCode !== 200 || !res.body) { + logger.error( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + ); + return false; + } + currentUser = res.body; + } catch (err) { + logger.error( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + ); + if (err.stack) { + logger.error( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}` + ); + } + return false; + } + // Get days since last use const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24)); @@ -81,88 +132,279 @@ async function licenseReleaseProfessional(config, logger, qrsInstance, neverRele // Compare userDir and userId // If the user is in the neverReleaseUsers array, do not release the license let doNotRelease = false; + let doNotReleaseReason = ''; + + // Check do-not-release user names // eslint-disable-next-line no-restricted-syntax for (const user of neverReleaseUsers) { if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) { doNotRelease = true; + doNotReleaseReason = 'User is in the neverRelease.user list'; break; } } - // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license + // Check do-not-release tags + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not haven any neverReleaseTags set + if (!doNotRelease) { + // Check if the user has any of the neverReleaseTags set + // currentUser.tags is an array of tag objects. Each object has properties id and name + // eslint-disable-next-line no-restricted-syntax + for (const tag of currentUser.tags) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseTag of neverReleaseTags) { + if (tag.name === neverReleaseTag) { + doNotRelease = true; + doNotReleaseReason = `User tagged with '${neverReleaseTag}', which is in the neverRelease.tag list`; + break; + } + } + if (doNotRelease) { + break; + } + } + } + + // Check do-not-release custom properties + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not have any neverReleaseCustomProperties set + if (!doNotRelease) { + // currentUser.customProperties is an array of custom property objects. + // Each object looks like this: + // { + // "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f", + // "value": "foo", + // "definition": { + // "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f", + // "name": "bar", + // "valueType": "Text" + // } + // } + // eslint-disable-next-line no-restricted-syntax + for (const customProperty of currentUser.customProperties) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseCustomProperty of neverReleaseCustomProperties) { + if ( + customProperty.definition.name === neverReleaseCustomProperty.name && + customProperty.value === neverReleaseCustomProperty.value + ) { + doNotRelease = true; + doNotReleaseReason = `User has custom property '${neverReleaseCustomProperty.name}' set to '${neverReleaseCustomProperty.value}', which is in the neverRelease.customProperty list`; + break; + } + } + if (doNotRelease) { + break; + } + } + } + + // Check do-not-release user directory + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not have any neverReleaseUserDirectories set + if (!doNotRelease) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseUserDir of neverReleaseUserDirectories) { + if (license.user.userDirectory === neverReleaseUserDir) { + doNotRelease = true; + doNotReleaseReason = `User's user directory is '${neverReleaseUserDir}', which is in the neverRelease.userDirectory list`; + break; + } + } + } + + // Check do-not-release inactive users + if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase() !== 'ignore') { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'No' (case insensitive) and currentUser.inactive===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'Yes' (case insensitive) and currentUser.inactive===true + const neverReleaseInactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase(); + if ( + (neverReleaseInactive === 'no' && currentUser.inactive === false) || + (neverReleaseInactive === 'yes' && currentUser.inactive === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has inactive status '${currentUser.inactive}'`; + } + } + + // Check do-not-release blocked users + if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase() !== 'ignore') { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'No' (case insensitive) and currentUser.blacklisted===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'Yes' (case insensitive) and currentUser.blacklisted===true + const neverReleaseBlocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase(); + if ( + (neverReleaseBlocked === 'no' && currentUser.blacklisted === false) || + (neverReleaseBlocked === 'yes' && currentUser.blacklisted === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has blocked status '${currentUser.blacklisted}'`; + } + } + + // Check do-not-release removed externally users if ( !doNotRelease && - daysSinceLastUse >= config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') + config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally').toLowerCase() !== 'ignore' ) { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'No' (case insensitive) and currentUser.removedExternally===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'Yes' (case insensitive) and currentUser.removedExternally===true + const neverReleaseRemovedExternally = config + .get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally') + .toLowerCase(); + if ( + (neverReleaseRemovedExternally === 'no' && currentUser.removedExternally === false) || + (neverReleaseRemovedExternally === 'yes' && currentUser.removedExternally === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has removedExternally status '${currentUser.removedExternally}'`; + } + } + + // Should currentUser be released? + if (!doNotRelease) { + logger.info( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseProfessional array` + ); releaseProfessional.push({ licenseId: license.id, userDir: license.user.userDirectory, userId: license.user.userId, daysSinceLastUse, }); + } else { + logger.info( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}` + ); } } } - // Release all licenses in the releaseProfessional array - // eslint-disable-next-line no-restricted-syntax - for (const licenseRelease of releaseProfessional) { - logger.info( - `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` - ); - - // Release license - // eslint-disable-next-line no-await-in-loop - const result2 = await qrsInstance.Delete(`license/professionalaccesstype/${licenseRelease.licenseId}`); + logger.verbose( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Professional licenses to be released: ${JSON.stringify(releaseProfessional, null, 2)}` + ); + + // Is license release dry-run enabled? If so, do not release any licenses + if (config.get('Butler.qlikSenseLicense.licenseRelease.dryRun') === true) { + logger.info('QLIKSENSE LICENSE RELEASE PROFESSIONAL: Dry-run enabled. No licenses will be released'); + } else { + // Release all licenses in the releaseProfessional array + // eslint-disable-next-line no-restricted-syntax + for (const licenseRelease of releaseProfessional) { + logger.info( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + ); + + // Release license + // eslint-disable-next-line no-await-in-loop + const result2 = await qrsInstance.Delete(`license/professionalaccesstype/${licenseRelease.licenseId}`); - // Is status code 204? Error if it's nmt - if (result2.statusCode !== 204) { - logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result2.statusCode}`); - return false; - } + // Is status code 204? Error if it's nmt + if (result2.statusCode !== 204) { + logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result2.statusCode}`); + return false; + } - // Debug log - logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: ${JSON.stringify(result2.body)}`); + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: ${JSON.stringify(result2.body)}`); - // Write info about released license to InfluxDB? - if ( - config.get('Butler.influxDb.enable') === true && - config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') >= 0 - ) { - // eslint-disable-next-line no-await-in-loop - await postQlikSenseLicenseReleasedToInfluxDB({ - licenseType: 'professional', - licenseId: licenseRelease.licenseId, - userDir: licenseRelease.userDir, - userId: licenseRelease.userId, - daysSinceLastUse: licenseRelease.daysSinceLastUse, - }); + // Write info about released license to InfluxDB? + if ( + config.get('Butler.influxDb.enable') === true && + config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') >= 0 + ) { + // eslint-disable-next-line no-await-in-loop + await postQlikSenseLicenseReleasedToInfluxDB({ + licenseType: 'professional', + licenseId: licenseRelease.licenseId, + userDir: licenseRelease.userDir, + userId: licenseRelease.userId, + daysSinceLastUse: licenseRelease.daysSinceLastUse, + }); + } } } return true; } // Function to release analyzer licenses -async function licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers) { - const result3 = await qrsInstance.Get(`license/analyzeraccesstype/full`); +async function licenseReleaseAnalyzer(config, logger, qrsInstance) { + // Build date filter to be used when fetching licenses with old lastUsed date + // Get the current date and time + const currentDate = new Date(); + + // Get the release threshold (days) from the configuration + const releaseThresholdDays = config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays'); + + // Subtract the release threshold (days) from the current date, then round to the last moment of that day + const cutoffDate = new Date(currentDate); + cutoffDate.setDate(cutoffDate.getDate() - releaseThresholdDays); + cutoffDate.setHours(23, 59, 59, 999); + + // verbose log, format dates as yyyy-mm-ddThh:mm:ss.sssZ + logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: currentDate: ${currentDate.toISOString()}`); + logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: releaseThresholdDays: ${releaseThresholdDays}`); + logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: cutoffDate: ${cutoffDate.toISOString()}`); + + // Get all assigned analyzer licenses + const url = `license/analyzeraccesstype/full?filter=lastUsed le '${cutoffDate.toISOString()}'`; + logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Query URL: ${url}`); + const result3 = await qrsInstance.Get(url); // Is status code 200 or body is empty? if (result3.statusCode !== 200 || !result3.body) { - logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result3.statusCode}`); + logger.error( + `QLIKSENSE LICENSE RELEASE ANALYZER: Could not get list of assigned analyzer licenses. HTTP status code ${result3.statusCode}` + ); return; } // Debug log - logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Allocated: ${JSON.stringify(result3.body)}`); + logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Assigned: ${JSON.stringify(result3.body)}`); // Determnine which allocated licenses to release. // Only release licenses that are NOT quarantined - // Take into account the releaese threshold (days), i.e. days since last use // Loop over all licenses retrived in previous step, add licenses to be released to releaseAnalyzer array const releaseAnalyzer = []; + + const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user'); + const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag'); + const neverReleaseCustomProperties = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty'); + const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory'); + // eslint-disable-next-line no-restricted-syntax for (const license of result3.body) { if (!license.quarantined) { + // Get full user info + let currentUser; + try { + // eslint-disable-next-line no-await-in-loop + const res = await qrsInstance.Get(`user/${license.user.id}`); + if (res.statusCode !== 200 || !res.body) { + logger.error( + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + ); + return; + } + currentUser = res.body; + } catch (err) { + logger.error( + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}` + ); + if (err.stack) { + logger.error( + `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}` + ); + } + return; + } + // Get days since last use const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24)); @@ -170,64 +412,203 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseU // Compare userDir and userId // If the user is in the neverReleaseUsers array, do not release the license let doNotRelease = false; + let doNotReleaseReason = ''; + + // Check do-not-release user names // eslint-disable-next-line no-restricted-syntax for (const user of neverReleaseUsers) { if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) { doNotRelease = true; + doNotReleaseReason = 'User is in the neverRelease.user list'; break; } } - // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license + // Check do-not-release tags + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not haven any neverReleaseTags set + if (!doNotRelease) { + // Check if the user has any of the neverReleaseTags set + // currentUser.tags is an array of tag objects. Each object has properties id and name + // eslint-disable-next-line no-restricted-syntax + for (const tag of currentUser.tags) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseTag of neverReleaseTags) { + if (tag.name === neverReleaseTag) { + doNotRelease = true; + doNotReleaseReason = `User tagged with '${neverReleaseTag}', which is in the neverRelease.tag list`; + break; + } + } + if (doNotRelease) { + break; + } + } + } + + // Check do-not-release custom properties + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not have any neverReleaseCustomProperties set + if (!doNotRelease) { + // currentUser.customProperties is an array of custom property objects. + // Each object looks like this: + // { + // "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f", + // "value": "foo", + // "definition": { + // "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f", + // "name": "bar", + // "valueType": "Text" + // } + // } + // eslint-disable-next-line no-restricted-syntax + for (const customProperty of currentUser.customProperties) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseCustomProperty of neverReleaseCustomProperties) { + if ( + customProperty.definition.name === neverReleaseCustomProperty.name && + customProperty.value === neverReleaseCustomProperty.value + ) { + doNotRelease = true; + doNotReleaseReason = `User has custom property '${neverReleaseCustomProperty.name}' set to '${neverReleaseCustomProperty.value}', which is in the neverRelease.customProperty list`; + break; + } + } + if (doNotRelease) { + break; + } + } + } + + // Check do-not-release user directory + // If... + // - the user is not already marked as doNotRelease=true and + // - the currentUser does not have any neverReleaseUserDirectories set + if (!doNotRelease) { + // eslint-disable-next-line no-restricted-syntax + for (const neverReleaseUserDir of neverReleaseUserDirectories) { + if (license.user.userDirectory === neverReleaseUserDir) { + doNotRelease = true; + doNotReleaseReason = `User's user directory is '${neverReleaseUserDir}', which is in the neverRelease.userDirectory list`; + break; + } + } + } + + // Check do-not-release inactive users + if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase() !== 'ignore') { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'No' (case insensitive) and currentUser.inactive===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'Yes' (case insensitive) and currentUser.inactive===true + const neverReleaseInactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase(); + if ( + (neverReleaseInactive === 'no' && currentUser.inactive === false) || + (neverReleaseInactive === 'yes' && currentUser.inactive === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has inactive status '${currentUser.inactive}'`; + } + } + + // Check do-not-release blocked users + if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase() !== 'ignore') { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'No' (case insensitive) and currentUser.blacklisted===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'Yes' (case insensitive) and currentUser.blacklisted===true + const neverReleaseBlocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase(); + if ( + (neverReleaseBlocked === 'no' && currentUser.blacklisted === false) || + (neverReleaseBlocked === 'yes' && currentUser.blacklisted === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has blocked status '${currentUser.blacklisted}'`; + } + } + + // Check do-not-release removed externally users if ( !doNotRelease && - daysSinceLastUse >= config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') + config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally').toLowerCase() !== 'ignore' ) { + // Do not release user if... + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'No' (case insensitive) and currentUser.removedExternally===false + // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'Yes' (case insensitive) and currentUser.removedExternally===true + const neverReleaseRemovedExternally = config + .get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally') + .toLowerCase(); + if ( + (neverReleaseRemovedExternally === 'no' && currentUser.removedExternally === false) || + (neverReleaseRemovedExternally === 'yes' && currentUser.removedExternally === true) + ) { + doNotRelease = true; + doNotReleaseReason = `User has removedExternally status '${currentUser.removedExternally}'`; + } + } + + // Should currentUser be released? + if (!doNotRelease) { + logger.info( + `QLIKSENSE LICENSE RELEASE ANALYZER: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseAnalyzer array` + ); releaseAnalyzer.push({ licenseId: license.id, userDir: license.user.userDirectory, userId: license.user.userId, daysSinceLastUse, }); + } else { + logger.info( + `QLIKSENSE LICENSE RELEASE ANALYZER: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}` + ); } } } - // Release all licenses in the releaseAnalyzer array - // eslint-disable-next-line no-restricted-syntax - for (const licenseRelease of releaseAnalyzer) { - logger.info( - `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` - ); + logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: Analyzer licenses to be released: ${JSON.stringify(releaseAnalyzer, null, 2)}`); - // Release license - // eslint-disable-next-line no-await-in-loop - const result4 = await qrsInstance.Delete(`license/analyzeraccesstype/${licenseRelease.licenseId}`); + // Is license release dry-run enabled? If so, do not release any licenses + if (config.get('Butler.qlikSenseLicense.licenseRelease.dryRun') === true) { + logger.info('QLIKSENSE LICENSE RELEASE ANALYZER: Dry-run enabled. No licenses will be released'); + } else { + // Release all licenses in the releaseAnalyzer array + // eslint-disable-next-line no-restricted-syntax + for (const licenseRelease of releaseAnalyzer) { + logger.info( + `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + ); - // Is status code 204? Error if it's nmt - if (result4.statusCode !== 204) { - logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result4.statusCode}`); - return; - } + // Release license + // eslint-disable-next-line no-await-in-loop + const result4 = await qrsInstance.Delete(`license/analyzeraccesstype/${licenseRelease.licenseId}`); - // Debug log - logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: ${JSON.stringify(result4.body)}`); + // Is status code 204? Error if it's nmt + if (result4.statusCode !== 204) { + logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result4.statusCode}`); + return; + } - // Write info about released license to InfluxDB? - if ( - config.get('Butler.influxDb.enable') === true && - config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') >= 0 - ) { - // eslint-disable-next-line no-await-in-loop - await postQlikSenseLicenseReleasedToInfluxDB({ - licenseType: 'analyzer', - licenseId: licenseRelease.licenseId, - userDir: licenseRelease.userDir, - userId: licenseRelease.userId, - daysSinceLastUse: licenseRelease.daysSinceLastUse, - }); + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: ${JSON.stringify(result4.body)}`); + + // Write info about released license to InfluxDB? + if ( + config.get('Butler.influxDb.enable') === true && + config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') >= 0 + ) { + // eslint-disable-next-line no-await-in-loop + await postQlikSenseLicenseReleasedToInfluxDB({ + licenseType: 'analyzer', + licenseId: licenseRelease.licenseId, + userDir: licenseRelease.userDir, + userId: licenseRelease.userId, + daysSinceLastUse: licenseRelease.daysSinceLastUse, + }); + } } } + return true; } // Function to release Qlik Sense licenses @@ -248,30 +629,33 @@ async function checkQlikSenseLicenseRelease(config, logger) { }; const qrsInstance = new QrsInteract(configQRS); - // Which user accounts should never be released? - // Get info from config file - const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers'); + // Is license release enabled for professional access licenses? + if (config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable') === true) { + // Release licenses of type "professional" + const res = await licenseReleaseProfessional(config, logger, qrsInstance); - // Release licenses of type "professional" - let res = await licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers); - - // Success? - if (!res) { - return false; + // Success? + if (!res) { + return false; + } } - // Release licenses of type "analyzer" - res = await licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers); - // Success? - if (!res) { - return false; + // Is license release enabled for analyzer access licenses? + if (config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable') === true) { + // Release licenses of type "analyzer" + const res = await licenseReleaseAnalyzer(config, logger, qrsInstance); + + // Success? + if (!res) { + return false; + } } - return true + return true; } catch (err) { - logger.error(`QLIKSENSE LICENSE MONITOR: ${err}`); + logger.error(`QLIKSENSE LICENSE RELEASE: ${err}`); if (err.stack) { - logger.error(`QLIKSENSE LICENSE MONITOR: ${err.stack}`); + logger.error(`QLIKSENSE LICENSE RELEASE: ${err.stack}`); } return false; }