From 3770d8ab6ed7c7b31b8899f1216798f6a4ff6025 Mon Sep 17 00:00:00 2001 From: Wisotzky Sven Date: Thu, 4 Jul 2024 12:16:56 +0200 Subject: [PATCH 1/3] state fixed for ipl!nk/graal --- templates/common_resources_graaljs/common/IntentLogic.mjs | 1 - templates/ngJS iplink/intent-type-resources/state.ftl | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/common_resources_graaljs/common/IntentLogic.mjs b/templates/common_resources_graaljs/common/IntentLogic.mjs index 7f5f553..5126562 100644 --- a/templates/common_resources_graaljs/common/IntentLogic.mjs +++ b/templates/common_resources_graaljs/common/IntentLogic.mjs @@ -53,7 +53,6 @@ export class IntentLogic { * * @param {string} target Intent target * @param {Dict} config Intent configuration - * @param {string} neId Target device * @returns {Dict} site-level settings (site.*) */ diff --git a/templates/ngJS iplink/intent-type-resources/state.ftl b/templates/ngJS iplink/intent-type-resources/state.ftl index 524aaa6..8b6b09f 100644 --- a/templates/ngJS iplink/intent-type-resources/state.ftl +++ b/templates/ngJS iplink/intent-type-resources/state.ftl @@ -1,5 +1,5 @@ - + <{{ intent_type }}-state xmlns="http://www.nokia.com/management-solutions/{{ intent_type }}"> ${state.subnet} <#if indicators.state??> ${indicators.state?values[0]} @@ -17,5 +17,5 @@ ${indicators.utilization?values[0]?c} - + From d4124abcad66b08d71e0c0128ade86eb7667ff6b Mon Sep 17 00:00:00 2001 From: Wisotzky Sven Date: Thu, 4 Jul 2024 12:17:50 +0200 Subject: [PATCH 2/3] (WIP) adding validation for viewconfig using schema-form --- media/viewconfig-schema.json | 484 +++++++++++++++++++++++++ src/providers/IntentManagerProvider.ts | 29 ++ 2 files changed, 513 insertions(+) create mode 100644 media/viewconfig-schema.json diff --git a/media/viewconfig-schema.json b/media/viewconfig-schema.json new file mode 100644 index 0000000..b5964f4 --- /dev/null +++ b/media/viewconfig-schema.json @@ -0,0 +1,484 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "patternProperties": { + "^.*$": { + "type": "object", + "properties": { + "title": { + "markdownDescription": "Attribute label to be displayed in the input form. Default: Attribute name as defined in YANG.", + "type": "string" + }, + "description": { + "markdownDescription": "Attribute description to be displayed as tooltip. Default: Attribute description as defined in YANG.", + "type": "string" + }, + "readOnly": { + "markdownDescription": "Attribute is read-only, blocked from getting modified by the user.", + "type": "boolean", + "default": false + }, + "visible": { + "markdownDescription": "Attribute is hidden from the input form.", + "type": "boolean", + "default": true + }, + "required": { + "markdownDescription": "Attribute is mandatory. Must be present to submit the form to create/update the intent.", + "type": "boolean", + "default": false + }, + "default": { + "markdownDescription": "Default value for attribute." + }, + "dependsOn": { + "markdownDescription": "Attribute depends on another attribute that must be provided first.", + "type": "string" + }, + "when": { + "markdownDescription": "XPATH expression used as condition to display/unlock the attribute on the form.", + "type": "string" + + }, + "columnSpan": { + "markdownDescription": "Defines the component width within the input form grid.", + "type": "integer", + "default": 2, + "minimum": 1 + }, + "newRow": { + "markdownDescription": "Shows the input component on the next line.", + "type": "boolean", + "default": false + }, + "type": { + "type": "string", + "enum": [ + "object", + "string", + "password", + "leafref", + "number", + "boolean", + "enum", + "choice", + "list", + "empty", + "bits", + "binary", + "union", + "propertyGroup", + "propertyList" + ] + } + }, + "additionalProperties": true, + "oneOf": [ + { + "properties": { + "type": { + "const": "string" + }, + "default": { + "type": "string" + }, + "wrap": { + "type": "string", + "enum": ["soft"] + }, + "validations": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "length": { + "type": "object", + "properties": { + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "componentProps": { + "type": "object", + "properties": { + "inputFieldProps": { + "type": "object", + "properties": { + "autoFocus": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "textAreaProps": { + "type": "object", + "properties": { + "style": { + "type": "object", + "properties": { + "height": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "component": { + "type": "object", + "properties": { + "input": { + "type": "string", + "enum": [ + "textArea" + ] + } + }, + "additionalProperties": false + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "password" + }, + "default": { + "type": "string" + }, + "componentProps": { + "type": "object", + "properties": { + "inputFieldProps": { + "type": "object", + "properties": { + "encode": { + "type": "boolean" + }, + "encodeCallback": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "leafref" + }, + "default": { + "type": "string" + }, + "suggest": { + "markdownDescription": "JavaScript method used as callout to populate the list of entries to pick from.", + "type": "string" + }, + "displayKey": { + "markdownDescription": "Object attribute displayed, once an entry is selected. In single mode, this attribute will be send when committing the form.", + "type": "string" + }, + "componentProps": { + "type": "object", + "properties": { + "isPagination": { + "type": "boolean" + }, + "crossLaunch": { + "type": "string" + }, + "launchText": { + "type": "string" + }, + "isObject": { + "type": "boolean" + }, + "paginationProps": { + "type": "object", + "properties": { + "pageLabel": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "enum" + }, + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": 0 + } + ] + }, + "sortable": { + "type": "boolean" + }, + "floatingFilter": { + "type": "boolean" + }, + "enum": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": 0 + } + ] + }, + "label": { + "type": "string" + } + }, + "additionalProperties": false + }, + "minItems": 1 + } + ] + }, + "component": { + "type": "object", + "properties": { + "input": { + "type": "string", + "enum": [ + "radiobutton" + ] + } + }, + "additionalProperties": false + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "list" + }, + "default": { + "type": "string" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "empty" + }, + "default": { + "type": "string" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "bits" + }, + "default": { + "type": "string" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "binary" + }, + "default": { + "type": "string" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "union" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "choice" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "propertyGroup" + }, + "default": { + "type": "string" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "number" + }, + "default": { + "type": "number" + }, + "component": { + "type": "object", + "properties": { + "input": { + "type": "string", + "enum": [ + "slider", + "numberInput" + ] + } + }, + "additionalProperties": false + }, + "validations": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ranges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "fractionDigits": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "boolean" + }, + "default": { + "type": "boolean" + }, + "component": { + "type": "string", + "enum": [ + "toggle" + ] + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "object" + }, + "default": { + "type": "object" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "const": "propertyList" + }, + "default": { + "type": "array" + } + }, + "required": ["type"] + }, + { + "properties": { + "type": { + "not": {} + } + } + } + ] + } + } +} diff --git a/src/providers/IntentManagerProvider.ts b/src/providers/IntentManagerProvider.ts index 56fe754..a6cf391 100644 --- a/src/providers/IntentManagerProvider.ts +++ b/src/providers/IntentManagerProvider.ts @@ -389,6 +389,8 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. this.osdVersion = json.version.number; vscode.window.showInformationMessage("Connected to "+this.nspAddr+", NSP version: "+this.nspVersion+", OSD version: "+this.osdVersion); + + this._addViewConfigSchema(); } /** @@ -531,6 +533,33 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. vscode.window.showWarningMessage(errmsg); } + /** + * Enable json-schema for validation of .viewConfig files + * + */ + + private _addViewConfigSchema() { + const jsonSchemas : {fileMatch: string[], schema: boolean, url:string}[] | undefined = vscode.workspace.getConfiguration('json').get('schemas'); + + if (jsonSchemas) { + const schemaPath = vscode.Uri.joinPath(this.extensionUri, 'media', 'viewconfig-schema.json').toString(); + + let entryExists = false; + for (const schema of jsonSchemas) { + if (schema.fileMatch.includes("*.viewConfig")) { + schema.url = schemaPath; + entryExists = true; + break; + } + } + + if (!entryExists) + jsonSchemas.push({"fileMatch": ["*.viewConfig"], "schema": false, "url": schemaPath}); + + vscode.workspace.getConfiguration('json').update('schemas', jsonSchemas, vscode.ConfigurationTarget.Global); + } + } + // --- SECTION: vscode.FileSystemProvider implementation ---------------- /** From 54f4d1dbdd28488ebcaa974ce71d1c825593ac12 Mon Sep 17 00:00:00 2001 From: Wisotzky Sven Date: Fri, 5 Jul 2024 13:43:39 +0200 Subject: [PATCH 3/3] 3.0.0 candidate --- CHANGELOG.md | 11 + package.json | 10 +- src/providers/IntentManagerProvider.ts | 71 +- .../common/IntentHandler.mjs | 695 ++++++++++-------- .../common/IntentLogic.mjs | 3 +- .../common/ResourceAdmin.mjs | 100 ++- .../intent-type-resources/Cisco IOS-XR.ftl | 6 +- .../intent-type-resources/JunOS.ftl | 8 +- .../intent-type-resources/OpenConfig.ftl | 14 +- .../intent-type-resources/SR OS.ftl | 14 +- .../intent-type-resources/SRLinux.ftl | 8 +- templates/ngJS iplink/script-content.mjs | 6 +- 12 files changed, 558 insertions(+), 388 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 933d324..05a6738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -187,3 +187,14 @@ Updates: * Show NSP version as {major}.{minor}. With this "24.4.0" is displayed now as "24.4". * New IPL!nk template for next-gen JavaScript engine (GraalJS) * Hide empty tabs in audit report + +## [3.0.0] + +Updates: +* IPL!nk for GraalJS to support assurance/state (experimental) +* IPL!nk for GraalJS to cache device-names (experimental) +* IPL!nk for GraalJS to provide better logging / error-messages +* NSP connection settings per workspace +* Validation for *.viewConfig files (partial coverage) +* NSP release and OSD version improvements (performance/robbustness) +* Retrieve list of intents including config (fix) diff --git a/package.json b/package.json index c3dc616..bfea70c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nokia-intent-manager", "displayName": "NOKIA_IM", "description": "NOKIA IM vsCode Developer Plugin", - "version": "2.5.0", + "version": "3.0.0", "icon": "media/NSP_Logo.png", "publisher": "Nokia", "repository": "http://github.com/nokia/vscode-intent-manager", @@ -229,14 +229,14 @@ "intentManager.NSPIP": { "type": "string", "default": "localhost", - "scope": "application", + "scope": "window", "format": "ipv4", "description": "Intent Manager hostname or IP address" }, "intentManager.port": { "type": "string", "default": "443", - "scope": "application", + "scope": "window", "enum": [ "443", "8545" @@ -250,13 +250,13 @@ "intentManager.user": { "type": "string", "default": "admin", - "scope": "application", + "scope": "window", "description": "Intent Manager username (default: admin)" }, "intentManager.password": { "type": "null", "markdownDescription": "[Set Password](command:nokia-intent-manager.setPassword)", - "scope": "application", + "scope": "window", "description": "Intent Manager password" }, "intentManager.timeout": { diff --git a/src/providers/IntentManagerProvider.ts b/src/providers/IntentManagerProvider.ts index a6cf391..97ff074 100644 --- a/src/providers/IntentManagerProvider.ts +++ b/src/providers/IntentManagerProvider.ts @@ -121,6 +121,8 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. this._eventEmiter = new vscode.EventEmitter(); this.onDidChangeFileDecorations = this._eventEmiter.event; + + this._addViewConfigSchema(); } /** @@ -188,6 +190,7 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. resolve(json.access_token); this.pluginLogs.info("new authToken:", json.access_token); setTimeout(() => this._revokeAuthToken(), 600000); // automatically revoke token after 10min + this._getNSPversion(); } else { this.pluginLogs.warn("NSP response:", response.status, json.error); @@ -365,32 +368,41 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. */ private async _getNSPversion(): Promise { - this.pluginLogs.info("Requesting NSP version"); - const url = "https://"+this.nspAddr+"/internal/shared-app-banner-utils/rest/api/v1/appBannerUtils/release-version"; - let response: any = await this._callNSP(url, {method: "GET"}); - if (!response) - throw vscode.FileSystemError.Unavailable("Lost connection to NSP"); - if (!response.ok) - this._raiseRestconfError("Getting NSP release failed!", await response.json()); - let json : any = await response.json(); - this.nspVersion = json.response.data.nspOSVersion.match(/\d+\.\d+(?=\.\d+)/)[0]; - - this._eventEmiter.fire(vscode.Uri.parse('im:/')); + let updated = false; - this.pluginLogs.info("Requesting OSD version"); - response = await this._callNSP("/logviewer/api/status", {method: "GET"}); - if (!response) - throw vscode.FileSystemError.Unavailable("Lost connection to NSP logviewer (opensearch)"); - if (!response.ok) { - const text: string = await response.text(); - throw vscode.FileSystemError.Unavailable(text); + if (!this.nspVersion) { + this.pluginLogs.info("Requesting NSP release"); + const url = "https://"+this.nspAddr+"/internal/shared-app-banner-utils/rest/api/v1/appBannerUtils/release-version"; + const response: any = await this._callNSP(url, {method: "GET"}); + if (!response) + this.pluginLogs.error("Lost connection to NSP"); + else if (response.ok) { + const json = await response.json(); + this.nspVersion = json.response.data.nspOSVersion.match(/\d+\.\d+(?=\.\d+)/)[0]; + updated = true; + } else + this.pluginLogs.error("Getting NSP release failed!"); } - json = await response.json(); - this.osdVersion = json.version.number; - vscode.window.showInformationMessage("Connected to "+this.nspAddr+", NSP version: "+this.nspVersion+", OSD version: "+this.osdVersion); + if (!this.osdVersion) { + this.pluginLogs.info("Requesting OSD version"); + const response: any = await this._callNSP("/logviewer/api/status", {method: "GET"}); + if (!response) + this.pluginLogs.error("Lost connection to NSP logviewer (opensearch)"); + else if (response.ok) { + const json = await response.json(); + this.osdVersion = json.version.number; + updated = true; + } else + this.pluginLogs.error("Getting OSD version failed!"); + } - this._addViewConfigSchema(); + if (updated) { + const msg = "Connected to "+this.nspAddr+", NSP version: "+(this.nspVersion??"unknown")+", OSD version: "+(this.osdVersion??"unknown"); + this.pluginLogs.info(msg); + vscode.window.showInformationMessage(msg); + this._eventEmiter.fire(vscode.Uri.parse('im:/')); + } } /** @@ -540,10 +552,9 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. private _addViewConfigSchema() { const jsonSchemas : {fileMatch: string[], schema: boolean, url:string}[] | undefined = vscode.workspace.getConfiguration('json').get('schemas'); + const schemaPath : string = vscode.Uri.joinPath(this.extensionUri, 'media', 'viewconfig-schema.json').toString(); - if (jsonSchemas) { - const schemaPath = vscode.Uri.joinPath(this.extensionUri, 'media', 'viewconfig-schema.json').toString(); - + if (jsonSchemas !== undefined) { let entryExists = false; for (const schema of jsonSchemas) { if (schema.fileMatch.includes("*.viewConfig")) { @@ -556,7 +567,7 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. if (!entryExists) jsonSchemas.push({"fileMatch": ["*.viewConfig"], "schema": false, "url": schemaPath}); - vscode.workspace.getConfiguration('json').update('schemas', jsonSchemas, vscode.ConfigurationTarget.Global); + vscode.workspace.getConfiguration('json').update('schemas', jsonSchemas, vscode.ConfigurationTarget.Workspace); } } @@ -578,9 +589,6 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. let result:[string, vscode.FileType][] = []; - if (!this.nspVersion) - this._getNSPversion(); - if (path === "im:/") { // readDirectory() was executed with IM root folder // @@ -674,7 +682,7 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. const body = { "ibn:input": { "filter": { - "config-required": false, + "config-required": true, "intent-type-list": [ { "intent-type": intent_type, @@ -1929,6 +1937,9 @@ export class IntentManagerProvider implements vscode.FileSystemProvider, vscode. if (!token) throw vscode.FileSystemError.Unavailable('NSP is not reachable'); + if (!this.osdVersion) + await this._getNSPversion(); + const url = "/logviewer/api/console/proxy?path=nsp-mdt-logs-*/_search&method=GET"; const body = {"query": query, "sort": {"@datetime": "desc"}, "size": this.logLimit}; diff --git a/templates/common_resources_graaljs/common/IntentHandler.mjs b/templates/common_resources_graaljs/common/IntentHandler.mjs index e65189d..3642005 100644 --- a/templates/common_resources_graaljs/common/IntentHandler.mjs +++ b/templates/common_resources_graaljs/common/IntentHandler.mjs @@ -16,13 +16,14 @@ let SynchronizeResult = classResolver.resolveClass("com.nokia.fnms.controller.ib let AuditReport = classResolver.resolveClass("com.nokia.fnms.controller.ibn.intenttype.spi.AuditReport"); let MisAlignedObject = classResolver.resolveClass("com.nokia.fnms.controller.ibn.intenttype.spi.MisAlignedObject"); let MisAlignedAttribute = classResolver.resolveClass("com.nokia.fnms.controller.ibn.intenttype.spi.MisAlignedAttribute"); -let RuntimeException = classResolver.resolveClass("java.lang.RuntimeException"); let ArrayList = classResolver.resolveClass("java.util.ArrayList"); export class IntentHandler extends CalloutHandler { #logic; + #deviceCache; + #dcLastUpdated; /** * Create IntentHandler @@ -35,6 +36,8 @@ export class IntentHandler extends CalloutHandler { super(); this.#logic = intentLogic; + this.#deviceCache = {}; + this.#dcLastUpdated = -1; } /** @@ -67,15 +70,19 @@ export class IntentHandler extends CalloutHandler logger.error("Exception {} occured.", exception); result = { success: false, errmsg: "Couldn't connect to mediator. Exception "+exception+" occured." }; } - else if (httpStatus === 404) { - // Treat resource not found as normal case - // This is to avoid confusing developers when reading the logs - logger.info("NSP response: {} {}", httpStatus, "Resource not found"); - result = { success: false, response: JSON.parse(response), errmsg: "Not Found" }; - } else if (httpStatus >= 400) { // Either client error (4xx) or server error (5xx) - logger.warn("NSP response: {} {}", httpStatus, response); + + // Note: + // 404 (resource not found) is considered a normal case, while it might happen during + // audits when the resource was not yet created. To avoid confusing developers getting + // in panic when reading the logs, status-code 404 using log-level info instead of warn + + if (httpStatus === 404) { + logger.info("NSP response: {} {}", httpStatus, response); + } + else + logger.warn("NSP response: {} {}", httpStatus, response); // Error details returned in accordance to RFC8020 ch7.1 // {"ietf-restconf:errors":{"error":[{ ___error details__ }]}} @@ -111,11 +118,7 @@ export class IntentHandler extends CalloutHandler break; } - const errorObject = JSON.parse(response); - if ('ietf-restconf:errors' in errorObject) - errorObject['ietf-restconf:errors'].error.forEach( errorDetails => errmsg += "\n\t"+errorDetails['error-message'] ); - - result = { success: false, response: JSON.parse(response), errmsg: errmsg }; + result = { success: false, errmsg: errmsg }; } else { // Should be 200 OK @@ -159,7 +162,7 @@ export class IntentHandler extends CalloutHandler restClient.setPort(managerInfo.getPort()); restClient.setProtocol(managerInfo.getProtocol().toString()); - restClient.patch(url, "application/yang-patch+json", body, "application/json", (exception, httpStatus, response) => { + restClient.patch(url, "application/yang-patch+json", body, "application/yang-data+json", (exception, httpStatus, response) => { const duration = Date.now()-startTS; logger.info("PATCH {} {} finished within {} ms", url, body, duration|0); @@ -171,44 +174,56 @@ export class IntentHandler extends CalloutHandler // Either client error (4xx) or server error (5xx) logger.warn("NSP response: {} {}", httpStatus, response); - // Error details returned in accordance to RFC8072 ch2.3 (2 options: global errors OR edit errors ) - // {"ietf-yang-patch:yang-patch-status":{"ietf-restconf:errors":{"error":[{ error details }]}}} - // {"ietf-yang-patch:yang-patch-status":{"edit-status":{"edit":[{"ietf-restconf:errors":{"error":[{ error details }]}}]}}} - // - // Error fields: - // error-type enumeration - // error-tag string - // error-app-tag? string - // error-path? instance-identifier - // error-message? string - // error-info? anydata - - // extract error details from response: - const errorObject = JSON.parse(response); - if ('ietf-yang-patch:yang-patch-status' in errorObject) { - const yangPatchStatus = errorObject['ietf-yang-patch:yang-patch-status']; - - if ('ietf-restconf:errors' in yangPatchStatus) { - // global errors - let errmsg = "[site:"+neId+"] rfc8072 global-errors:"; - yangPatchStatus['ietf-restconf:errors'].error.forEach( errorDetails => errmsg += "\n\t"+errorDetails['error-message'] ); - result = { success: false, response: errorObject, errmsg: errmsg }; - } - else if ('edit-status' in yangPatchStatus) { - // edit errors - let errmsg = "[site:"+neId+"] rfc8072 edit-errors:"; - yangPatchStatus['edit-status'].edit.forEach( edit => { - errmsg += "\n\t"+edit['edit-id']+":"; - edit['ietf-restconf:errors'].error.forEach( errorDetails => { - errmsg += "\n\t\t" + errorDetails['error-tag'] + " path:" + errorDetails['error-path'] + " details:" + errorDetails['error-message']; + let errmsg=""; + if ((/]*>(.*)<\/body>/i); + if (errMatch) + errmsg = errMatch[1]; // extracted errmsg from html body + else + errmsg = "HTTP ERROR "+httpStatus; + } else { + // Extract error details from JSON response: + // + // Error details returned in accordance to RFC8072 ch2.3 (2 options: global errors OR edit errors ) + // {"ietf-yang-patch:yang-patch-status":{"ietf-restconf:errors":{"error":[{ error details }]}}} + // {"ietf-yang-patch:yang-patch-status":{"edit-status":{"edit":[{"ietf-restconf:errors":{"error":[{ error details }]}}]}}} + // + // Error fields: + // error-type enumeration + // error-tag string + // error-app-tag? string + // error-path? instance-identifier + // error-message? string + // error-info? anydata + + const errorObject = JSON.parse(response); + if ('ietf-yang-patch:yang-patch-status' in errorObject) { + const yangPatchStatus = errorObject['ietf-yang-patch:yang-patch-status']; + + // Check for RFC8072 YANG-PATCH ERRORS: global-errors + if ('ietf-restconf:errors' in yangPatchStatus) { + const errList = yangPatchStatus['ietf-restconf:errors'].error; + errmsg = errList.map(error => error["error-message"]).join(', '); + } + + // Check for RFC8072 YANG-PATCH ERRORS: edit-errors + if ('edit-status' in yangPatchStatus) { + yangPatchStatus['edit-status'].edit.forEach( edit => { + if (edit['edit-id']) + errmsg += "[object: "+edit['edit-id']+"] "; + const errList = edit['ietf-restconf:errors'].error; + errmsg += errList.map(error => { + if (error['error-path']) + return "[path: " + error['error-path'] + "] " + error['error-message']; + else + return error['error-message']; + }).join(', ')+" "; }); - }); - result = { success: false, response: errorObject, errmsg: errmsg }; + } } - else - result = { success: false, response: errorObject, errmsg: "HTTP ERROR "+httpStatus}; - } else - result = { success: false, response: errorObject, errmsg: "HTTP ERROR "+httpStatus}; + } + result = { success: false, errmsg: errmsg.trim() }; } else { // 2xx - Success logger.info("NSP response: {} {}", httpStatus, response); @@ -276,14 +291,74 @@ export class IntentHandler extends CalloutHandler return result; } -/** - * Get list-keys from YANG model for MDC-managed nodes - * - * @param {} neId device - * @param {} listPath path of list to get the keys - * @returns listKeys[] - * - **/ + #updateDeviceCache() { + const startTS = Date.now(); + + if (startTS - this.#dcLastUpdated < 300000) + // Keep cache! Content was updated within the last 5min + return; + + this.#dcLastUpdated = startTS; + logger.debug("IntentHandler::#updateDeviceCache()"); + + var managerInfo = mds.getManagerByName('NSP'); + if (managerInfo.getConnectivityState().toString() === 'CONNECTED') { + restClient.setIp(managerInfo.getIp()); + restClient.setPort(managerInfo.getPort()); + restClient.setProtocol(managerInfo.getProtocol().toString()); + + var url = "https://restconf-gateway/restconf/operations/nsp-inventory:find"; + var options = { + "xpath-filter": "/nsp-equipment:network/network-element", + "depth": 3, + "fields": "ne-id;ne-name", + "offset": 0 + }; + + let total=1; + let offset=0; + let newCache = {}; + + while (offset { + if (exception) { + logger.error("Exception {} occured.", exception); + this.#dcLastUpdated = -1; + total = offset; + } + else if (httpStatus >= 400) { + // Either client error (4xx) or server error (5xx) + logger.warn("NSP response: {} {}", httpStatus, response); + this.#dcLastUpdated = -1; + total = offset; + } + else { + const output = JSON.parse(response)["nsp-inventory:output"]; + total = output["total-count"]; + offset = output["end-index"]+1; + output.data.forEach(entry => newCache[entry['ne-id']] = entry['ne-name']); + } + }); + } + + this.#deviceCache = newCache; + this.#dcLastUpdated = Date.now(); + logger.info("device cache updated: {} entries", total); + } + + const duration = Date.now()-startTS; + logger.debug("CalloutHandler::#updateDeviceCache() finished within {} ms", duration|0); + } + + /** + * Get list-keys from YANG model for MDC-managed nodes + * + * @param {} neId device + * @param {} listPath path of list to get the keys + * @returns listKeys[] + * + **/ #getListKeys(neId, listPath) { const startTS = Date.now(); @@ -320,7 +395,7 @@ export class IntentHandler extends CalloutHandler } } } else - logger.error("#getListKeys() failed with error:\n{}", result["errmsg"]); + logger.error("#getListKeys() failed with {}", result["errmsg"]); const duration = Date.now()-startTS; logger.debug("IntentHandler::#getListKeys({}, {}) finished within {} ms", neId, listPath, duration|0); @@ -328,21 +403,21 @@ export class IntentHandler extends CalloutHandler return listKeys; } -/** - * JSONPath 0.8.4 - XPath for JSON - * available from https://code.google.com/archive/p/jsonpath/ - * - * Copyright (c) 2007 Stefan Goessner (goessner.net) - * Licensed under the MIT (MIT-LICENSE.txt) licence. - * - * @param {} obj - * @param {} expr - * @param {} arg - * @returns result - * - * @throws SyntaxError - * - **/ + /** + * JSONPath 0.8.4 - XPath for JSON + * available from https://code.google.com/archive/p/jsonpath/ + * + * Copyright (c) 2007 Stefan Goessner (goessner.net) + * Licensed under the MIT (MIT-LICENSE.txt) licence. + * + * @param {} obj + * @param {} expr + * @param {} arg + * @returns result + * + * @throws SyntaxError + * + **/ #jsonPath(obj, expr, arg) { var P = { @@ -436,7 +511,7 @@ export class IntentHandler extends CalloutHandler * @param {string} rootXPath Root XPATH of configuration * @param {Object} config Desired configuration * - * @throws {RuntimeException} /resolve-synchronize failed + * @throws {Error} /resolve-synchronize failed * **/ @@ -453,7 +528,7 @@ export class IntentHandler extends CalloutHandler }; const resolveResponse = this.#fwkAction("/resolve-synchronize", unresolvedConfig); if (!resolveResponse.success) - throw new RuntimeException("Resolve Synchronize failed with {}", resolveResponse.errmsg); + throw new Error("Resolve Synchronize failed with {}", resolveResponse.errmsg); // enable for detailed debugging: // logger.info("desired config: {}", JSON.stringify(config)); @@ -472,18 +547,18 @@ export class IntentHandler extends CalloutHandler * @param {string} target Intent target * @param {AuditReport} auditReport Audit report before applying approvals * - * @throws {RuntimeException} /resolve-audit failed + * @throws {Error} /resolve-audit failed * **/ - #resolveAudit(target, auditReport) { + #resolveAudit(auditReport) { const startTS = Date.now(); logger.debug("IntentHandler::#resolveAudit()"); // Convert audit report to JSON const auditReportJson = { - "target": target, - "intent-type": this.#logic.INTENT_TYPE + "target": auditReport.getTarget(), + "intent-type": auditReport.getIntentType() }; if (auditReport.getMisAlignedAttributes()) { @@ -515,7 +590,7 @@ export class IntentHandler extends CalloutHandler // Call the mediator to resolve audit report let resolveResponse = this.#fwkAction("/resolve-audit", auditReportJson); if (!resolveResponse.success) - throw new RuntimeException("/resolve-audit failed with " + resolveResponse.errmsg); + throw new Error("/resolve-audit failed with {}", resolveResponse.errmsg); const resolvedAuditReportJson = resolveResponse.response; logger.info("resolved audit report:\n{}", JSON.stringify(resolvedAuditReportJson, null, " ")); @@ -611,9 +686,6 @@ export class IntentHandler extends CalloutHandler else // missing object: is-configured=true, is-undesired=default(false) auditReport.addMisAlignedObject(new MisAlignedObject('/'+basePath+'/'+path+key, true, neId)); - - // Alternative option: Report as misaligned attribute (JSON string) - // auditReport.addMisAlignedAttribute(new MisAlignedAttribute(path+key, JSON.stringify(iCfg[key]), null, obj)); } else { // mismatch: leaf is unconfigured auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+basePath+'/'+path+key, iCfg[key].toString(), null, obj)); @@ -645,13 +717,10 @@ export class IntentHandler extends CalloutHandler const aVal = JSON.stringify(aCfg[key]); if ((aVal === '{}') || (aVal === '[]') || (aVal === '[null]')) - auditReport.addMisAlignedAttribute(new MisAlignedAttribute(aKey, null, aVal, obj)); + auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+basePath+'/'+aKey, null, aVal, obj)); else // undesired object: is-configured=true, is-undesired=default(true) - auditReport.addMisAlignedObject(new MisAlignedObject('/'+basePath+'/'+aKey, true, neId, true)); - - // Alternative option: Report as misaligned attribute (JSON string) - // auditReport.addMisAlignedAttribute(new MisAlignedAttribute(aKey, null, JSON.stringify(aCfg[key]), obj)); + auditReport.addMisAlignedObject(new MisAlignedObject('/'+basePath+'/'+aKey, true, neId, true)); } else { // mismatch: additional leaf auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+basePath+'/'+aKey, null, aCfg[key].toString(), obj)); @@ -674,7 +743,7 @@ export class IntentHandler extends CalloutHandler * @param {} auditReport used to report differences * @param {} obj object reference used for report * - * @throws RuntimeException + * @throws {Error} * **/ @@ -716,20 +785,20 @@ export class IntentHandler extends CalloutHandler match = RegExp(iValue).test(aValue[0]); break; default: - throw new RuntimeException("Unsupported check '"+check+"' for object("+path+"), jsonpath("+path+"), expected value("+iValue+")"); + throw new Error("Unsupported check '"+check+"' for object("+path+"), jsonpath("+path+"), expected value("+iValue+")"); } if (!match) - auditReport.addMisAlignedAttribute(new MisAlignedAttribute(qPath+'/'+key, iValue.toString(), aValue[0].toString(), siteName)); + auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+qPath+'/'+key, iValue.toString(), aValue[0].toString(), siteName)); } else { - auditReport.addMisAlignedAttribute(new MisAlignedAttribute(qPath+'/'+key, iValue.toString(), null, siteName)); + auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+qPath+'/'+key, iValue.toString(), null, siteName)); } } } } else if (key in aState) { if (iState[key] !== aState[key]) - auditReport.addMisAlignedAttribute(new MisAlignedAttribute(qPath+'/'+key, iState[key].toString(), aState[key].toString(), siteName)); + auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+qPath+'/'+key, iState[key].toString(), aState[key].toString(), siteName)); } else { - auditReport.addMisAlignedAttribute(new MisAlignedAttribute(qPath+'/'+key, iState[key].toString(), null, siteName)); + auditReport.addMisAlignedAttribute(new MisAlignedAttribute('/'+qPath+'/'+key, iState[key].toString(), null, siteName)); } } @@ -748,7 +817,10 @@ export class IntentHandler extends CalloutHandler * mediator and if the corresponding freemarker template (ftl) could be loaded. * * @param {} input input provided by intent-engine - * @returns {ValidateResult} * + * @returns {ValidateResult} + * + * @throws ContextErrorException + * **/ validate(input) { @@ -822,127 +894,142 @@ export class IntentHandler extends CalloutHandler logger.info("IntentHandler::synchronize() in state {} ", state); const config = JSON.parse(input.getJsonIntentConfiguration())[0][this.#logic.INTENT_ROOT]; - + var topology = input.getCurrentTopology(); var syncResult = new SynchronizeResult(); - + var syncErrors = []; + var sitesConfigs = {}; var sitesCleanups = {}; - var deploymentErrors = []; - const yangPatchTemplate = resourceProvider.getResource("common/patch.ftl"); + try { + // Update device-cache (as needed) + this.#updateDeviceCache(); + + const yangPatchTemplate = resourceProvider.getResource("common/patch.ftl"); + + // Recall nodal configuration elements from previous synchronize (for cleanup/housekeeping) + if (topology && topology.getXtraInfo()!==null && !topology.getXtraInfo().isEmpty()) { + topology.getXtraInfo().forEach(item => { + if (item.getKey() === 'sitesCleanups') { + sitesCleanups = JSON.parse(item.getValue()); + sitesConfigs = JSON.parse(item.getValue()); // deep-clone of sitesCleanups + logger.info("sitesCleanups restored: "+item.getValue()); + } + }); + } - // Recall nodal configuration elements from previous synchronize (for cleanup/housekeeping) - if (topology && topology.getXtraInfo()!==null && !topology.getXtraInfo().isEmpty()) { - topology.getXtraInfo().forEach(item => { - if (item.getKey() === 'sitesCleanups') { - sitesCleanups = JSON.parse(item.getValue()); - sitesConfigs = JSON.parse(item.getValue()); // deep-clone of sitesCleanups - logger.info("sitesCleanups restored: "+item.getValue()); - } - }); - } + // Secure resources from Resource Admin + // Right now, we are assuming that reservation is required in any state but delete - // Secure resources from Resource Admin - // Right now, we are assuming that reservation is required in any state but delete + if (state !== 'delete') + this.#logic.obtainResources(target, config); - if (state !== 'delete') - this.#logic.obtainResources(target, config); + if (state === "active") { + const global = this.#logic.getGlobalParameters(target, config); - if (state === "active") { - const global = this.#logic.getGlobalParameters(target, config); + // Iterate sites to populate/update sitesConfigs per target device + this.#logic.getSiteParameters(target, config, this.#deviceCache).forEach(site => { + const neId = site['ne-id']; + const neInfo = mds.getAllInfoFromDevices(neId); + const neFamilyTypeRelease = neInfo.get(0).getFamilyTypeRelease(); + const neType = neFamilyTypeRelease.split(':')[0]; + const neVersion = neFamilyTypeRelease.split(':')[1]; - // Iterate sites to populate/update sitesConfigs per target device - this.#logic.getSiteParameters(target, config).forEach(site => { - const neId = site['ne-id']; - const neInfo = mds.getAllInfoFromDevices(neId); - const neFamilyTypeRelease = neInfo.get(0).getFamilyTypeRelease(); - const neType = neFamilyTypeRelease.split(':')[0]; - const neVersion = neFamilyTypeRelease.split(':')[1]; + // ensure we've got the user-friendly deviceName + site['ne-name'] = this.#deviceCache[neId]; - if (!(neId in sitesConfigs)) - sitesConfigs[neId] = {}; + if (!(neId in sitesConfigs)) + sitesConfigs[neId] = {}; - const siteFTL = resourceProvider.getResource(this.#logic.getTemplateName(neId, neType)); - const objects = JSON.parse(utilityService.processTemplate(siteFTL, {'target': target, 'site': site, 'global': global, 'neVersion': neVersion, 'mode': 'sync'})); - - for (const objectName in objects) { - if ("config" in objects[objectName]) { - sitesConfigs[neId][objectName] = objects[objectName].config; - - // Convert 'value' object to JSON string as required as input for PATCH.ftl - if (objects[objectName].config.value) { - let value = this.#resolveSynchronize(target, neId, '/'+objects[objectName].config.target, objects[objectName].config.value); - sitesConfigs[neId][objectName].value = JSON.stringify(value); + const siteFTL = resourceProvider.getResource(this.#logic.getTemplateName(neId, neType)); + const objects = JSON.parse(utilityService.processTemplate(siteFTL, {'target': target, 'site': site, 'global': global, 'neVersion': neVersion, 'mode': 'sync'})); + + for (const objectName in objects) { + if ("config" in objects[objectName]) { + sitesConfigs[neId][objectName] = objects[objectName].config; + + // Convert 'value' object to JSON string as required as input for PATCH.ftl + if (objects[objectName].config.value) { + let value = this.#resolveSynchronize(target, neId, '/'+objects[objectName].config.target, objects[objectName].config.value); + sitesConfigs[neId][objectName].value = JSON.stringify(value); + } } } - } - }); - } - - // Deploy changes to target devices and update topology objects and xtra-data - if (state === "active" || state === "suspend" || state === "delete") { - let topologyObjects = []; - for (const neId in sitesConfigs) { - const body = utilityService.processTemplate(yangPatchTemplate, {'patchId': target, 'patchItems': sitesConfigs[neId]}); - - let result = this.#restconfPatchDevice(neId, body); - - if (result.success) { - // RESTCONF YANG PATCH was successful - // - objects that have been added/updated are added to the new topology - // - objects that have been added/updated are added to siteCleanups (extraData) to enable housekeeping - - sitesCleanups[neId] = {}; - for (const objectName in sitesConfigs[neId]) { - if (sitesConfigs[neId][objectName]["operation"]==="replace") { - // For operation "replace" remember how to clean-up the object created (house-keeping). - // For cleanup we are using operation "remove", to avoid the operation from failing, - // if the corresponding device configuration was deleted from the network already. - - sitesCleanups[neId][objectName] = {'target': sitesConfigs[neId][objectName].target, 'operation': 'remove'}; - topologyObjects.push(topologyFactory.createTopologyObjectFrom(objectName, sitesConfigs[neId][objectName].target, "INFRASTRUCTURE", neId)); - } + }); + } - // NOTE: - // Operations "merge", and "remove" will not be reverted back! - // Operations "create", and "delete" should not be used (not reverted back either)! - } - - if (Object.keys(sitesCleanups[neId]).length === 0) - delete sitesCleanups[neId]; + // Deploy changes to target devices and update topology objects and xtra-data + if (state === "active" || state === "suspend" || state === "delete") { + let topologyObjects = []; + for (const neId in sitesConfigs) { + const body = utilityService.processTemplate(yangPatchTemplate, {'patchId': target, 'patchItems': sitesConfigs[neId]}); - } else { - logger.error("Deployment on {} failed with error:\n{}", neId, result.errmsg); - deploymentErrors.push(result.errmsg); + let result = this.#restconfPatchDevice(neId, body); - // RESTCONF YANG PATCH failed - // - Keep siteCleanups (extraData) for this site to enable housekeeping - // - Generate topology from siteCleanup (same content as it was before) - - if (neId in sitesCleanups) { - for (const objectName in sitesCleanups[neId]) { - topologyObjects.push(topologyFactory.createTopologyObjectFrom(objectName, sitesCleanups[neId][objectName].target, "INFRASTRUCTURE", neId)); + if (result.success) { + // RESTCONF YANG PATCH was successful + // - objects that have been added/updated are added to the new topology + // - objects that have been added/updated are added to siteCleanups (extraData) to enable housekeeping + + sitesCleanups[neId] = {}; + for (const objectName in sitesConfigs[neId]) { + if (sitesConfigs[neId][objectName]["operation"]==="replace") { + // For operation "replace" remember how to clean-up the object created (house-keeping). + // For cleanup we are using operation "remove", to avoid the operation from failing, + // if the corresponding device configuration was deleted from the network already. + + sitesCleanups[neId][objectName] = {'target': sitesConfigs[neId][objectName].target, 'operation': 'remove'}; + topologyObjects.push(topologyFactory.createTopologyObjectFrom(objectName, sitesConfigs[neId][objectName].target, "INFRASTRUCTURE", neId)); + } + + // NOTE: + // Operations "merge", and "remove" will not be reverted back! + // Operations "create", and "delete" should not be used (not reverted back either)! + } + + if (Object.keys(sitesCleanups[neId]).length === 0) + delete sitesCleanups[neId]; + + } else { + if (neId in this.#deviceCache) { + logger.error("Deployment on {} ({}) failed with {}", this.#deviceCache[neId], neId, result.errmsg); + syncErrors.push("[site: "+this.#deviceCache[neId]+", "+neId+"] "+result.errmsg); + } else { + logger.error("Deployment on {} failed with {}", neId, result.errmsg); + syncErrors.push("[site: "+neId+"] "+result.errmsg); + } + + // RESTCONF YANG PATCH failed + // - Keep siteCleanups (extraData) for this site to enable housekeeping + // - Generate topology from siteCleanup (same content as it was before) + + if (neId in sitesCleanups) { + for (const objectName in sitesCleanups[neId]) { + topologyObjects.push(topologyFactory.createTopologyObjectFrom(objectName, sitesCleanups[neId][objectName].target, "INFRASTRUCTURE", neId)); + } } } - } - - if (topology === null) - topology = topologyFactory.createServiceTopology(); + + if (topology === null) + topology = topologyFactory.createServiceTopology(); - let xtrainfo = topologyFactory.createTopologyXtraInfoFrom("sitesCleanups", JSON.stringify(sitesCleanups)); + let xtrainfo = topologyFactory.createTopologyXtraInfoFrom("sitesCleanups", JSON.stringify(sitesCleanups)); - topology.setXtraInfo([xtrainfo]); - topology.setTopologyObjects(topologyObjects); + topology.setXtraInfo([xtrainfo]); + topology.setTopologyObjects(topologyObjects); + } } - } - syncResult.setTopology(topology); - - if (deploymentErrors.length > 0) { + syncResult.setTopology(topology); + } catch (err) { + syncErrors.push(err.message); + } + + if (syncErrors.length > 0) { syncResult.setSuccess(false); syncResult.setErrorCode("500"); - syncResult.setErrorDetail(deploymentErrors.toString()); + syncResult.setErrorDetail(syncErrors.join('; ')); } else { syncResult.setSuccess(true); if (state === 'delete') @@ -961,8 +1048,7 @@ export class IntentHandler extends CalloutHandler * Compares actual against desired configuration to produce the AuditReport. * * @param {} input input provided by intent-engine - * - * @throws {RuntimeException} config/state retrieval failed + * @returns {AuditReport} audit report * **/ @@ -977,127 +1063,141 @@ export class IntentHandler extends CalloutHandler var topology = input.getCurrentTopology(); var auditReport = new AuditReport(); + auditReport.setIntentType(this.#logic.INTENT_TYPE); + auditReport.setTarget(target); + + try { + // Update device-cache (as needed) + this.#updateDeviceCache(); + + // Recall nodal configuration elements from previous synchronize + var obsoleted = {}; + if (topology && topology.getXtraInfo()!==null && !topology.getXtraInfo().isEmpty()) { + topology.getXtraInfo().forEach(item => { + if (item.getKey() === 'sitesCleanups') { + obsoleted = JSON.parse(item.getValue()); + } + }); + } - // Recall nodal configuration elements from previous synchronize - var obsoleted = {}; - if (topology && topology.getXtraInfo()!==null && !topology.getXtraInfo().isEmpty()) { - topology.getXtraInfo().forEach(item => { - if (item.getKey() === 'sitesCleanups') { - obsoleted = JSON.parse(item.getValue()); - } - }); - } - - if (state === 'active') { - // Obtain resources from Resource Admin - // Remind, this is done even if the intent was not synchronized before! - // Required for getSiteParameters() and getGlobalParameters() - this.#logic.obtainResources(target, config); - const global = this.#logic.getGlobalParameters(target, config); - - // Iterate sites to populate/update sitesConfigs per target device - this.#logic.getSiteParameters(target, config).forEach(site => { - const neId = site['ne-id']; - const neInfo = mds.getAllInfoFromDevices(neId); - const neFamilyTypeRelease = neInfo.get(0).getFamilyTypeRelease(); - const neType = neFamilyTypeRelease.split(':')[0]; - const neVersion = neFamilyTypeRelease.split(':')[1]; + if (state === 'active') { + // Obtain resources from Resource Admin + // Remind, this is done even if the intent was not synchronized before! + // Required for getSiteParameters() and getGlobalParameters() + this.#logic.obtainResources(target, config); + const global = this.#logic.getGlobalParameters(target, config); + + // Iterate sites to populate/update sitesConfigs per target device + this.#logic.getSiteParameters(target, config, this.#deviceCache).forEach(site => { + const neId = site['ne-id']; + const neInfo = mds.getAllInfoFromDevices(neId); + const neFamilyTypeRelease = neInfo.get(0).getFamilyTypeRelease(); + const neType = neFamilyTypeRelease.split(':')[0]; + const neVersion = neFamilyTypeRelease.split(':')[1]; - const siteFTL = resourceProvider.getResource(this.#logic.getTemplateName(neId, neType)); - const objects = JSON.parse(utilityService.processTemplate(siteFTL, {'target': target, 'site': site, 'global': global, 'neVersion': neVersion, 'mode': 'audit'})); + // ensure we've got the user-friendly deviceName + site['ne-name'] = this.#deviceCache[neId]; - // Audit device configuration - for (const objectName in objects) { - if ("config" in objects[objectName]) { - const result = this.#restconfGetDevice(neId, objects[objectName].config.target+"?content=config"); - if (result.success) { - let iCfg = objects[objectName].config.value; - for (const key in iCfg) { - iCfg = iCfg[key]; - break; - } - - let aCfg = result.response; - for (const key in aCfg) { - aCfg = aCfg[key]; - break; - } + const siteFTL = resourceProvider.getResource(this.#logic.getTemplateName(neId, neType)); + const objects = JSON.parse(utilityService.processTemplate(siteFTL, {'target': target, 'site': site, 'global': global, 'neVersion': neVersion, 'mode': 'audit'})); - if (Array.isArray(aCfg)) - if (aCfg.length === 0) { - // an empty was returned - // missing object: is-configured=true, is-undesired=default(false) - auditReport.addMisAlignedObject(new MisAlignedObject('/'+objects[objectName].config.target, true, neId)); - aCfg = null; - } else { - // Due to the nature of RESTCONF GET, we've received a single entry list - // Execute the audit against this single entry - aCfg = aCfg[0]; + // Audit device configuration + for (const objectName in objects) { + if ("config" in objects[objectName]) { + const result = this.#restconfGetDevice(neId, objects[objectName].config.target+"?content=config"); + if (result.success) { + let iCfg = objects[objectName].config.value; + for (const key in iCfg) { + iCfg = iCfg[key]; + break; + } + + let aCfg = result.response; + for (const key in aCfg) { + aCfg = aCfg[key]; + break; } - if (aCfg) { - this.#compareConfig(neId, objects[objectName].config.target, aCfg, iCfg, objects[objectName].config.operation, objects[objectName].config.ignoreChildren, auditReport, neId, ''); - // this.#compareConfig(neId, objects[objectName].config.target, aCfg, iCfg, objects[objectName].config.operation, objects[objectName].config.ignoreChildren, auditReport, objectName, ''); + if (Array.isArray(aCfg)) + if (aCfg.length === 0) { + // an empty was returned + // missing object: is-configured=true, is-undesired=default(false) + auditReport.addMisAlignedObject(new MisAlignedObject('/'+objects[objectName].config.target, true, neId)); + aCfg = null; + } else { + // Due to the nature of RESTCONF GET, we've received a single entry list + // Execute the audit against this single entry + aCfg = aCfg[0]; + } + + if (aCfg) + this.#compareConfig(neId, objects[objectName].config.target, aCfg, iCfg, objects[objectName].config.operation, objects[objectName].config.ignoreChildren, auditReport, neId, ''); + } + else if (result.errmsg === "Not Found") { + // get failed, because path is not configured + // missing object: is-configured=true, is-undesired=default(false) + auditReport.addMisAlignedObject(new MisAlignedObject('/'+objects[objectName].config.target, true, neId)); + } else { + logger.error("RESTCONF GET failed with {}" + result.errmsg); + throw new Error("RESTCONF GET failed with " + result.errmsg); } - } - else if (result.errmsg === "Not Found") { - // get failed, because path is not configured - // missing object: is-configured=true, is-undesired=default(false) - auditReport.addMisAlignedObject(new MisAlignedObject('/'+objects[objectName].config.target, true, neId)); - } else { - logger.error("RESTCONF GET failed with error:\n" + result.errmsg); - throw new RuntimeException("RESTCONF GET failed with " + result.errmsg); - } - // Configuration object is still present, remove from obsoleted - if (neId in obsoleted) - if (objectName in obsoleted[neId]) - delete obsoleted[neId][objectName]; + // Configuration object is still present, remove from obsoleted + if (neId in obsoleted) + if (objectName in obsoleted[neId]) + delete obsoleted[neId][objectName]; + } } - } - for (const objectName in objects) { - if ("health" in objects[objectName]) { - for (const path in objects[objectName].health) { - const iState = objects[objectName].health[path]; - const result = this.#restconfGetDevice(neId, path); - if (result.success) { - let aState = result.response; - for (const key in aState) { - aState = aState[key]; - break; + for (const objectName in objects) { + if ("health" in objects[objectName]) { + for (const path in objects[objectName].health) { + const iState = objects[objectName].health[path]; + const result = this.#restconfGetDevice(neId, path); + if (result.success) { + let aState = result.response; + for (const key in aState) { + aState = aState[key]; + break; + } + if (Array.isArray(aState)) + aState = aState[0]; + + this.#compareState(neId, aState, iState, auditReport, path); + } + else if (result.errmsg === "Not Found") { + // get failed, because path is not available + // missing state object: is-configured=false, is-undesired=default(false) + auditReport.addMisAlignedObject(new MisAlignedObject('/'+path, false, neId)); + } else { + logger.error("RESTCONF GET failed with {}", result.errmsg); + throw new Error("RESTCONF GET failed with " + result.errmsg); } - if (Array.isArray(aState)) - aState = aState[0]; - - this.#compareState(neId, aState, iState, auditReport, '/'+path); - } - else if (result.errmsg === "Not Found") { - // get failed, because path is not available - // missing state object: is-configured=false, is-undesired=default(false) - auditReport.addMisAlignedObject(new MisAlignedObject('/'+path, false, neId)); - // this.#compareState(neId, {}, iState, auditReport, '/'+path); - } else { - logger.error("RESTCONF GET failed with error:\n" + result.errmsg); - throw new RuntimeException("RESTCONF GET failed with " + result.errmsg); } } } - } - }); - } + }); + } + + // Report undesired objects: is-configured=true, is-undesired=true + for (const neId in obsoleted) + for (const objectName in obsoleted[neId]) + auditReport.addMisAlignedObject(new MisAlignedObject('/'+obsoleted[neId][objectName].target, true, neId, true)); - // Report undesired objects: is-configured=true, is-undesired=true - for (const neId in obsoleted) - for (const objectName in obsoleted[neId]) - auditReport.addMisAlignedObject(new MisAlignedObject('/'+obsoleted[neId][objectName].target, true, neId, true)); + auditReport = this.#resolveAudit(auditReport); + } catch (err) { + auditReport.setErrorCode("500"); + auditReport.setErrorDetail(err.message); - const resolvedAuditReport = this.#resolveAudit(target, auditReport); + // under review with altiplano-team: + // auditReport.setSuccess(false); + throw err; + } const duration = Date.now()-startTS; logger.info("IntentHandler::onAudit() finished within {} ms", duration|0); - return resolvedAuditReport; + return auditReport; } /** @@ -1121,12 +1221,15 @@ export class IntentHandler extends CalloutHandler // Iterate sites to get indiciators let indicators = {}; - this.#logic.getSiteParameters(target, config).forEach(site => { + this.#logic.getSiteParameters(target, config, this.#deviceCache).forEach(site => { const neId = site['ne-id']; const neInfo = mds.getAllInfoFromDevices(neId); const neFamilyTypeRelease = neInfo.get(0).getFamilyTypeRelease(); const neType = neFamilyTypeRelease.split(':')[0]; const neVersion = neFamilyTypeRelease.split(':')[1]; + + // ensure we've got the user-friendly deviceName + site['ne-name'] = this.#deviceCache[neId]; const global = this.#logic.getGlobalParameters(target, config); const siteFTL = resourceProvider.getResource(this.#logic.getTemplateName(neId, neType)); diff --git a/templates/common_resources_graaljs/common/IntentLogic.mjs b/templates/common_resources_graaljs/common/IntentLogic.mjs index 5126562..6b12aac 100644 --- a/templates/common_resources_graaljs/common/IntentLogic.mjs +++ b/templates/common_resources_graaljs/common/IntentLogic.mjs @@ -53,10 +53,11 @@ export class IntentLogic { * * @param {string} target Intent target * @param {Dict} config Intent configuration + * @param {Dict} siteNames Used to translate siteId to siteNames (w/o API calls) * @returns {Dict} site-level settings (site.*) */ - static getSiteParameters(target, config) { + static getSiteParameters(target, config, siteNames) { return config; } diff --git a/templates/common_resources_graaljs/common/ResourceAdmin.mjs b/templates/common_resources_graaljs/common/ResourceAdmin.mjs index 7ef3806..4101806 100644 --- a/templates/common_resources_graaljs/common/ResourceAdmin.mjs +++ b/templates/common_resources_graaljs/common/ResourceAdmin.mjs @@ -9,8 +9,6 @@ /* global mds, logger, restClient, resourceProvider, utilityService */ /* eslint no-undef: "error" */ -let RuntimeException = classResolver.resolveClass("java.lang.RuntimeException"); - export class ResourceAdmin { /** * Executes nsp-inventory:find operation @@ -44,8 +42,34 @@ export class ResourceAdmin { } else if (httpStatus >= 400) { // Either client error (4xx) or server error (5xx) + let errmsg = "HTTP ERROR "+httpStatus; logger.warn("NSP response: {} {}", httpStatus, response); - result = { success: false, response: {}, errmsg: response}; + + if ((/]*>(.*)<\/body>/i); + if (errMatch) + errmsg = errMatch[1].trim(); // extracted errmsg from html body + } else { + // Extract error details from JSON response: + // + // Error details returned in accordance to RFC802 + // {"ietf-restconf:errors":{"error":[{ error details }]}} + // + // Error fields: + // error-type enumeration + // error-tag string + // error-app-tag? string + // error-path? instance-identifier + // error-message? string + // error-info? anydata + + const errorObject = JSON.parse(response); + if ('ietf-restconf:errors' in errorObject) + errmsg = errorObject['ietf-restconf:errors'].error.map(error => error["error-message"]).join(', '); + } + + result = { success: false, response: {}, errmsg: errmsg}; } else { logger.debug("NSP response: {} {}", httpStatus, response); @@ -106,8 +130,34 @@ export class ResourceAdmin { } else if (httpStatus >= 400) { // Either client error (4xx) or server error (5xx) + let errmsg = "HTTP ERROR "+httpStatus; logger.warn("NSP response: {} {}", httpStatus, response); - result = { success: false, response: {}, errmsg: response}; + + if ((/]*>(.*)<\/body>/i); + if (errMatch) + errmsg = errMatch[1].trim(); // extracted errmsg from html body + } else { + // Extract error details from JSON response: + // + // Error details returned in accordance to RFC802 + // {"ietf-restconf:errors":{"error":[{ error details }]}} + // + // Error fields: + // error-type enumeration + // error-tag string + // error-app-tag? string + // error-path? instance-identifier + // error-message? string + // error-info? anydata + + const errorObject = JSON.parse(response); + if ('ietf-restconf:errors' in errorObject) + errmsg = errorObject['ietf-restconf:errors'].error.map(error => error["error-message"]).join(', '); + } + + result = { success: false, response: {}, errmsg: errmsg}; } else { logger.debug("NSP response: {} {}", httpStatus, response); @@ -140,20 +190,17 @@ export class ResourceAdmin { const result = this.#nspFindEntry("/nsp-resource-pool:resource-pools/ip-resource-pools[name='"+pool+"' and scope='"+scope+"']/consumed-resources[reference='"+target+"']"); if (!result.success) - throw new RuntimeException("Find subnet failed with "+result.errmsg); + throw new Error("Find subnet failed with "+result.errmsg); - let subnet = ""; - if ('value' in result.response) { - subnet = result.response.value; - logger.info("subnet: {}", subnet ); - } else { - logger.info("subnet: to be reserved/obtained"); - } + if ('value' in result.response) + logger.info("subnet: {}", result.response.value ); + else + throw Error("Subnet for pool="+pool+" scope="+scope+" target="+target+" must be reserved/obtained first!"); const duration = Date.now()-startTS; logger.info("ResourceAdmin::getSubnet(pool={}, scope={}, target={}) finished within {} ms", pool, scope, target, duration|0); - return subnet; + return result.response.value; } /** @@ -167,7 +214,7 @@ export class ResourceAdmin { * @param {} target reference for reservation * @returns subnet as string, for example 10.0.0.0/31 (or empty string in error cases) * - * @throws {RuntimeException} obtain subnet failed + * @throws {Error} obtain subnet failed * **/ @@ -178,7 +225,7 @@ export class ResourceAdmin { const result = this.#nspFindEntry("/nsp-resource-pool:resource-pools/ip-resource-pools[name='"+pool+"' and scope='"+scope+"']/consumed-resources[reference='"+target+"']"); if (!result.success) - throw new RuntimeException("Find subnet failed with "+result.errmsg); + throw new Error("Find subnet failed with "+result.errmsg); let subnet = ""; if ('value' in result.response) { @@ -198,7 +245,7 @@ export class ResourceAdmin { const result = this.#restconfNspAction(resource, input); if (!result.success) - throw new RuntimeException("Obtain subnet failed with "+result.errmsg); + throw new Error("Obtain subnet failed with "+result.errmsg); subnet = result.response["nsp-resource-pool:output"]["consumed-resources"][0][0].value; logger.info("subnet: {} (new entry)", subnet ); @@ -252,20 +299,17 @@ export class ResourceAdmin { const result = this.#nspFindEntry( "/nsp-resource-pool:resource-pools/numeric-resource-pools[name='"+pool+"' and scope='"+scope+"']/num-consumed-resources[reference='"+target+"']"); if (!result.success) - throw new RuntimeException("Find id failed with "+result.errmsg); + throw new Error("Find id failed with "+result.errmsg); - let id = ""; - if ('value' in result.response) { - id = result.response.value; - logger.info("id: {}", id ); - } else { - logger.info("id: to be reserved/obtained"); - } + if ('value' in result.response) + logger.info("id: {}", result.response.value ); + else + throw Error("Id for pool="+pool+" scope="+scope+" target="+target+" must be reserved/obtained first!"); const duration = Date.now()-startTS; logger.info("ResourceAdmin::getId(pool={}, scope={}, target={}) finished within {} ms", pool, scope, target, duration|0); - return id; + return result.response.value; } /** @@ -277,7 +321,7 @@ export class ResourceAdmin { * @param {} target reference for reservation * @returns number as string * - * @throws {RuntimeException} obtain number failed + * @throws {Error} obtain number failed * **/ @@ -288,7 +332,7 @@ export class ResourceAdmin { const result = this.#nspFindEntry("/nsp-resource-pool:resource-pools/numeric-resource-pools[name='"+pool+"' and scope='"+scope+"']/num-consumed-resources[reference='"+target+"']"); if (!result.success) - throw new RuntimeException("Find id failed with "+result.errmsg); + throw new Error("Find id failed with "+result.errmsg); let id = ""; if ('value' in result.response) { @@ -306,7 +350,7 @@ export class ResourceAdmin { const result = this.#restconfNspAction(resource, input); if (!result.success) - throw new RuntimeException("Obtain id failed with "+result.errmsg); + throw new Error("Obtain id failed with "+result.errmsg); id = result.response["nsp-resource-pool:output"]["num-consumed-resources"][0].value; logger.info("id: {} (new entry)", id ); diff --git a/templates/ngJS iplink/intent-type-resources/Cisco IOS-XR.ftl b/templates/ngJS iplink/intent-type-resources/Cisco IOS-XR.ftl index 5c1ef48..0eed11a 100644 --- a/templates/ngJS iplink/intent-type-resources/Cisco IOS-XR.ftl +++ b/templates/ngJS iplink/intent-type-resources/Cisco IOS-XR.ftl @@ -1,6 +1,6 @@ <#setting number_format="computer"> { - "[${site.ne\-name}] INTERFACE ${site.port\-id}": { + "INTERFACE ${site.port\-id}": { "config": { "target": "Cisco-IOS-XR-um-interface-cfg:/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -26,7 +26,7 @@ }, "health": {} }, - "[${site.ne\-name}] ISIS INTERFACE ${site.port\-id}": { + "ISIS INTERFACE ${site.port\-id}": { "config": { "target": "Cisco-IOS-XR-um-router-isis-cfg:/router/isis/processes/process=isis/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -46,7 +46,7 @@ }, "health": {} }, - "[${site.ne\-name}] LDP INTERFACE ${site.port\-id}": { + "LDP INTERFACE ${site.port\-id}": { "config": { "target": "Cisco-IOS-XR-um-mpls-ldp-cfg:/mpls/ldp/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", diff --git a/templates/ngJS iplink/intent-type-resources/JunOS.ftl b/templates/ngJS iplink/intent-type-resources/JunOS.ftl index 777f308..95ac613 100644 --- a/templates/ngJS iplink/intent-type-resources/JunOS.ftl +++ b/templates/ngJS iplink/intent-type-resources/JunOS.ftl @@ -1,6 +1,6 @@ <#setting number_format="computer"> { - "[${site.ne\-name}] INTERFACE ${site.port\-id}": { + "INTERFACE ${site.port\-id}": { "config": { "target": "junos-conf-root:/configuration/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -30,7 +30,7 @@ }, "health": {} }, - "[${site.ne\-name}] LLDP INTERFACE ${site.port\-id}": { + "LLDP INTERFACE ${site.port\-id}": { "config": { "target": "junos-conf-root:/configuration/protocols/lldp/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -43,7 +43,7 @@ }, "health": {} }, - "[${site.ne\-name}] ISIS INTERFACE ${site.port\-id}.1": { + "ISIS INTERFACE ${site.port\-id}.1": { "config": { "target": "junos-conf-root:/configuration/protocols/isis/interface=${site.port\-id?url('ISO-8859-1')}.1", "operation": "replace", @@ -57,7 +57,7 @@ }, "health": {} }, - "[${site.ne\-name}] LDP INTERFACE ${site.port\-id}.1": { + "LDP INTERFACE ${site.port\-id}.1": { "config": { "target": "junos-conf-root:/configuration/protocols/ldp/interface=${site.port\-id?url('ISO-8859-1')}.1", "operation": "replace", diff --git a/templates/ngJS iplink/intent-type-resources/OpenConfig.ftl b/templates/ngJS iplink/intent-type-resources/OpenConfig.ftl index 2b4ce5e..c9120c8 100644 --- a/templates/ngJS iplink/intent-type-resources/OpenConfig.ftl +++ b/templates/ngJS iplink/intent-type-resources/OpenConfig.ftl @@ -1,6 +1,6 @@ <#setting number_format="computer"> { - "[${site.ne\-name}] INTERFACE ${site.port\-id}": { + "INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-interfaces:/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -75,7 +75,7 @@ } } }, - "[${site.ne\-name}] LLDP INTERFACE ${site.port\-id}": { + "LLDP INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-lldp:/lldp/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -90,7 +90,7 @@ } } }, - "[${site.ne\-name}] NETWORK INTERFACE ${site.port\-id}": { + "NETWORK INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-network-instance:/network-instances/network-instance=Base/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -107,7 +107,7 @@ } } }, - "[${site.ne\-name}] ISIS INTERFACE ${site.port\-id}": { + "ISIS INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-network-instance:/network-instances/network-instance=Base/protocols/protocol=ISIS,0/isis/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -168,7 +168,7 @@ --> } }, - "[${site.ne\-name}] MPLS INTERFACE ${site.port\-id}": { + "MPLS INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-network-instance:/network-instances/network-instance=Base/mpls/global/interface-attributes/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -188,7 +188,7 @@ } } }, - "[${site.ne\-name}] TE INTERFACE ${site.port\-id}": { + "TE INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-network-instance:/network-instances/network-instance=Base/mpls/te-interface-attributes/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -208,7 +208,7 @@ } } }, - "[${site.ne\-name}] LDP INTERFACE ${site.port\-id}": { + "LDP INTERFACE ${site.port\-id}": { "config": { "target": "openconfig-network-instance:/network-instances/network-instance=Base/mpls/signaling-protocols/ldp/interface-attributes/interfaces/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", diff --git a/templates/ngJS iplink/intent-type-resources/SR OS.ftl b/templates/ngJS iplink/intent-type-resources/SR OS.ftl index a2a07ed..d60e37c 100644 --- a/templates/ngJS iplink/intent-type-resources/SR OS.ftl +++ b/templates/ngJS iplink/intent-type-resources/SR OS.ftl @@ -1,7 +1,7 @@ <#setting number_format="computer"> -<#assign ifname="${site['ne-name']}_to_${site.peer['ne-name']}"> +<#assign ifname="${target}_${site['ne-name']}_to_${site.peer['ne-name']}"> { - "[${site.ne\-name}] PORT ${site.port\-id}": { + "PORT ${site.port\-id}": { "config": { "target": "nokia-conf:/configure/port=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -60,7 +60,7 @@ } } }, - "[${site.ne\-name}] IP INTERFACE ${ifname}": { + "IP INTERFACE ${ifname}": { "config": { "target": "nokia-conf:/configure/router=Base/interface=${ifname?url('ISO-8859-1')}", "operation": "replace", @@ -85,7 +85,7 @@ } } }, - "[${site.ne\-name}] ISIS INTERFACE ${ifname}": { + "ISIS INTERFACE ${ifname}": { "config": { "target": "nokia-conf:/configure/router=Base/isis=0/interface=${ifname?url('ISO-8859-1')}", "operation": "replace", @@ -113,7 +113,7 @@ } } }, - "[${site.ne\-name}] LDP INTERFACE ${ifname}": { + "LDP INTERFACE ${ifname}": { "config": { "target": "nokia-conf:/configure/router=Base/ldp/interface-parameters/interface=${ifname?url('ISO-8859-1')}", "operation": "replace", @@ -133,7 +133,7 @@ } } }, - "[${site.ne\-name}] TWAMP REFLECTOR": { + "TWAMP REFLECTOR": { "config": { "target": "nokia-conf:/configure/router=Base/twamp-light/reflector", "operation": "replace", @@ -152,7 +152,7 @@ } } }, - "[${site.ne\-name}] TWAMP SESSION ${target}": { + "TWAMP SESSION ${target}": { "config": { "target": "nokia-conf:/configure/oam-pm/session=${target?url('ISO-8859-1')}", "operation": "replace", diff --git a/templates/ngJS iplink/intent-type-resources/SRLinux.ftl b/templates/ngJS iplink/intent-type-resources/SRLinux.ftl index 18e5a10..e80454c 100644 --- a/templates/ngJS iplink/intent-type-resources/SRLinux.ftl +++ b/templates/ngJS iplink/intent-type-resources/SRLinux.ftl @@ -1,6 +1,6 @@ <#setting number_format="computer"> { - "[${site.ne\-name}] INTERFACE ${site.port\-id}": { + "INTERFACE ${site.port\-id}": { "config": { "target": "srl_nokia-interfaces:/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -45,7 +45,7 @@ } } }, - "[${site.ne\-name}] LLDP INTERFACE ${site.port\-id}": { + "LLDP INTERFACE ${site.port\-id}": { "config": { "target": "srl_nokia-system:/system/srl_nokia-lldp:lldp/interface=${site.port\-id?url('ISO-8859-1')}", "operation": "replace", @@ -70,7 +70,7 @@ } } }, - "[${site.ne\-name}] IP INTERFACE BINDING ${site.port\-id}.1": { + "IP INTERFACE BINDING ${site.port\-id}.1": { "config": { "target": "srl_nokia-network-instance:/network-instance=default/interface=${site.port\-id?url('ISO-8859-1')}.1", "operation": "replace", @@ -97,7 +97,7 @@ } } }, - "[${site.ne\-name}] ISIS INTERFACE ${site.port\-id}.1": { + "ISIS INTERFACE ${site.port\-id}.1": { "config": { "target": "srl_nokia-network-instance:/network-instance=default/protocols/srl_nokia-isis:isis/instance=0/interface=${site.port\-id?url('ISO-8859-1')}.1", "operation": "replace", diff --git a/templates/ngJS iplink/script-content.mjs b/templates/ngJS iplink/script-content.mjs index d6005ec..50bc98b 100644 --- a/templates/ngJS iplink/script-content.mjs +++ b/templates/ngJS iplink/script-content.mjs @@ -19,14 +19,14 @@ class IPLink extends (IntentLogic) { contextualErrorJsonObj["Value inconsistency"] = "endpoint-a and endpoint-b must resite on different devices!"; } - static getSiteParameters(target, config) { + static getSiteParameters(target, config, siteNames) { var sites = []; sites.push(config["endpoint-a"]); sites.push(config["endpoint-b"]); // Obtain ne-name from NSP inventory - sites[0]['ne-name'] = IntentHandler.getDeviceDetails(sites[0]['ne-id'])['ne-name']; - sites[1]['ne-name'] = IntentHandler.getDeviceDetails(sites[1]['ne-id'])['ne-name']; + sites[0]['ne-name'] = siteNames[sites[0]['ne-id']]; + sites[1]['ne-name'] = siteNames[sites[1]['ne-id']]; // Obtain an /31 subnet var subnet = ResourceAdmin.getSubnet("ip-pool", "global", target);