Skip to content

Commit

Permalink
WIP towards #353, session handling commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Göran Sander committed Mar 7, 2024
1 parent 7684c2b commit e5ceae9
Show file tree
Hide file tree
Showing 9 changed files with 836 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ qvfs
qvfs_1
qvfs_2
qvfs_3
qvfs_tmp

src/node_modules/*

Expand Down
82 changes: 82 additions & 0 deletions src/ctrl-q.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import importAppFromFile from './lib/cmd/importapp.js';
import exportAppToFile from './lib/cmd/exportapp.js';
import testConnection from './lib/cmd/testconnection.js';
import visTask from './lib/cmd/vistask.js';
import getSessions from './lib/cmd/getsessions.js';
import deleteSessions from './lib/cmd/deletesessions.js';

import {
sharedParamAssertOptions,
Expand All @@ -38,6 +40,8 @@ import {
taskImportAssertOptions,
appImportAssertOptions,
appExportAssertOptions,
getSessionsAssertOptions,
deleteSessionsAssertOptions,
} from './lib/util/assert-options.js';

const program = new Command();
Expand Down Expand Up @@ -849,6 +853,84 @@ const program = new Command();
.option('--vis-host <host>', 'host for visualisation server', 'localhost')
.option('--vis-port <port>', 'port for visualisation server', '3000');

// Get proxy sessions
program
.command('sessions-get')
.description('get info about proxy sessions on one or more virtual proxies')
.action(async (options) => {
await sharedParamAssertOptions(options);
await getSessionsAssertOptions(options);

getSessions(options, null);
})
.addOption(
new Option('--log-level <level>', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info')
)

.requiredOption('--host <host>', 'Qlik Sense host (IP/FQDN) where Qlik Repository Service (QRS) is running')
.requiredOption('--virtual-proxy <prefix>', 'Qlik Sense virtual proxy prefix to access QRS via', '')
.option('--qrs-port <port>', 'Qlik Sense repository service (QRS) port (usually 4242)', '4242')

.option('--session-virtual-proxy <prefix...>', 'one or more Qlik Sense virtual proxies to get sessions for')
.option(
'--host-proxy <host...>',
'Qlik Sense hosts/proxies (IP/FQDN) to get sessions from. Must match the host names of the Sense nodes'
)
.option('--qps-port <port>', 'Qlik Sense proxy service (QPS) port (usually 4243)', '4243')

.requiredOption('--secure <true|false>', 'connection to Qlik Sense repository service 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')

.option('--output-format <json|table>', 'output format', 'json')

.addOption(
new Option('-s, --sort-by <column>', 'column to sort output table by')
.choices(['prefix', 'proxyhost', 'proxyname', 'userdir', 'userid', 'username'])
.default('prefix')
);

// Delete proxy sessions
program
.command('sessions-delete')
.description('delete proxy session(s) on a specific virtual proxy and proxy service')
.action(async (options) => {
await sharedParamAssertOptions(options);
await deleteSessionsAssertOptions(options);

deleteSessions(options);
})
.addOption(
new Option('--log-level <level>', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']).default('info')
)
.requiredOption('--host <host>', 'Qlik Sense host (IP/FQDN) where Qlik Repository Service (QRS) is running')
.requiredOption('--virtual-proxy <prefix>', 'Qlik Sense virtual proxy prefix to access QRS via', '')
.option('--qrs-port <port>', 'Qlik Sense repository service (QRS) port (usually 4242)', '4242')

.option('--session-id <id...>', 'session IDs to delete')
.requiredOption('--session-virtual-proxy <prefix>', 'Qlik Sense virtual proxy (prefix) to delete proxy session(s) on', '')
.requiredOption(
'--host-proxy <host>',
'Qlik Sense proxy (IP/FQDN) where sessions should be deleted. Must match the host name of a Sense node'
)
.option('--qps-port <port>', 'Qlik Sense proxy service (QPS) port (usually 4243)', '4243')

.requiredOption('--secure <true|false>', 'connection to Qlik Sense repository service 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');

// .option('--dry-run', 'do a dry run, i.e. do not delete any sessions - just show what would be deleted')

// Parse command line params
await program.parseAsync(process.argv);
})();
36 changes: 36 additions & 0 deletions src/lib/cmd/deletesessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { deleteSessionsFromQSEoWIds } from '../util/session.js';
import { logger, setLoggingLevel, isPkg, execPath } from '../../globals.js';
import { catchLog } from '../util/log.js';

/**
* Delete Qlik Sense proxy sessions
* @param {object} options - Options object
*/
const deleteSessions = async (options) => {
try {
// Set log level
setLoggingLevel(options.logLevel);

logger.verbose(`Ctrl-Q was started as a stand-alone binary: ${isPkg}`);
logger.verbose(`Ctrl-Q was started from ${execPath}`);

logger.info('Delete Qlik Sense proxy sessions');
logger.debug(`Options: ${JSON.stringify(options, null, 2)}`);

logger.info(`Deleting sessions on proxy "${options.hostProxy}", virtual proxy "${options.sessionVirtualProxy}"`);
const deleteResult = await deleteSessionsFromQSEoWIds(options);

if (deleteResult === false) {
logger.error('Error deleting proxy sessions.');
return false;
}

return true;
} catch (err) {
catchLog(`Error deleting proxy sessions from host "${options.hostProxy}", virtual proxy "${options.sessionVirtualProxy}"`, err);

return false;
}
};

