Skip to content

Commit

Permalink
feat: Add command for listing/exporting info about reload tasks
Browse files Browse the repository at this point in the history
Implements #105
  • Loading branch information
Göran Sander committed Oct 26, 2022
1 parent 851416e commit 9c1e0c9
Show file tree
Hide file tree
Showing 15 changed files with 1,945 additions and 38 deletions.
229 changes: 228 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,28 @@
},
"license": "MIT",
"dependencies": {
"axios": "^1.1.3",
"commander": "^9.4.1",
"csv-stringify": "^6.2.0",
"enigma.js": "^2.10.0",
"eslint": "^8.25.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"fs-extra": "^10.1.0",
"luxon": "^3.0.4",
"nanoid": "^3.3.4",
"node-xlsx": "^0.21.0",
"qrs-interact": "^6.3.1",
"random-words": "^1.2.0",
"table": "^6.8.0",
"text-treeview": "^1.0.2",
"upath": "^2.0.1",
"winston": "^3.8.2",
"winston-daily-rotate-file": "^4.7.1",
"ws": "^8.9.0"
"ws": "^8.9.0",
"yesno": "^0.4.0"
},
"devDependencies": {
"prettier": "2.7.1",
Expand Down
72 changes: 68 additions & 4 deletions src/ctrl-q.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const { Command, Option } = require('commander');
const { logger, appVersion, getLoggingLevel, setLoggingLevel, isPkg, execPath } = require('./globals');
const { logger, appVersion } = require('./globals');

const { createUserActivityCustomProperty } = require('./lib/createuseractivitycp');
// const { createUserActivityCustomProperty } = require('./lib/createuseractivitycp');

const { getMasterDimension } = require('./lib/getdim');
const { createMasterDimension } = require('./lib/createdim');
const { deleteMasterDimension } = require('./lib/deletedim');

const { getMasterMeasure } = require('./lib/getmeasure');
Expand All @@ -16,16 +15,18 @@ const { importMasterItemFromFile } = require('./lib/import-masteritem-excel');

const { scrambleField } = require('./lib/scramblefield');
const { getScript } = require('./lib/getscript');
const { getTask } = require('./lib/task/gettask');

const {
sharedParamAssertOptions,
userActivityCustomPropertyAssertOptions,
// userActivityCustomPropertyAssertOptions,
masterItemImportAssertOptions,
masterItemMeasureDeleteAssertOptions,
masterItemDimDeleteAssertOptions,
masterItemGetAssertOptions,
getScriptAssertOptions,
getBookmarkAssertOptions,
getTaskAssertOptions,
} = require('./lib/assert-options');

const program = new Command();
Expand Down Expand Up @@ -396,6 +397,69 @@ const program = new Command();
.option('--bookmark <bookmarks...>', 'bookmark to retrieve. If not specified all bookmarks will be retrieved')
.option('--output-format <json|table>', 'output format', 'json');

// Get tasks command
program
.command('task-get')
.description('get info about one or more tasks')
.action(async (options) => {
logger.verbose(`appid=${options.appId}`);
logger.verbose(`itemid=${options.itemid}`);

sharedParamAssertOptions(options);
getTaskAssertOptions(options);

getTask(options);
})
.addOption(
new Option('--log-level <level>', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info')
)
.requiredOption('--host <host>', 'Qlik Sense server IP/FQDN')
.option('--port <port>', 'Qlik Sense repository service (QRS) port', '4242')
.option('--schema-version <string>', 'Qlik Sense engine schema version', '12.612.0')
.requiredOption('--virtual-proxy <prefix>', 'Qlik Sense virtual proxy prefix', '')
.requiredOption('--secure <true|false>', 'connection to Qlik Sense engine is via https', true)
.requiredOption('--auth-user-dir <directory>', 'user directory for user to connect with')
.requiredOption('--auth-user-id <userid>', 'user ID for user to connect with')

.addOption(new Option('-a, --auth-type <type>', 'authentication type').choices(['cert']).default('cert'))
.option('--auth-cert-file <file>', 'Qlik Sense certificate file (exported from QMC)', './cert/client.pem')
.option('--auth-cert-key-file <file>', 'Qlik Sense certificate key file (exported from QMC)', './cert/client_key.pem')
.option('--auth-root-cert-file <file>', 'Qlik Sense root certificate file (exported from QMC)', './cert/root.pem')

.addOption(new Option('--task-type <type>', 'type of tasks to list').choices(['reload']).default('reload'))
.option('--task-id <ids...>', 'use task IDs to select which tasks to retrieve')
// .option('--task-tag <tags...>', 'use tags to select which tasks to retrieve')

.addOption(new Option('--output-format <format>', 'output format').choices(['table', 'tree']).default('tree'))
.addOption(new Option('--output-dest <dest>', 'where to send task info').choices(['screen', 'file']).default('screen'))
.addOption(new Option('--output-file-name <name>', 'file name to store task info in').default(''))
.addOption(new Option('--output-file-format <format>', 'file type/format').choices(['excel', 'csv', 'json']).default('excel'))
.option('--output-file-overwrite', 'overwrite output file without asking')

.addOption(new Option('--text-color <show>', 'use colored text in task views').choices(['yes', 'no']).default('yes'))

.option('--tree-icons', 'display task status icons in tree view')
.addOption(
new Option('--tree-details [detail...]', 'display details for each task in tree view')
.choices(['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream'])
.default('')
)

.addOption(
new Option('--table-details [detail...]', 'which aspects of tasks should be included in table view')
.choices([
'common',
'lastexecution',
'tag',
'customproperty',
'schematrigger',
'compositetrigger',
'comptimeconstraint',
'comprule',
])
.default('')
);

// Parse command line params
await program.parseAsync(process.argv);
})();
48 changes: 33 additions & 15 deletions src/globals.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const winston = require('winston');
const upath = require('upath');
const { promises: Fs } = require('fs');
const fs = require('fs');
require('winston-daily-rotate-file');

