UNPKG

netconf-client

Version:
483 lines 16.5 kB
// import * as getoptsImport from 'getopts'; import * as getoptsImport from 'getopts'; import * as packageJson from '../../package.json' with { type: 'json' }; import { GetDataResultType } from "../lib/index.js"; import { showHelp } from "./help.js"; import { Output } from "./output.js"; import { parseConnStr } from "./parse-conn-str.js"; export const DEFAULT_USER = 'admin'; export const DEFAULT_PASS = 'admin'; export const DEFAULT_PORT = 2022; export const DEFAULT_XPATH = '/'; const OPERATION_ALIASES = { add: 'create', cre: 'create', rem: 'delete', del: 'delete', rep: 'replace', sub: 'subscribe', rpc: 'rpc', exec: 'rpc', }; export var OperationType; (function (OperationType) { /** * Print hello message and exit */ OperationType["HELLO"] = "hello"; /** * get-data operation */ OperationType["GET"] = "get"; /** * edit-config, nc:operation="merge" */ OperationType["MERGE"] = "merge"; /** * edit-config, nc:operation="create" */ OperationType["CREATE"] = "create"; /** * edit-config, nc:operation="delete" */ OperationType["DELETE"] = "delete"; /** * edit-config, nc:operation="replace" */ OperationType["REPLACE"] = "replace"; /** * subscribe to notifications */ OperationType["SUBSCRIBE"] = "subscribe"; /** * arbitrary rpc exec */ OperationType["RPC"] = "rpc"; })(OperationType || (OperationType = {})); export var ResultFormat; (function (ResultFormat) { ResultFormat["JSON"] = "json"; ResultFormat["XML"] = "xml"; ResultFormat["YAML"] = "yaml"; /** * all data is printed in key=value format (nested keys are joined with a dot) */ ResultFormat["KEYVALUE"] = "keyvalue"; /** * all data is printed in tree format, useful for reviewing the data */ ResultFormat["TREE"] = "tree"; })(ResultFormat || (ResultFormat = {})); /** * Parse command line arguments * * @returns If parsing was successful, an object with the parsed options is returned. In case when help or version * options are provided, the function will return undefined and the program must exit with 0 (success) code. * If parsing fails, the function will throw an error. */ // eslint-disable-next-line max-lines-per-function, sonarjs/cognitive-complexity export async function parseArgs() { let args = process.argv.slice(2); const getopts = getoptsImport.default; const opt = getopts(args, { alias: { allowmultiple: ['allow-multiple'], b: ['beforekey', 'before-key'], config: ['configonly', 'config-only'], f: ['fulltree', 'full-tree'], h: 'help', H: 'host', j: 'json', k: ['keyvalue', 'key-value'], p: 'port', P: 'pass', readonly: 'read-only', schema: ['schemaonly', 'schema-only'], s: ['shownamespaces', 'show-namespaces'], state: ['stateonly', 'state-only'], stdin: ['stdin'], U: 'user', v: 'version', V: 'verbose', x: 'xml', y: 'yaml', }, default: { allowmultiple: false, beforekey: undefined, configonly: false, fulltree: false, host: undefined, json: false, keyvalue: false, namespace: undefined, pass: undefined, port: undefined, readonly: false, schemaonly: false, stateonly: false, stdin: false, user: undefined, shownamespaces: false, xml: false, yaml: false, hello: false, }, // eslint-disable-next-line id-denylist boolean: [ 'allowmultiple', 'configonly', 'fulltree', 'help', 'json', 'keyvalue', 'read-only', 'schemaonly', 'shownamespaces', 'stateonly', 'stdin', 'version', 'verbose', 'xml', 'yaml', 'hello', ], unknown: (optionName) => { if (optionName.startsWith('xmlns')) { return true; } else { throw new Error(`Unknown option: ${optionName}`); } }, }); if (opt.help) { showHelp(); return undefined; } if (opt.version) { console.info(packageJson.default.version); return undefined; } // Check verbose level. If it's an array, use the length, otherwise use 1 if it's true, 0 otherwise // Immediatly set the verbose level to the Output class. let verbose = 0; if (Array.isArray(opt.verbose)) { verbose = opt.verbose.length; } else { verbose = opt.verbose ? 1 : 0; } if (verbose > 0) { Output.verbosity = verbose; Output.debug(`Verbose output enabled, level ${verbose}`); } let xpath; let conn; let stream; let operationType; const keyValuePairs = {}; let listItems = []; // Parsing command line arguments and getting the requested XPath and credentials (if provided) args = opt._.filter(arg => arg !== ''); while (args.length) { // Operation const op = args[0].substring(0, 3).toLowerCase(); if (op in OPERATION_ALIASES) { const normalizedOp = OPERATION_ALIASES[op]; operationType = normalizedOp; args.shift(); // XPath } else if (args[0].substring(0, 1) === '/') { xpath = args.shift(); // Test if the argument is a list item } else if (args[0].startsWith('[')) { // list item if (listItems.length) { throw new Error('List items can only be provided once'); } listItems = addListItems(args.shift()); // Test if the argument is a var=val } else if (args[0].includes('=')) { // var=val pushKeyValuePair(keyValuePairs, args.shift()); // Connection string } else { if (!conn && !operationType) { conn = args.shift(); } else { stream = args.shift(); } } } if (opt.stdin) { // read from stdin const stdin = process.stdin; stdin.setEncoding('utf8'); stdin.on('data', (data) => { const lines = data.split('\n'); lines.forEach(line => { if (line.includes('=')) pushKeyValuePair(keyValuePairs, line); }); }); // await for stdin eof await new Promise(resolve => stdin.on('end', resolve)); } if (listItems.length && Object.keys(keyValuePairs).length) { throw new Error('Cannot mix list items and key-value pairs'); } // Get connection arguments const connArgs = getConnectionArgs(opt, conn); // Determine the operation type to be performed if (opt.hello) { operationType = OperationType.HELLO; } else if (Object.keys(keyValuePairs).length && operationType === undefined) { // If there are key-value pairs, and the operation is not set, change the operation to MERGE operationType = OperationType.MERGE; } else if (operationType === undefined) { // If operation is not set, assume GET operationType = OperationType.GET; if (listItems.length) { throw new Error('List items can only be provided for create and delete operations'); } } if (Number(opt['config-only']) + Number(opt['state-only']) + Number(opt['schema-only']) > 1) { throw new Error('Cannot mix --config-only, --state-only and --schema-only'); } const operationMap = { [OperationType.HELLO]: () => ({ type: OperationType.HELLO }), [OperationType.GET]: (x) => ({ type: OperationType.GET, options: { xpath: x ?? DEFAULT_XPATH, configFilter: opt['config-only'] ? GetDataResultType.CONFIG : opt['state-only'] ? GetDataResultType.STATE : opt['schema-only'] ? GetDataResultType.SCHEMA : undefined, fullTree: opt['full-tree'] || opt['show-namespaces'], showNamespaces: opt['show-namespaces'], }, }), [OperationType.MERGE]: (x) => ({ type: OperationType.MERGE, options: { xpath: x ?? DEFAULT_XPATH, values: keyValuePairs, allowMultiple: opt['allow-multiple'], }, }), [OperationType.CREATE]: (x) => ({ type: OperationType.CREATE, options: { xpath: x ?? DEFAULT_XPATH, editConfigValues: Object.keys(keyValuePairs).length ? { type: 'keyvalue', values: keyValuePairs, } : { type: 'list', values: listItems, }, beforeKey: opt['before-key'], allowMultiple: opt['allow-multiple'], }, }), [OperationType.DELETE]: (x) => ({ type: OperationType.DELETE, options: { xpath: x ?? DEFAULT_XPATH, editConfigValues: Object.keys(keyValuePairs).length ? { type: 'keyvalue', values: keyValuePairs, } : { type: 'list', values: listItems, }, allowMultiple: opt['allow-multiple'], }, }), [OperationType.REPLACE]: (x) => ({ type: OperationType.REPLACE, options: { xpath: x ?? DEFAULT_XPATH, editConfigValues: Object.keys(keyValuePairs).length ? { type: 'keyvalue', values: keyValuePairs, } : { type: 'list', values: listItems, }, allowMultiple: opt['allow-multiple'], }, }), [OperationType.SUBSCRIBE]: (x) => ({ type: OperationType.SUBSCRIBE, options: stream ? { type: 'stream', stream, } : { type: 'xpath', xpath: x ?? DEFAULT_XPATH, }, }), [OperationType.RPC]: (x) => ({ type: OperationType.RPC, options: { cmd: x ?? DEFAULT_XPATH, values: keyValuePairs, }, }), }; const operation = operationMap[operationType](xpath); if (Number(opt.json) + Number(opt.xml) + Number(opt.yaml) > 1) { throw new Error('Cannot mix --json, --xml and --yaml'); } const namespaces = []; Object.keys(opt).forEach(key => { if (key.startsWith('xmlns')) { if (key.includes(':')) { const idx = key.indexOf(':'); const alias = key.substring(idx + 1); if (Array.isArray(opt[key])) { throw new Error(`Namespace provided twice (${key})`); } const uri = opt[key]; namespaces.push({ alias, uri }); } else { if (Array.isArray(opt[key])) { throw new Error(`Namespace provided twice (${key})`); } namespaces.push(opt[key]); } } }); const cliOptions = { host: connArgs.host, port: connArgs.port ?? DEFAULT_PORT, user: connArgs.user ?? DEFAULT_USER, pass: connArgs.pass ?? DEFAULT_PASS, operation, namespaces, readOnly: opt['read-only'], resultFormat: opt.json ? ResultFormat.JSON : opt.xml ? ResultFormat.XML : opt.yaml ? ResultFormat.YAML : opt.keyvalue ? ResultFormat.KEYVALUE : ResultFormat.TREE, }; return cliOptions; } function pushKeyValuePair(obj, argument) { const idx = argument.indexOf('='); const key = argument.substring(0, idx).trim(); const val = argument.substring(idx + 1); if (key.length) setNestedValue(obj, key, val); } /** * Set a nested value in an object using a dot notation * * @param obj - The object to set the value in * @param key - The key to set the value in, use `/` for nested properties, for example, 'a/b/c' should result into object with * nested properties a, a.b and a.b.c * @param value - The value to set */ // eslint-disable-next-line sonarjs/cognitive-complexity function setNestedValue(obj, key, value) { // double slash/wildcard is not allowed if (key.includes('//') || key.includes('*')) { throw new Error('Cannot use double slash or wildcard in key'); } // remove leading and trailing slashes if (key.startsWith('/')) key = key.substring(1); if (key.endsWith('/')) key = key.substring(0, key.length - 1); const keys = key.split('/'); let current = obj; for (let i = 0; i < keys.length; i++) { const arrayMatch = keys[i].match(/^(.+)\[(\d+)]$/); if (arrayMatch) { const arrayKey = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10) - 1; // Convert to zero-based index if (!current[arrayKey]) { current[arrayKey] = []; } else if (!Array.isArray(current[arrayKey])) { throw new Error(`Expected ${arrayKey} to be an array`); } // Ensure the array has enough elements while (current[arrayKey].length <= arrayIndex) { current[arrayKey].push({}); } if (i === keys.length - 1) { current[arrayKey][arrayIndex] = value; } else { current = current[arrayKey][arrayIndex]; } } else { if (i === keys.length - 1) { current[keys[i]] = value; } else { if (!current[keys[i]] || typeof current[keys[i]] !== 'object') { current[keys[i]] = {}; } current = current[keys[i]]; } } } } function addListItems(argument) { if (!RegExp(/^\[.*]$/).test(argument)) { throw new Error('Invalid list, List must be enclosed in square brackets'); } // remove square brackets const val = argument.trim().substring(1, argument.length - 1); // split on comma return val.split(',').map(item => item.trim()); } /** * Get connection arguments from environment or command line arguments or connection string * Command line arguments override environment variables. * Connection string overrides command line arguments. * * @param opt - Command line options * @param connStr - Connection string, if present in command line arguments * @returns Connection arguments */ function getConnectionArgs(opt, connStr) { const envConfig = removeUndefined({ host: process.env.NETCONF_HOST, port: process.env.NETCONF_PORT ? parseInt(process.env.NETCONF_PORT, 10) : undefined, user: process.env.NETCONF_USER, pass: process.env.NETCONF_PASS, }); const connConfig = connStr ? removeUndefined(parseConnStr(connStr)) : {}; const argsConfig = removeUndefined({ host: opt.host, port: opt.port, user: opt.user, pass: opt.pass, }); const config = { ...envConfig, ...argsConfig, ...connConfig, }; if (!config.host) { throw new Error('Host is not provided. Use -H flag, NETCONF_HOST environment variable, or connection string.'); } return { host: config.host, port: config.port, user: config.user, pass: config.pass, }; } function removeUndefined(obj) { return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)); } //# sourceMappingURL=parse-args.js.map