netconf-client
Version:
483 lines • 16.5 kB
JavaScript
// 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