// Get app version from package.json file
Expand Down Expand Up @@ -43,27 +44,16 @@ const setLoggingLevel = (newLevel) => {
logTransports.find((transport) => transport.name === 'console').level = newLevel;
};

// Check file existence
async function exists(pathToCheck) {
try {
await Fs.access(pathToCheck);
return true;
} catch {
return false;
}
}

const verifyFileExists = (file) =>
const verifyFileExists = async (file) =>
// eslint-disable-next-line no-async-promise-executor, no-unused-vars
new Promise(async (resolve, reject) => {
try {
logger.debug(`Checking if file ${file} exists`);

const fileExists = await exists(file);

if (fileExists === true) {
try {
await Fs.access(file);
resolve(true);
} else {
} catch {
resolve(false);
}
} catch (err) {
Expand All @@ -72,6 +62,32 @@ const verifyFileExists = (file) =>
}
});

const generateXrfKey = () => {
let xrfString = '';
// eslint-disable-next-line no-plusplus
for (let i = 0; i < 16; i++) {
if (Math.floor(Math.random() * 2) === 0) {
xrfString += Math.floor(Math.random() * 10).toString();
} else {
const charNumber = Math.floor(Math.random() * 26);
if (Math.floor(Math.random() * 2) === 0) {
// lowercase letter
xrfString += String.fromCharCode(charNumber + 97);
} else {
xrfString += String.fromCharCode(charNumber + 65);
}
}
}
return xrfString;
};

/**
* Helper function to read the contents of the certificate files:
* @param {*} filename
* @returns
*/
const readCert = (filename) => fs.readFileSync(filename);

module.exports = {
logger,
appVersion,
Expand All @@ -80,4 +96,6 @@ module.exports = {
execPath,
isPkg,
verifyFileExists,
generateXrfKey,
readCert,
};
19 changes: 17 additions & 2 deletions src/lib/assert-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@ const sharedParamAssertOptions = (options) => {
logger.error('Mandatory option --auth-type is missing. Use it to specify how authorization with Qlik Sense will be done.');
process.exit(1);
}

// Verify that certificate files exists (if specified)
// TODO
};

const userActivityCustomPropertyAssertOptions = (options) => {
const newOptions = options;

// If certificate authentication is used: certs and user dir/id must be present.
if (options.authType === 'cert' && (!options.authUserDir || !options.authUserId)) {
logger.error('User directory and user ID are mandatory options when using certificate for authenticating with Sense');
process.exit(1);
}

// Ensure activity buckets are all integers
options.activityBuckets = options.activityBuckets.map( (item) => parseInt(item, 10));
newOptions.activityBuckets = options.activityBuckets.map((item) => parseInt(item, 10));
logger.verbose(`User activity buckets: ${options.activityBuckets}`);

// Sort activity buckets
options.activityBuckets.sort((a, b) => a - b);
return options;
return newOptions;
};

const masterItemImportAssertOptions = (options) => {
Expand Down Expand Up @@ -85,18 +90,27 @@ const masterItemDimDeleteAssertOptions = (options) => {
}
};

// eslint-disable-next-line no-unused-vars
const masterItemGetAssertOptions = (options) => {
//
};

// eslint-disable-next-line no-unused-vars
const getScriptAssertOptions = (options) => {
//
};

// eslint-disable-next-line no-unused-vars
const getBookmarkAssertOptions = (options) => {
//
};

// eslint-disable-next-line no-unused-vars
const getTaskAssertOptions = (options) => {
// TODO --task-id and --task-tag only for task tables, not trees

};

module.exports = {
sharedParamAssertOptions,
userActivityCustomPropertyAssertOptions,
Expand All @@ -106,4 +120,5 @@ module.exports = {
masterItemGetAssertOptions,
getScriptAssertOptions,
getBookmarkAssertOptions,
getTaskAssertOptions,
};
16 changes: 1 addition & 15 deletions src/lib/enigma.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
const SenseUtilities = require('enigma.js/sense-utilities');
const WebSocket = require('ws');
const fs = require('fs-extra');
const path = require('path');

const { logger, execPath, verifyFileExists } = require('../globals');
const { logger, execPath, verifyFileExists, readCert } = require('../globals');

/**
* Helper function to read the contents of the certificate files:
* @param {*} filename
* @returns
*/
const readCert = (filename) => fs.readFileSync(filename);

/**
*
* @param {*} options
* @param {*} command
* @returns
*/
const setupEnigmaConnection = async (options) => {
logger.debug('Prepping for Enigma connection...');

Expand Down
57 changes: 57 additions & 0 deletions src/lib/lookups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const mapDaylightSavingTime = new Map([
[0, 'ObserveDaylightSavingTime'],
[1, 'PermanentStandardTime'],
[2, 'PermanentDaylightSavingTime'],
]);

const mapEventType = new Map([
[0, 'Schema'],
[1, 'Composite'],
]);

const mapIncrementOption = new Map([
[0, 'once'],
[1, 'hourly'],
[2, 'daily'],
[3, 'weekly'],
[4, 'monthly'],
[5, 'custom'],
]);

const mapRuleState = new Map([
[0, 'Undefined'],
[1, 'TaskSuccessful'],
[2, 'TaskFail'],
]);

const mapTaskExecutionStatus = new Map([
[0, 'NeverStarted'],
[1, 'Triggered'],
[2, 'Started'],
[3, 'Queued'],
[4, 'AbortInitiated'],
[5, 'Aborting'],
[6, 'Aborted'],
[7, 'FinishedSuccess'],
[8, 'FinishedFail'],
[9, 'Skipped'],
[10, 'Retry'],
[11, 'Error'],
[12, 'Reset'],
]);

const mapTaskType = new Map([
[0, 'Reload'],
[1, 'ExternalProgram'],
[2, 'UserSync'],
[3, 'Distribute'],
]);

module.exports = {
mapDaylightSavingTime,
mapEventType,
mapIncrementOption,
mapRuleState,
mapTaskExecutionStatus,
mapTaskType,
};
38 changes: 38 additions & 0 deletions src/lib/qrs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const https = require('https');

const { logger, generateXrfKey, readCert } = require('../globals');

const setupQRSConnection = (options, param) => {
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
cert: readCert(param.fileCert),
key: readCert(param.fileCertKey),
});

// Set up Sense repository service configuration
const xrfKey = generateXrfKey();

const axiosConfig = {
url: `${param.path}?xrfkey=${xrfKey}`,
method: 'get',
baseURL: `https://${options.host}:${options.port}`,
headers: {
'x-qlik-xrfkey': xrfKey,
'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_api',
},
responseType: 'application/json',
httpsAgent,
timeout: 60000,
// passphrase: "YYY"
};

if (param.filter?.length > 0) {
axiosConfig.url += `&filter=${param.filter}`;
}

return axiosConfig;
};

module.exports = {
setupQRSConnection,
};

0 comments on commit 9c1e0c9

Please sign in to comment.