UNPKG

netconf-client

Version:
229 lines 10.4 kB
import { map, of, switchMap, tap } from 'rxjs'; import { Output } from "../cli/output.js"; /** * Build a configuration object for Netconf edit-config RPC based on the XPath filter. * The class takes the XPath filter and the schema observable and builds the configuration object * that can be used in edit-config RPC. */ export class NetconfBuildConfig { xpath; schema; namespace; guessNamespace; get hasNamespace() { return this.namespace !== undefined && (typeof this.namespace === 'string' || (Array.isArray(this.namespace) && this.namespace.length > 0)); } /** * Create a new BuildEditConfig object. * * @param {string} xpath - XPath filter of the configuration object, * for example, //interfaces/interface[name="eth1"] * @param {Observable<NetconfType>} schema - Observable that emits the schema of the device. * The class may use it if the XPath filter is not straightforward. * @param {string | (string | NamespaceType)[]} namespace - Optional target namespace for the configuration object. * it can also be an array of namespaces with their aliases, see {@link NamespaceType}. */ constructor(xpath, schema, namespace, guessNamespace) { xpath = xpath.trim(); if (!xpath || xpath === '/' || xpath === '//') { throw new Error('XPath for edit-config must contain at least one element'); } if (xpath.includes('|')) { throw new Error('XPath for edit-config must not contain |'); } this.xpath = xpath; this.schema = schema; this.namespace = namespace; this.guessNamespace = guessNamespace; } /** * Build a configuration object for Netconf edit-config RPC. The function will return an array of configuration * objects, that is, the object where configuration must be updated according to the xpath filter. * For example, if Xpath filter is //interfaces/interface[name="eth1"], the function will return an array of * objects, containing the interface with name eth1 (array of one object). * * @param {NetconfType} targetObj - The root configuration object from which to build the configuration based * on the XPath filter. * * @returns {NetconfType[]} Array of objects where configuration must be updated, for example, * for the XPath //interfaces/interface[name="eth1"], the function will return an array of one object * containing the interface with name eth1. The returned objects are nested in the targetObj. * The function returns an empty array if: * - the schema is not provided. * - the XPath filter cannot be matched with schema of XPath */ build(targetObj) { if (!(this.xpath.includes('//') || this.xpath.includes('*'))) { // Try to build based on XPath first return this.buildFromXPath(targetObj).pipe(switchMap(result => { if (result !== undefined) { return of(result); } if (this.schema === undefined) { return of([]); } return this.buildFromSchema(this.schema, targetObj); })); } if (this.schema) { return this.buildFromSchema(this.schema, targetObj); } return of([]); } /** * Build a configuration object for Netconf edit-config RPC based on the XPath filter. The function will * attempt to build the configuration object from the XPath provided. If the XPath has features not supported * by the function, the function will return undefined. The function only supports full XPath with * predicates for matching nodes. * * @param {NetconfType} targetObj - The root configuration object from which to build the configuration based * on the XPath filter. * * @returns {NetconfType[] | undefined} Array including the configuration object that needs to be updated. */ buildFromXPath(targetObj) { const VALID_SEGMENT = /^([\w.:\-]+)(?:\[([^=@]+)=(["'])([^"'\]]+)\3])?$/; // Split the XPath filter into tokens const xpathSegments = this.xpath.split('/').filter(x => x !== ''); // Check each segment is valid for (const segment of xpathSegments) { if (!VALID_SEGMENT.test(segment)) { return of(undefined); } } // Observable for namespace, requesting server if needed const namespace = this.hasNamespace ? of(this.getNamespaces()) : this.guessNamespace ? of(null).pipe(tap(() => Output.debug('Sending request to the server to guess the namespace')), switchMap(() => this.guessNamespace ? this.guessNamespace : of(undefined)), map(ns => ns ? { xmlns: ns } : undefined)) : of(undefined); return namespace.pipe(map(ns => { let current = targetObj; for (let i = 0; i < xpathSegments.length; i++) { const segment = xpathSegments[i]; const match = segment.match(VALID_SEGMENT); if (!match) return undefined; const [, tag, predKey, , predVal] = match; current[tag] = i === 0 && ns ? { $: ns } : {}; if (predKey && predVal) { current[tag][predKey] = predVal; } current = current[tag]; } return [current]; })); } /** * Build a configuration object for Netconf edit-config RPC based on the schema. The function will * request the schema from the server using the xpath filter and build the configuration object based on the schema. * * @param {NetconfType} targetObj - The root configuration object from which to build the configuration based * on the XPath filter. * * @returns {NetconfType[]} Array of objects where configuration must be updated. */ buildFromSchema(schema, targetObj) { // remove leading / or // let xpath = this.xpath.replace(/^\/+/, ''); // convert wildcard to * xpath = xpath.replace(/\/\//g, '/*/'); // merge */* into * xpath = xpath.replace(/\*\/\*/g, '*'); // remove any predicates/matching square brackets from the xpath const regExp = /\[[^[\]]*]/; while (regExp.test(xpath)) { xpath = xpath.replace(regExp, ''); } const xpathSegments = xpath.split('/'); Output.debug('Build configuration object based on leaf schema'); Output.debug('Sending request to the server to get the leaf schema'); return schema.pipe(map((schemaObj) => { // Deep copy schema into targetObj Object.assign(targetObj, structuredClone(schemaObj)); // match the schema with the xpath and return the config parts if (targetObj.$?.xmlns) delete targetObj.$.xmlns; return this.findConfigPartsInSchema(targetObj, xpathSegments); })); } findConfigPartsInSchema(schema, xpathSteps) { const configParts = []; // Helper function to recursively search the schema // eslint-disable-next-line sonarjs/cognitive-complexity const searchSchema = (currentObj, steps, step) => { if (steps.length === 0) { this.stripNestedObjects(currentObj); configParts.push(currentObj); return true; } if (steps.length === 1 && steps[0] === '*' && !Array.isArray(currentObj)) { this.stripNestedObjects(currentObj); configParts.push(currentObj); return true; } if (typeof currentObj !== 'object' || currentObj === null) { return false; } // Iterate through the properties of currentObj let branchPassed = false; for (const key of Object.keys(currentObj)) { const isWildcardMatch = steps[0] === '*' && steps.length > 1 && key === steps[1]; // let twigPassed = false; if (key === steps[0] || isWildcardMatch) { const newSteps = isWildcardMatch ? steps.slice(1) : steps; // If this is out object, but it is an array, convert it to an object if (newSteps.length === 1 && Array.isArray(currentObj[key])) { currentObj[key] = {}; } branchPassed = searchSchema(currentObj[key], newSteps.slice(1), step + 1) || branchPassed; } else if (steps[0] === '*' && steps.length === 1) { branchPassed = searchSchema(currentObj[key], [], step + 1) || branchPassed; } else { branchPassed = searchSchema(currentObj[key], steps, step + 1) || branchPassed; } if (!branchPassed && (Array.isArray(currentObj[key]) || typeof currentObj[key] === 'object') && key !== '$') { delete currentObj[key]; } } if (step === 1 && this.hasNamespace) { currentObj.$ = { ...currentObj.$ ?? {}, ...this.getNamespaces(), }; } return branchPassed; }; // Start the recursive search searchSchema(schema, xpathSteps, 0); return configParts; } stripNestedObjects = (obj) => { if (typeof obj === 'object' && obj !== null) { Object.keys(obj).forEach(key => { if (obj[key] && typeof obj[key] === 'object' && key !== '$') { delete obj[key]; } }); } }; getNamespaces() { if (typeof this.namespace === 'string') { return { xmlns: this.namespace }; } return this.namespace?.reduce((acc, curr) => { if (typeof curr === 'string') { acc.xmlns = curr; } else { acc[`xmlns:${curr.alias}`] = curr.uri; } return acc; }, {}); } } //# sourceMappingURL=netconf-build-config.js.map