export default deleteSessions;
194 changes: 194 additions & 0 deletions src/lib/cmd/getsessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { table } from 'table';
import { getSessionsFromQseow } from '../util/session.js';
import { logger, setLoggingLevel, isPkg, execPath } from '../../globals.js';
import { catchLog } from '../util/log.js';

const consoleTableConfig = {
border: {
topBody: `─`,
topJoin: `┬`,
topLeft: `┌`,
topRight: `┐`,

bottomBody: `─`,
bottomJoin: `┴`,
bottomLeft: `└`,
bottomRight: `┘`,

bodyLeft: `│`,
bodyRight: `│`,
bodyJoin: `│`,

joinBody: `─`,
joinLeft: `├`,
joinRight: `┤`,
joinJoin: `┼`,
},
columns: {
// 3: { width: 40 },
// 4: { width: 100 },
// 5: { width: 30 },
// 6: { width: 30 },
},
};

/**
*
* @param {*} options
*/
const getSessions = async (options) => {
try {
// Set log level
setLoggingLevel(options.logLevel);

logger.verbose(`Ctrl-Q was started as a stand-alone binary: ${isPkg}`);
logger.verbose(`Ctrl-Q was started from ${execPath}`);

logger.info('Get Qlik Sense proxy sessions');
logger.debug(`Options: ${JSON.stringify(options, null, 2)}`);

const sessionDataArray = await getSessionsFromQseow(options, null);

if (sessionDataArray === false || sessionDataArray === undefined) {
logger.error(`Error getting proxy sessions from from QSEoW`);
return false;
}

// Build table or json, depending on output format
if (options.outputFormat === 'table') {
const sessionsTable = [];
sessionsTable.push([
'Virtual proxy description',
'Virtual proxy prefix',
'Virtual proxy session cookie header',
'Linked proxy service',
'Load balancing nodes',
'Session user directory',
'Session user ID',
'Session user name',
'Session attributes',
'Session ID',
]);

// Get total number of sessions
// Sum the number of entries in each sessionDataArray.sessions array
const totalSessions = sessionDataArray.reduce((acc, vp) => acc + vp.sessions.length, 0);

// Get sessions per proxy host
// First get all unique proxy hosts
const uniqueProxyHosts = [...new Set(sessionDataArray.map((vp) => vp.hostProxy))];
const sessionsPerProxyHost = uniqueProxyHosts.map((host) => {
const sessions = sessionDataArray.filter((vp) => vp.hostProxy === host).reduce((acc, vp) => acc + vp.sessions.length, 0);
return { host, sessions };
});

// Get mapping between proxy host and proxy host name
// Reduce to uniqur mappings
const proxyHostNameMap = sessionDataArray.map((vp) => ({ hostProxy: vp.hostProxy, hostProxyName: vp.hostProxyName }));
const uniqueProxyHostNameMap = proxyHostNameMap.filter(
(vp, index, self) => index === self.findIndex((t) => t.hostProxy === vp.hostProxy)
);

// Build text for table header
let headerText = `-- Sessions per virtual proxy and proxy services --\n\nTotal number of sessions: ${totalSessions}\n\n`;

// Add sessions per proxy host
headerText += 'Sessions per proxy service:\n';
sessionsPerProxyHost.forEach((p) => {
// Get name of proxy host
const proxyHostName = uniqueProxyHostNameMap.find((m) => m.hostProxy === p.host).hostProxyName;
headerText += ` ${proxyHostName}: ${p.host}: ${p.sessions}\n`;
});

consoleTableConfig.header = {
alignment: 'left',
content: headerText,
};

// Expand all session data into a single array of objects to make later sorting and filtering easier
const sessionsTabular = [];
sessionDataArray.forEach((vp) => {
vp.sessions.forEach((s) => {
const proxyHostName = uniqueProxyHostNameMap.find((m) => m.hostProxy === vp.hostProxy).hostProxyName;
const vpLoadBalancingNodes = vp.virtualproxy.loadBalancingServerNodes
.map((node) => `${node.name}: ${node.hostName}`)
.join('\n');

// session.Attributes is an array, where each element is an object with a single key-value pair
const attributes = s.Attributes.map((a) => {
// Convert object to string on the format "key: value"
const attr = Object.keys(a).map((key) => `${key}: ${a[key]}`)[0];

return attr;
}).join('\n');

sessionsTabular.push({
vpDescription: vp.virtualproxy.description,
vpPrefix: vp.virtualproxy.prefix,
vpSessionCookieHeaderName: vp.virtualproxy.sessionCookieHeaderName,
proxyHost: vp.hostProxy,
proxyName: proxyHostName,
proxyFull: `${proxyHostName}:\n${vp.hostProxy}`,
loadBalancingNodes: vpLoadBalancingNodes,
userDir: s.UserDirectory,
userId: s.UserId,
userName: s.UserName === undefined || s.UserName === null ? '' : s.UserName,
attributes,
sessionId: s.SessionId,
});
});
});

// Sort the sessionDataArray as specified in the options.sortBy option
// Possible values are: 'prefix', 'proxyhost', 'proxyname'
if (options.sortBy !== undefined && options.sortBy !== null && options.sortBy !== '') {
if (options.sortBy === 'prefix') {
sessionsTabular.sort((a, b) => a.vpPrefix.localeCompare(b.vpPrefix));
} else if (options.sortBy === 'proxyhost') {
sessionsTabular.sort((a, b) => a.proxyHost.localeCompare(b.proxyHost));
} else if (options.sortBy === 'proxyname') {
sessionsTabular.sort((a, b) => a.proxyName.localeCompare(b.proxyName));
} else if (options.sortBy === 'userdir') {
sessionsTabular.sort((a, b) => a.userDir.localeCompare(b.userDir));
} else if (options.sortBy === 'userid') {
sessionsTabular.sort((a, b) => a.userId.localeCompare(b.userId));
} else if (options.sortBy === 'username') {
sessionsTabular.sort((a, b) => a.userName.localeCompare(b.userName));
}
} else {
logger.warn('--sort-by option is invalid. Use default sorting.');
}

// Add to table that will be printed to console
// eslint-disable-next-line no-restricted-syntax
for (const s of sessionsTabular) {
sessionsTable.push([
s.vpDescription,
s.vpPrefix,
s.vpSessionCookieHeaderName,
s.proxyFull,
s.loadBalancingNodes,
s.userDir,
s.userId,
s.userName,
s.attributes,
s.sessionId,
]);
}

// Print table to console
logger.info(`\n${table(sessionsTable, consoleTableConfig)}`);
} else {
logger.error('Invalid --output-format option');
return false;
}

return sessionDataArray;
} catch (err) {
catchLog(`Error getting proxy sessions from host ${options.host}`, err);

return false;
}
};

export default getSessions;
13 changes: 11 additions & 2 deletions src/lib/util/assert-options.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'path';
import { version as uuidVersion } from 'uuid';
import { validate as uuidValidate } from 'uuid';
import { version as uuidVersion,validate as uuidValidate } from 'uuid';
import fs from 'fs';
import { logger, execPath, mergeDirFilePath, verifyFileExists } from '../../globals.js';

Expand Down Expand Up @@ -294,3 +293,13 @@ export const variableDeleteAssertOptions = (options) => {
process.exit(1);
}
};

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

// eslint-disable-next-line no-unused-vars
export const deleteSessionsAssertOptions = (options) => {
//
}
Loading

0 comments on commit e5ceae9

Please sign in to comment.