From fc41ae955f4eec0b757de447080fe86e3f6b26ac Mon Sep 17 00:00:00 2001 From: "Vyacheslav.Sviridov" Date: Thu, 9 Jun 2022 20:52:09 +0600 Subject: [PATCH] first alpha --- LICENSE | 40 +- README.md | 388 +++++++++--------- bin/enm-cli.js | 210 +++++----- .../AutoProvisioning/AutoProvisioning.js | 177 ++++---- .../AutoProvisioning/inputHandler.js | 201 +++++---- .../AutoProvisioning/nodes.js | 328 +++++++-------- .../AutoProvisioning/projects.js | 244 +++++------ .../TopologyBrowser/TopologyBrowser.js | 154 +++++++ .../TopologyBrowser/commands/alarms.js | 252 ++++++------ .../TopologyBrowser/commands/commit.js | 35 ++ .../TopologyBrowser/commands/config.js | 75 ++-- .../TopologyBrowser/commands/description.js | 21 + .../TopologyBrowser/commands/end.js | 18 +- .../TopologyBrowser/commands/get.js | 23 ++ .../TopologyBrowser/commands/goToFdn.js} | 57 +-- .../TopologyBrowser/commands/home.js | 17 +- .../TopologyBrowser/commands/initialPrompt.js | 43 +- .../commands/nextAttributes.js | 14 + .../TopologyBrowser/commands/nextObjects.js | 57 +++ .../TopologyBrowser/commands/search.js | 127 ++++++ .../TopologyBrowser/commands/set.js | 54 ++- .../TopologyBrowser/commands/setAttribute.js | 12 + .../commands/setIdByCommand.js | 20 +- .../TopologyBrowser/commands/show.js | 38 +- .../TopologyBrowser/commands/sync.js | 26 ++ .../TopologyBrowser/commands/up.js | 25 +- lib/applications/TopologyBrowser/help.js | 25 ++ .../TopologyBrowser/inputHandler.js | 109 +++++ .../TopologyBrowser/inputValue.js | 313 +++++++------- .../{AxiosHttpClient => }/AxiosHttpClient.js | 98 ++--- lib/components/{ENM => }/ENM.js | 63 +-- .../components}/SpinnerWithCounter.js | 68 +-- .../TopologyBrowser/TopologyBrowser.js | 132 ------ .../TopologyBrowser/commands/commit.js | 34 -- .../TopologyBrowser/commands/description.js | 21 - .../TopologyBrowser/commands/get.js | 20 - .../commands/nextAttributes.js | 14 - .../TopologyBrowser/commands/nextObjects.js | 34 -- .../TopologyBrowser/commands/setAttribute.js | 14 - .../TopologyBrowser/commands/sync.js | 30 -- .../TopologyBrowser/inputHandler.js | 136 ------ package.json | 86 ++-- util/banner.js | 11 + util/createNext.js | 32 +- util/eventTimeToString.js | 10 + util/logAlarm.js | 26 ++ util/logAttributeData.js | 84 ++++ util/logAttributes.js | 34 ++ util/logCommit.js | 10 + util/logDetails.js | 11 + util/logError.js | 82 ++-- util/logNode.js | 96 ++--- util/logProject.js | 70 ++-- util/validation.js | 73 +++- 54 files changed, 2416 insertions(+), 1976 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 bin/enm-cli.js rename lib/{components => applications}/AutoProvisioning/AutoProvisioning.js (91%) mode change 100644 => 100755 rename lib/{components => applications}/AutoProvisioning/inputHandler.js (66%) mode change 100644 => 100755 rename lib/{components => applications}/AutoProvisioning/nodes.js (96%) mode change 100644 => 100755 rename lib/{components => applications}/AutoProvisioning/projects.js (95%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/TopologyBrowser.js rename lib/{components => applications}/TopologyBrowser/commands/alarms.js (66%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/commit.js rename lib/{components => applications}/TopologyBrowser/commands/config.js (64%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/description.js rename lib/{components => applications}/TopologyBrowser/commands/end.js (53%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/get.js rename lib/{components/TopologyBrowser/commands/fdn.js => applications/TopologyBrowser/commands/goToFdn.js} (56%) mode change 100644 => 100755 rename lib/{components => applications}/TopologyBrowser/commands/home.js (78%) mode change 100644 => 100755 rename lib/{components => applications}/TopologyBrowser/commands/initialPrompt.js (60%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/nextAttributes.js create mode 100755 lib/applications/TopologyBrowser/commands/nextObjects.js create mode 100755 lib/applications/TopologyBrowser/commands/search.js rename lib/{components => applications}/TopologyBrowser/commands/set.js (65%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/setAttribute.js rename lib/{components => applications}/TopologyBrowser/commands/setIdByCommand.js (60%) mode change 100644 => 100755 rename lib/{components => applications}/TopologyBrowser/commands/show.js (69%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/commands/sync.js rename lib/{components => applications}/TopologyBrowser/commands/up.js (60%) mode change 100644 => 100755 create mode 100755 lib/applications/TopologyBrowser/help.js create mode 100755 lib/applications/TopologyBrowser/inputHandler.js rename lib/{components => applications}/TopologyBrowser/inputValue.js (77%) mode change 100644 => 100755 rename lib/components/{AxiosHttpClient => }/AxiosHttpClient.js (80%) mode change 100644 => 100755 rename lib/components/{ENM => }/ENM.js (82%) mode change 100644 => 100755 rename {util => lib/components}/SpinnerWithCounter.js (94%) mode change 100644 => 100755 delete mode 100644 lib/components/TopologyBrowser/TopologyBrowser.js delete mode 100644 lib/components/TopologyBrowser/commands/commit.js delete mode 100644 lib/components/TopologyBrowser/commands/description.js delete mode 100644 lib/components/TopologyBrowser/commands/get.js delete mode 100644 lib/components/TopologyBrowser/commands/nextAttributes.js delete mode 100644 lib/components/TopologyBrowser/commands/nextObjects.js delete mode 100644 lib/components/TopologyBrowser/commands/setAttribute.js delete mode 100644 lib/components/TopologyBrowser/commands/sync.js delete mode 100644 lib/components/TopologyBrowser/inputHandler.js mode change 100644 => 100755 package.json create mode 100755 util/banner.js mode change 100644 => 100755 util/createNext.js create mode 100755 util/eventTimeToString.js create mode 100755 util/logAlarm.js create mode 100755 util/logAttributeData.js create mode 100755 util/logAttributes.js create mode 100755 util/logCommit.js create mode 100755 util/logDetails.js mode change 100644 => 100755 util/logError.js mode change 100644 => 100755 util/logNode.js mode change 100644 => 100755 util/logProject.js mode change 100644 => 100755 util/validation.js diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index 30d66cb..eb6cf53 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2021 Vyacheslav Sviridov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2021 Vyacheslav Sviridov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index aa52a3e..29ef006 --- a/README.md +++ b/README.md @@ -1,194 +1,194 @@ -# Cli application based on ENM AutoProvisioning API - -[![Github version](https://img.shields.io/github/package-json/version/vvsviridov/prvn-cli?label=prvn-cli&color=brightgreen&logo=github)](https://github.com/vvsviridov/prvn-cli) -[![Npm version](https://img.shields.io/npm/v/prvn-cli?color=red&logo=npm&label=prvn-cli)](https://www.npmjs.com/package/prvn-cli) - -## Main goal - -A simple CLI interface for AutoProvisioning API. - -## Installation - -First you need **node.js** which can be downloaded from official site [nodejs.org](https://nodejs.org/en/download/) and installed as described in the docs. - -Then you can run directly from NPM without installation - -``` -npx prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com -``` - -Or install with NPM - -``` -npm i prvn-cli -``` - -Or download this repository and run from the project root directory ... - -``` -npm install -``` - -... to install dependencies, - -``` -npm link -``` - -... to add application to your OS $PATH variable if you want to run it from anywhere. -Now you can launch apllication - -``` -prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com -``` - -## Usage - -Recommended environment is Windows Terminal (not _cmd.exe_) or any shell with rich formatting support. After application successfully launched youll see root content and available commands. - -### Help Page -``` -> prvn-cli --help -Usage: prvn-cli [options] - -Options: - -V, --version output the version number - -l, --login ENM User Login - -p, --password ENM User Password - -u, --url ENM Url - -h, --help display help for command -``` -### Connection -``` ->prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com -✔ Login in... -✔ Getting projects... -323 projects> (Use arrow keys or type to search) - ────────────── -> [new] - [exit] - ────────────── - Project1 (1) 1✅ - Project2 (2) 1✅ 1⌛ - Project3 (2) 1❌ 1⌚ - Project4 (1) 1❌ -``` - -### Working with Projects - -- `[new]` - Import an Auto Provisioning project to start Auto Provisioning workflows based on the content of the AutoProvisioning project. The Auto Provisioning project file contains project related data in the projectInfo.xml file and node folders which contain configurations required to execute AutoProvisioning use-cases. -- `[exit]` - Exit this app. - -Start typing and you see only commands and projects matches input. - -``` -323 projects> pro -────────────── -────────────── -> Project1 (1) 1✅ - Project2 (2) 1✅ 1⌛ - Project3 (2) 1❌ 1⌚ - Project4 (1) 1❌ -SubNetwork=ONRM_ROOT_MO> ex -────────────── ->[exit] -────────────── -``` - -### Working with Single Project - -Select project you want to work with. - -Available commands are: - -- `[delete]` - Delete of an Auto Provisioning project removes an Auto Provisioning project and all the Auto Provisioning data for nodes within that project. This includes removal or rollback of any ongoing Auto Provisioning workflows within that project. -- `[back]` - Return to projects. -- `[exit]` - Exit this app. - -``` -323 projects> Project1 (1) 1✅ -✔ Getting Project1s status... -✔ Getting Project1s properties... - - Project id : Project1 - Author : Ericsson - Creation Date : 2019-01-06 11:23:39 - Description : Project1 description - Nodes : - - RadioNode1 - RadioNode - 3432-762-238 - 192.168.192.168 - Successful - Integration Completed - -Project1> (Use arrow keys or type to search) - ────────────── -> [delete] - [back] - [exit] - ────────────── - RadioNode1 -``` - -### Wotking with Node - -Select node ... - -``` -Project1 (RadioNode1) > (Use arrow keys or type to search) - ────────────── -> [status] - [properties] - [delete] - [bind] - [cancel] - [resume] - [configurations] - [siteinstall] - [back] -(Move up and down to reveal more choices) -``` - -Available commands are: - -- `[status]` - Retrieving Auto Provisioning node status returns the node status information for each task that has been executed for the specified node. -- `[properties]` - Retrieving Auto Provisioning node properties returns the node properties for each task that has been executed for the specified node. -- `[delete]` - Delete an Auto Provisioning node removes the Auto Provisioning data for a Network Element. If a node is the last node in a project and there are no profiles associated with the project the project will automatically be deleted. -- `[bind]` - Binding a hardware serial number to a node configuration associates the specified node configurations with a hardware serial number for Zero Touch Integration or Hardware Replace. -- `[cancel]` - Cancelling the auto provisioning activity rolls back an AutoProvisoning workflow for Node Integration. For expansion a node is rolled back to it\s original configuration if additional configurations have been applied to the node. -- `[resume]` - Resuming the auto provisioning activity recommences an Auto Provisioning workflow that is suspended. -- `[configurations]` - Uploading an auto provisioning configuration replaces a configuration file that was part of the initial Auto Provisioning node configuration. -- `[siteinstall]` - Download Site Installation File (SIF) that is required to be taken on site for LMT Integration or LMT Hardware Replace. -- `[back]` - Return to project\s nodes. -- `[exit]` - Exit this app. - - -## Contribution - -1. [Fork it] -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am Add some feature`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request - -## Known Issues - -### flickering issue - -In some windows terminal spinner may look flickery. To fix this you need to modify file ./node_modules/ora/index.js - -Add 1 to _clearLine()_ on this line - -```javascript -this.stream.clearLine(1); -``` - -## Credits - -[Contact Me](https://github.com/vvsviridov/) to request new feature or bugs reporting. - -## Changes - -1.0.0 - is released +# Cli application based on ENM AutoProvisioning API + +[![Github version](https://img.shields.io/github/package-json/version/vvsviridov/prvn-cli?label=prvn-cli&color=brightgreen&logo=github)](https://github.com/vvsviridov/prvn-cli) +[![Npm version](https://img.shields.io/npm/v/prvn-cli?color=red&logo=npm&label=prvn-cli)](https://www.npmjs.com/package/prvn-cli) + +## Main goal + +A simple CLI interface for AutoProvisioning API. + +## Installation + +First you need **node.js** which can be downloaded from official site [nodejs.org](https://nodejs.org/en/download/) and installed as described in the docs. + +Then you can run directly from NPM without installation + +``` +npx prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com +``` + +Or install with NPM + +``` +npm i prvn-cli +``` + +Or download this repository and run from the project root directory ... + +``` +npm install +``` + +... to install dependencies, + +``` +npm link +``` + +... to add application to your OS $PATH variable if you want to run it from anywhere. +Now you can launch apllication + +``` +prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com +``` + +## Usage + +Recommended environment is Windows Terminal (not _cmd.exe_) or any shell with rich formatting support. After application successfully launched youll see root content and available commands. + +### Help Page +``` +> prvn-cli --help +Usage: prvn-cli [options] + +Options: + -V, --version output the version number + -l, --login ENM User Login + -p, --password ENM User Password + -u, --url ENM Url + -h, --help display help for command +``` +### Connection +``` +>prvn-cli -l USERNAME -p PASSWORD -u https://enm.your.company.domain.com +✔ Login in... +✔ Getting projects... +323 projects> (Use arrow keys or type to search) + ────────────── +> [new] + [exit] + ────────────── + Project1 (1) 1✅ + Project2 (2) 1✅ 1⌛ + Project3 (2) 1❌ 1⌚ + Project4 (1) 1❌ +``` + +### Working with Projects + +- `[new]` - Import an Auto Provisioning project to start Auto Provisioning workflows based on the content of the AutoProvisioning project. The Auto Provisioning project file contains project related data in the projectInfo.xml file and node folders which contain configurations required to execute AutoProvisioning use-cases. +- `[exit]` - Exit this app. + +Start typing and you see only commands and projects matches input. + +``` +323 projects> pro +────────────── +────────────── +> Project1 (1) 1✅ + Project2 (2) 1✅ 1⌛ + Project3 (2) 1❌ 1⌚ + Project4 (1) 1❌ +SubNetwork=ONRM_ROOT_MO> ex +────────────── +>[exit] +────────────── +``` + +### Working with Single Project + +Select project you want to work with. + +Available commands are: + +- `[delete]` - Delete of an Auto Provisioning project removes an Auto Provisioning project and all the Auto Provisioning data for nodes within that project. This includes removal or rollback of any ongoing Auto Provisioning workflows within that project. +- `[back]` - Return to projects. +- `[exit]` - Exit this app. + +``` +323 projects> Project1 (1) 1✅ +✔ Getting Project1s status... +✔ Getting Project1s properties... + + Project id : Project1 + Author : Ericsson + Creation Date : 2019-01-06 11:23:39 + Description : Project1 description + Nodes : + + RadioNode1 + RadioNode + 3432-762-238 + 192.168.192.168 + Successful + Integration Completed + +Project1> (Use arrow keys or type to search) + ────────────── +> [delete] + [back] + [exit] + ────────────── + RadioNode1 +``` + +### Wotking with Node + +Select node ... + +``` +Project1 (RadioNode1) > (Use arrow keys or type to search) + ────────────── +> [status] + [properties] + [delete] + [bind] + [cancel] + [resume] + [configurations] + [siteinstall] + [back] +(Move up and down to reveal more choices) +``` + +Available commands are: + +- `[status]` - Retrieving Auto Provisioning node status returns the node status information for each task that has been executed for the specified node. +- `[properties]` - Retrieving Auto Provisioning node properties returns the node properties for each task that has been executed for the specified node. +- `[delete]` - Delete an Auto Provisioning node removes the Auto Provisioning data for a Network Element. If a node is the last node in a project and there are no profiles associated with the project the project will automatically be deleted. +- `[bind]` - Binding a hardware serial number to a node configuration associates the specified node configurations with a hardware serial number for Zero Touch Integration or Hardware Replace. +- `[cancel]` - Cancelling the auto provisioning activity rolls back an AutoProvisoning workflow for Node Integration. For expansion a node is rolled back to it\s original configuration if additional configurations have been applied to the node. +- `[resume]` - Resuming the auto provisioning activity recommences an Auto Provisioning workflow that is suspended. +- `[configurations]` - Uploading an auto provisioning configuration replaces a configuration file that was part of the initial Auto Provisioning node configuration. +- `[siteinstall]` - Download Site Installation File (SIF) that is required to be taken on site for LMT Integration or LMT Hardware Replace. +- `[back]` - Return to project\s nodes. +- `[exit]` - Exit this app. + + +## Contribution + +1. [Fork it] +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am Add some feature`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Known Issues + +### flickering issue + +In some windows terminal spinner may look flickery. To fix this you need to modify file ./node_modules/ora/index.js + +Add 1 to _clearLine()_ on this line + +```javascript +this.stream.clearLine(1); +``` + +## Credits + +[Contact Me](https://github.com/vvsviridov/) to request new feature or bugs reporting. + +## Changes + +1.0.0 - is released diff --git a/bin/enm-cli.js b/bin/enm-cli.js old mode 100644 new mode 100755 index d3a4c45..2159ba5 --- a/bin/enm-cli.js +++ b/bin/enm-cli.js @@ -1,98 +1,112 @@ -#!/usr/bin/env node - -const { Command, Option } = require('commander') -const pkg = require('../package.json') -const inquirer = require('inquirer') -const { isEmpty } = require('../util/validation') - -require('dotenv').config() - -const AutoProvisioning = require('../lib/components/AutoProvisioning/AutoProvisioning') -const TopologyBrowser = require('../lib/components/TopologyBrowser/TopologyBrowser') - -const logError = require('../util/logError') - -const applications = ['tplg', 'prvn'] - -const program = new Command() -program - .version(pkg.version) - // .option('-l, --login ', 'ENM User Login') - // .option('-p, --password ', 'ENM User Password') - .addOption(new Option('-l, --login ', 'ENM User Login').env('LOGIN')) - .addOption(new Option('-p, --password ', 'ENM User Password').env('PASSWORD')) - .addOption(new Option('-a, --application ', 'Start specified application') - .choices(applications) - .default('tplg') - ) - .requiredOption('-u, --url ', 'ENM Url') - .parse(process.argv) - - -const options = program.opts() - - - -async function promptUsername() { - const input = await inquirer.prompt([ - { - type: 'input', - name: 'value', - suffix: chalk.bgGreen('?'), - message: 'Type ENM login', - validate: isEmpty, - } - ]) - return input.value -} - - -async function promptPassword() { - const input = await inquirer.prompt([ - { - type: 'password', - name: 'value', - message: `Type ${options.login}'s ENM password`, - validate: isEmpty, - } - ]) - return input.value -} - - -async function selectApplication() { - let selectedApp - if (options.application && options.application in applications) { - selectedApp = options.application - } else { - const input = await inquirer.prompt([ - { - type: 'list', - name: 'application', - suffix: '💾', - message, - choices: applications - }]) - selectedApp = input.application - } - return { - tplg: TopologyBrowser, - prvn: AutoProvisioning - }[selectedApp] -} - -async function main() { - try { - const app = new selectApplication()(options.login || await promptUsername(), options.password || await promptPassword(), options.url) - const result = await app.login() - const { code } = result - if (code === 'SUCCESS') { - await app.inputHandler() - await app.logout() - } - } catch (error) { - logError(error) - } -} - -; (async () => await main())() +#!/usr/bin/env node + +const { Command, Option } = require('commander') +const pkg = require('../package.json') +const inquirer = require('inquirer') +const path = require('path') + +const { isEmpty } = require('../util/validation') + +require('dotenv').config({ path: [...__dirname.split(path.sep).slice(0,-1), '.env'].join(path.sep) }) + +const AutoProvisioning = require('../lib/applications/AutoProvisioning/AutoProvisioning') +const TopologyBrowser = require('../lib/applications/TopologyBrowser/TopologyBrowser') + +const logError = require('../util/logError') + +if (process.env.NODE_ENV === 'development') { + process.on('uncaughtException', function (exception) { + console.log(exception); + }) +} + +const applications = [ + { + id: 'tplg', + appClass: TopologyBrowser, + name: 'Topology Browser', + }, + { + id: 'prvn', + appClass: AutoProvisioning, + name: 'Auto Provisioning', + } +] +const appIds = applications.map(item => item.id) + +const program = new Command() +program + .version(pkg.version) + .addOption(new Option('-l, --login ', 'ENM User Login').env('LOGIN')) + .addOption(new Option('-p, --password ', 'ENM User Password').env('PASSWORD')) + .addOption(new Option('-a, --application ', 'Start specified application') + .choices(appIds) + ) + .requiredOption('-u, --url ', 'ENM Url') + .parse(process.argv) + + +const options = program.opts() + + +async function promptUsername() { + const input = await inquirer.prompt([ + { + type: 'input', + name: 'value', + prefix: '👤', + message: 'Type ENM login:', + validate: isEmpty, + } + ]) + return input.value +} + + +async function promptPassword() { + const input = await inquirer.prompt([ + { + type: 'password', + name: 'value', + prefix: '🔑', + message: 'Type ENM password:', + validate: isEmpty, + } + ]) + return input.value +} + + +async function selectApplication() { + let selectedApp + if (options.application && appIds.includes(options.application)) { + selectedApp = options.application + } else { + const input = await inquirer.prompt([ + { + type: 'list', + name: 'application', + prefix: '💾', + message: 'Select Application:', + choices: applications.map(item => ({name: item.name, value: item.id, short: item.id})) + }]) + selectedApp = input.application + } + const { appClass } = applications.find(item => item.id === selectedApp) + const login = options.login || await promptUsername() + const password = options.password || await promptPassword() + return new appClass(login, password, options.url) +} + +async function main() { + try { + const app = await selectApplication() + await app.login() + await app.inputHandler() + await app.logout() + } catch (error) { + logError(error) + } +} + +;(async () => await main())() diff --git a/lib/components/AutoProvisioning/AutoProvisioning.js b/lib/applications/AutoProvisioning/AutoProvisioning.js old mode 100644 new mode 100755 similarity index 91% rename from lib/components/AutoProvisioning/AutoProvisioning.js rename to lib/applications/AutoProvisioning/AutoProvisioning.js index c129d56..b1af52a --- a/lib/components/AutoProvisioning/AutoProvisioning.js +++ b/lib/applications/AutoProvisioning/AutoProvisioning.js @@ -1,90 +1,89 @@ -const ENM = require('../ENM/ENM') - -const { getProjects, getProjectData, deleteProject, newProject } = require('./projects') -const { - getNode, - getNodeStatus, - getNodeProperties, - bindNode, - cancelNode, - resumeNode, - configurationsNode, - siteinstallNode -} = require('./nodes') -const inputHandler = require('./inputHandler') -const createNext = require('../../../util/createNext') - -class AutoProvisioning extends ENM { - constructor(username, password, url) { - super(username, password, url) - - this.appUrl = '/auto-provisioning/v1' - this.projects = null - this.projectIndex = -1 - this.nodes = null - this.nodeIndex = -1 - this.commands = null - this.choices = null - this.help = 'No results...' - } - - async getProjects() { - return await getProjects.call(this) - } - - async getProjectData() { - return await getProjectData.call(this) - } - - async newProject() { - await newProject.call(this) - } - - async deleteProject() { - await deleteProject.call(this) - } - - async getNode() { - return await getNode.call(this) - } - - async getNodeStatus() { - await getNodeStatus.call(this) - } - - async getNodeProperties() { - await getNodeProperties.call(this) - } - - async bindNode() { - await bindNode.call(this) - } - - async cancelNode() { - await cancelNode.call(this) - } - - async resumeNode() { - await resumeNode.call(this) - } - - async configurationsNode() { - await configurationsNode.call(this) - } - - async siteinstallNode() { - await siteinstallNode.call(this) - } - - async next(input) { - return createNext.call(this, input ? input : '') - } - - async inputHandler() { - await inputHandler.call(this) - } - -} - - +const ENM = require('../../components/ENM') + +const { getProjects, getProjectData, deleteProject, newProject } = require('./projects') +const { + getNode, + getNodeStatus, + getNodeProperties, + bindNode, + cancelNode, + resumeNode, + configurationsNode, + siteinstallNode +} = require('./nodes') +const inputHandler = require('./inputHandler') +const createNext = require('../../../util/createNext') + +class AutoProvisioning extends ENM { + constructor(username, password, url) { + super(username, password, url) + + this.appUrl = '/auto-provisioning/v1' + this.projects = null + this.projectIndex = -1 + this.nodes = null + this.nodeIndex = -1 + this.prompt = '' + this.help = 'No results...' + } + + async getProjects() { + return await getProjects.call(this) + } + + async getProjectData() { + return await getProjectData.call(this) + } + + async newProject() { + await newProject.call(this) + } + + async deleteProject() { + await deleteProject.call(this) + } + + async getNode() { + return await getNode.call(this) + } + + async getNodeStatus() { + await getNodeStatus.call(this) + } + + async getNodeProperties() { + await getNodeProperties.call(this) + } + + async bindNode() { + await bindNode.call(this) + } + + async cancelNode() { + await cancelNode.call(this) + } + + async resumeNode() { + await resumeNode.call(this) + } + + async configurationsNode() { + await configurationsNode.call(this) + } + + async siteinstallNode() { + await siteinstallNode.call(this) + } + + async next(input) { + return createNext.call(this, input ? input : '') + } + + async inputHandler() { + await inputHandler.call(this) + } + +} + + module.exports = AutoProvisioning \ No newline at end of file diff --git a/lib/components/AutoProvisioning/inputHandler.js b/lib/applications/AutoProvisioning/inputHandler.js old mode 100644 new mode 100755 similarity index 66% rename from lib/components/AutoProvisioning/inputHandler.js rename to lib/applications/AutoProvisioning/inputHandler.js index eb3fd5d..f572344 --- a/lib/components/AutoProvisioning/inputHandler.js +++ b/lib/applications/AutoProvisioning/inputHandler.js @@ -1,107 +1,96 @@ -const inquirer = require('inquirer') -const chalk = require('chalk') - -const logError = require('../../../util/logError') -const { isEmpty } = require('../../../util/validation') - - -inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) - - -async function commandOther(prvn, prompt, command) { - const choosedIndex = prvn.choices.indexOf(command) - if (choosedIndex !== -1) { - if (prvn.nodes) { - prvn.nodeIndex = choosedIndex - prompt = prvn.getNode() - } else { - prvn.projectIndex = choosedIndex - prompt = await prvn.getProjectData() - } - } - return prompt -} - - -async function handleCommand(prvn, prompt, command) { - const [, cmd] = command.match(/\[(\w+)\]/) || [, command] - switch (cmd) { - - case 'exit': - return - case 'new': - await prvn.newProject() - break - case 'back': - prompt = prvn.nodeIndex ? await prvn.getProjects() : await prvn.getProjectData() - break - case 'delete': - prvn.nodeIndex ? prvn.deleteNode() : await prvn.deleteProject() - break - - case 'status': - await prvn.getNodeStatus() - break - case 'properties': - await prvn.getNodeProperties() - break - case 'bind': - await prvn.bindNode() - break - case 'cancel': - await prvn.cancelNode() - break - case 'resume': - await prvn.resumeNode() - break - case 'configurations': - await prvn.configurationsNode() - break - case 'siteinstall': - await prvn.siteinstallNode() - break - - default: - prompt = await commandOther(prvn, prompt, command) - } - return prompt -} - - -async function inputHandlerLoop(prvn) { - let prompt = await prvn.getProjects() - let prefix = '' - while (true) { - try { - const input = await inquirer.prompt([ - { - type: 'autocomplete', - name: 'command', - message: chalk.bold.blue(prompt), - pageSize: 10, - prefix: chalk.bold.grey(prefix), - suffix: chalk.bold.blue('>'), - validate: isEmpty, - source: async (answers, input) => await prvn.next(input), - emptyText: prvn.help, - } - ]) - prompt = await handleCommand(prvn, prompt, input.command) - if (!prompt) break - } catch (error) { - logError(error) - } - } -} - - -async function inputHandler() { - try { - await inputHandlerLoop(this) - } catch (error) { - logError(error) - } -} - - +const inquirer = require('inquirer') +const chalk = require('chalk') + +const logError = require('../../../util/logError') +const { isEmpty } = require('../../../util/validation') + + +inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) + + +async function commandOther(prvn, command) { + const choosedIndex = prvn.choices.indexOf(command) + if (choosedIndex !== -1) { + if (prvn.nodes) { + prvn.nodeIndex = choosedIndex + prvn.getNode() + } else { + prvn.projectIndex = choosedIndex + await prvn.getProjectData() + } + } +} + + +async function handleCommand(prvn, command) { + const [, cmd] = command.match(/\[(\w+)\]/) || [, command] + switch (cmd) { + + case 'exit': + prvn.prompt = '' + break + case 'new': + await prvn.newProject() + break + case 'back': + prvn.nodeIndex ? await prvn.getProjects() : await prvn.getProjectData() + break + case 'delete': + prvn.nodeIndex ? prvn.deleteNode() : await prvn.deleteProject() + break + + case 'status': + await prvn.getNodeStatus() + break + case 'properties': + await prvn.getNodeProperties() + break + case 'bind': + await prvn.bindNode() + break + case 'cancel': + await prvn.cancelNode() + break + case 'resume': + await prvn.resumeNode() + break + case 'configurations': + await prvn.configurationsNode() + break + case 'siteinstall': + await prvn.siteinstallNode() + break + + default: + await commandOther(prvn, command) + } +} + + +async function inputHandler() { + await this.getProjects() + while (true) { + try { + const input = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'command', + message: chalk.bold.blue(this.prompt), + pageSize: 10, + prefix: '', + suffix: chalk.bold.blue('>'), + validate: isEmpty, + source: async (answers, input) => await this.next(input), + emptyText: this.help, + } + ]) + await handleCommand(this, input.command) + if (!this.prompt) break + } catch (error) { + logError(error) + } + } +} + + module.exports = inputHandler \ No newline at end of file diff --git a/lib/components/AutoProvisioning/nodes.js b/lib/applications/AutoProvisioning/nodes.js old mode 100644 new mode 100755 similarity index 96% rename from lib/components/AutoProvisioning/nodes.js rename to lib/applications/AutoProvisioning/nodes.js index f3f3ba5..3c859b6 --- a/lib/components/AutoProvisioning/nodes.js +++ b/lib/applications/AutoProvisioning/nodes.js @@ -1,165 +1,165 @@ -const fs = require('fs') -const fsPromises = require('fs').promises -const path = require('path') -const FormData = require('form-data') -const chalk = require('chalk') -const inquirer = require('inquirer') - -const { logNodeStatus, logNodeProperties } = require('../../../util/logNode') -const { isValidHardwareId } = require('../../../util/validation') - -const nodeCommands = [ - 'status', 'properties', 'delete', - 'bind', 'cancel', 'resume', - 'configurations', 'siteinstall', 'back', - 'exit' -] - -const nodeCommandsHelp = [ - '[status] - Retrieving Auto Provisioning node status returns the node status information for each task that has been executed for the specified node.', - '[properties] - Retrieving Auto Provisioning node properties returns the node properties for each task that has been executed for the specified node.', - '[delete] - Delete an Auto Provisioning node removes the Auto Provisioning data for a Network Element. If a node is the last node in a project and there are no profiles associated with the project the project will automatically be deleted.', - '[bind] - Binding a hardware serial number to a node configuration associates the specified node configurations with a hardware serial number for Zero Touch Integration or Hardware Replace.', - '[cancel] - Cancelling the auto provisioning activity rolls back an AutoProvisoning workflow for Node Integration. For expansion a node is rolled back to it\'s original configuration if additional configurations have been applied to the node.', - '[resume] - Resuming the auto provisioning activity recommences an Auto Provisioning workflow that is suspended.', - '[configurations] - Uploading an auto provisioning configuration replaces a configuration file that was part of the initial Auto Provisioning node configuration.', - '[siteinstall] - Download Site Installation File (SIF) that is required to be taken on site for LMT Integration or LMT Hardware Replace.', - '[back] - Return to project\'s nodes.', - '[exit] - Exit this app.', -] - - -async function getNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - this.commands = nodeCommands - this.help = nodeCommandsHelp.join('\n ') - this.choices = [] // this.nodes.filter(item => item !== nodeId) - return `${projectId} (${nodeId}) ` -} - - -async function getNodeStatus() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const axiosConfig = { - text: `Getting ${nodeId}'s status...`, - method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}` - } - const { data: { statusEntries } } = await this.httpClient.request(axiosConfig) - logNodeStatus(statusEntries) -} - - -async function getNodeProperties() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const axiosConfig = { - text: `Getting ${nodeId}'s properties...`, - method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}?filter=properties` - } - const { data: { attributes, attributeGroups } } = await this.httpClient.request(axiosConfig) - logNodeProperties(attributes, attributeGroups) -} - - -async function bindNode() { - const hardwareId = await inquirer.prompt([ - { - type: 'input', - name: 'value', - suffix: chalk.bgGreen('?'), - message: 'Type hardwareId', - validate: input => isValidHardwareId(input), - } - ]) - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const axiosConfig = { - text: `Binding ${hardwareId} to ${nodeId}...`, - method: 'put', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/bind`, - data: { - hardwareId, - }, - } - const { statusText } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Binding ${statusText}`)) -} - - -async function cancelNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const axiosConfig = { - text: `Canceling ${nodeId}...`, - method: 'post', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/cancel`, - } - const { statusText } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Canceling ${statusText}`)) -} - - -async function resumeNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const axiosConfig = { - text: `Resuming ${nodeId}...`, - method: 'post', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/resume`, - } - const { statusText } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Resuming ${statusText}`)) -} - - -async function configurationsNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const fileNameInput = await inquirer.prompt([{ - type: 'file-tree-selection', - name: 'nodeFile', - message: 'Choose a node file...', - }]) - const formData = new FormData() - formData.append('file', fs.createReadStream(fileNameInput.nodeFile)) - const axiosConfig = { - text: `Uploading ${nodeId} configuration to ${projectId}...`, - method: 'put', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations`, - headers: formData.getHeaders(), - data: formData - } - const { statusText } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Resuming ${statusText}`)) -} - - -async function siteinstallNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const saveFileName = path.join(process.cwd(), `Site_Install_${nodeId}.xml`) - const axiosConfig = { - text: `Downloading site install file ${nodeId}...`, - method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations/siteinstall`, - } - const { data } = await this.httpClient.request(axiosConfig) - await fsPromises.writeFile(saveFileName, data) - console.log(chalk.bgGreen(`Download site install file to ${saveFileName}`)) -} - - -module.exports = { - getNode, - getNodeStatus, - getNodeProperties, - bindNode, - cancelNode, - resumeNode, - configurationsNode, - siteinstallNode +const fs = require('fs') +const fsPromises = require('fs').promises +const path = require('path') +const FormData = require('form-data') +const chalk = require('chalk') +const inquirer = require('inquirer') + +const { logNodeStatus, logNodeProperties } = require('../../../util/logNode') +const { isValidHardwareId } = require('../../../util/validation') + +const nodeCommands = [ + 'status', 'properties', 'delete', + 'bind', 'cancel', 'resume', + 'configurations', 'siteinstall', 'back', + 'exit' +] + +const nodeCommandsHelp = [ + '[status] - Retrieving Auto Provisioning node status returns the node status information for each task that has been executed for the specified node.', + '[properties] - Retrieving Auto Provisioning node properties returns the node properties for each task that has been executed for the specified node.', + '[delete] - Delete an Auto Provisioning node removes the Auto Provisioning data for a Network Element. If a node is the last node in a project and there are no profiles associated with the project the project will automatically be deleted.', + '[bind] - Binding a hardware serial number to a node configuration associates the specified node configurations with a hardware serial number for Zero Touch Integration or Hardware Replace.', + '[cancel] - Cancelling the auto provisioning activity rolls back an AutoProvisoning workflow for Node Integration. For expansion a node is rolled back to it\'s original configuration if additional configurations have been applied to the node.', + '[resume] - Resuming the auto provisioning activity recommences an Auto Provisioning workflow that is suspended.', + '[configurations] - Uploading an auto provisioning configuration replaces a configuration file that was part of the initial Auto Provisioning node configuration.', + '[siteinstall] - Download Site Installation File (SIF) that is required to be taken on site for LMT Integration or LMT Hardware Replace.', + '[back] - Return to project\'s nodes.', + '[exit] - Exit this app.', +] + + +async function getNode() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + this.commands = nodeCommands + this.help = nodeCommandsHelp.join('\n ') + this.choices = [] // this.nodes.filter(item => item !== nodeId) + this.prompt = `${projectId} (${nodeId}) ` +} + + +async function getNodeStatus() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const axiosConfig = { + text: `Getting ${nodeId}'s status...`, + method: 'get', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}` + } + const { data: { statusEntries } } = await this.httpClient.request(axiosConfig) + logNodeStatus(statusEntries) +} + + +async function getNodeProperties() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const axiosConfig = { + text: `Getting ${nodeId}'s properties...`, + method: 'get', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}?filter=properties` + } + const { data: { attributes, attributeGroups } } = await this.httpClient.request(axiosConfig) + logNodeProperties(attributes, attributeGroups) +} + + +async function bindNode() { + const hardwareId = await inquirer.prompt([ + { + type: 'input', + name: 'value', + suffix: chalk.bgGreen('?'), + message: 'Type hardwareId', + validate: input => isValidHardwareId(input), + } + ]) + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const axiosConfig = { + text: `Binding ${hardwareId} to ${nodeId}...`, + method: 'put', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/bind`, + data: { + hardwareId, + }, + } + const { statusText } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Binding ${statusText}`)) +} + + +async function cancelNode() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const axiosConfig = { + text: `Canceling ${nodeId}...`, + method: 'post', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/cancel`, + } + const { statusText } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Canceling ${statusText}`)) +} + + +async function resumeNode() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const axiosConfig = { + text: `Resuming ${nodeId}...`, + method: 'post', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/resume`, + } + const { statusText } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Resuming ${statusText}`)) +} + + +async function configurationsNode() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const fileNameInput = await inquirer.prompt([{ + type: 'file-tree-selection', + name: 'nodeFile', + message: 'Choose a node file...', + }]) + const formData = new FormData() + formData.append('file', fs.createReadStream(fileNameInput.nodeFile)) + const axiosConfig = { + text: `Uploading ${nodeId} configuration to ${projectId}...`, + method: 'put', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations`, + headers: formData.getHeaders(), + data: formData + } + const { statusText } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Resuming ${statusText}`)) +} + + +async function siteinstallNode() { + const { projectId } = this.projects[this.projectIndex] + const nodeId = this.nodes[this.nodeIndex] + const saveFileName = path.join(process.cwd(), `Site_Install_${nodeId}.xml`) + const axiosConfig = { + text: `Downloading site install file ${nodeId}...`, + method: 'get', + url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations/siteinstall`, + } + const { data } = await this.httpClient.request(axiosConfig) + await fsPromises.writeFile(saveFileName, data) + console.log(chalk.bgGreen(`Download site install file to ${saveFileName}`)) +} + + +module.exports = { + getNode, + getNodeStatus, + getNodeProperties, + bindNode, + cancelNode, + resumeNode, + configurationsNode, + siteinstallNode } \ No newline at end of file diff --git a/lib/components/AutoProvisioning/projects.js b/lib/applications/AutoProvisioning/projects.js old mode 100644 new mode 100755 similarity index 95% rename from lib/components/AutoProvisioning/projects.js rename to lib/applications/AutoProvisioning/projects.js index 0ead7b8..858f8eb --- a/lib/components/AutoProvisioning/projects.js +++ b/lib/applications/AutoProvisioning/projects.js @@ -1,122 +1,122 @@ -const inquirer = require('inquirer') -const chalk = require('chalk') -const inquirerFileTreeSelection = require('inquirer-file-tree-selection-prompt') -const fs = require('fs') -const FormData = require('form-data') - -const logProject = require('../../../util/logProject') - -inquirer.registerPrompt('file-tree-selection', inquirerFileTreeSelection) - -const projectsCommands = ['new', 'exit'] -const projectsCommandsHelp = [ - '[new] - Import an Auto Provisioning project to start Auto Provisioning workflows based on the content of the AutoProvisioning project. The Auto Provisioning project file contains project related data in the projectInfo.xml file and node folders which contain configurations required to execute AutoProvisioning use-cases.', - '[exit] - Exit this app.', - 'Or choose a project from list...', -] -const projectCommands = ['delete', 'back', 'exit'] -const projectCommandsHelp = [ - '[delete] - Delete of an Auto Provisioning project removes an Auto Provisioning project and all the Auto Provisioning data for nodes within that project. This includes removal or rollback of any ongoing Auto Provisioning workflows within that project.', - '[back] - Return to projects.', - '[exit] - Exit this app.', - 'Or choose a node from list...', -] - -async function getProjects() { - const axiosConfig = { - text: 'Getting projects...', - method: 'get', - url: `${this.appUrl}/projects` - } - const response = await this.httpClient.request(axiosConfig) - this.projects = response.data - this.choices = projectsChoices(this.projects) - this.commands = projectsCommands - this.help = projectsCommandsHelp.join('\n ') - this.nodes = null - return `${this.projects.length} projects` -} - - -async function getProjectData() { - const { projectId } = this.projects[this.projectIndex] - const axiosConfig = { - text: `Getting ${projectId}'s status...`, - method: 'get', - url: `${this.appUrl}/projects/${projectId}` - } - const { data: { nodeSummary } } = await this.httpClient.request(axiosConfig) - axiosConfig.text = `Getting ${projectId}'s properties...` - axiosConfig.url += '?filter=properties' - const { data } = await this.httpClient.request(axiosConfig) - logProject(data, nodeSummary) - this.nodes = nodeSummary.map(node => node.id) - this.choices = this.nodes - this.commands = projectCommands - this.help = projectCommandsHelp.join('\n ') - this.nodeIndex = -1 - return projectId -} - - -async function newProject() { - const fileNameInput = await inquirer.prompt([{ - type: 'file-tree-selection', - name: 'projectFile', - message: 'Choose a project file...', - }]) - const formData = new FormData() - formData.append('file', fs.createReadStream(fileNameInput.projectFile)) - const axiosConfig = { - text: `Creating project...`, - method: 'post', - url: `${this.appUrl}/projects`, - headers: formData.getHeaders(), - data: formData - } - const { data: { id } } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Project ${id} created!`)) - await this.getProjectData(id) -} - - -async function deleteProject() { - const { projectId } = this.projects[this.projectIndex] - const axiosConfig = { - text: `Deleting project ${projectId}...`, - method: 'delete', - url: `${this.appUrl}/projects/${projectId}` - } - const { statusText } = await this.httpClient.request(axiosConfig) - console.log(chalk.bgGreen(`Delete ${statusText}`)) - this.choices = projectsChoices(this.projects) -} - - -function projectsChoices(projects) { - return projects.map(project => { - const { - projectId, - numberOfNodes, - integrationPhaseSummary: { - cancelled, - failed, - inProgress, - successful, - suspended, - }, - } = project - const statusList = [ - successful && successful + '✅', - inProgress && inProgress + '⌛', - suspended && suspended + '⌚', - cancelled && cancelled + '❌', - failed && failed + '⛔', - ] - const space = ' '.repeat(Math.max(14, projectId.length + 2) - projectId.length) - return `${chalk.bold(projectId)}${space}(${numberOfNodes}) ${statusList.filter(item => item).join(' ')}` - }) -} - - -module.exports = { getProjects, getProjectData, deleteProject, newProject } +const inquirer = require('inquirer') +const chalk = require('chalk') +const inquirerFileTreeSelection = require('inquirer-file-tree-selection-prompt') +const fs = require('fs') +const FormData = require('form-data') + +const logProject = require('../../../util/logProject') + +inquirer.registerPrompt('file-tree-selection', inquirerFileTreeSelection) + +const projectsCommands = ['new', 'exit'] +const projectsCommandsHelp = [ + '[new] - Import an Auto Provisioning project to start Auto Provisioning workflows based on the content of the AutoProvisioning project. The Auto Provisioning project file contains project related data in the projectInfo.xml file and node folders which contain configurations required to execute AutoProvisioning use-cases.', + '[exit] - Exit this app.', + 'Or choose a project from list...', +] +const projectCommands = ['delete', 'back', 'exit'] +const projectCommandsHelp = [ + '[delete] - Delete of an Auto Provisioning project removes an Auto Provisioning project and all the Auto Provisioning data for nodes within that project. This includes removal or rollback of any ongoing Auto Provisioning workflows within that project.', + '[back] - Return to projects.', + '[exit] - Exit this app.', + 'Or choose a node from list...', +] + +async function getProjects() { + const axiosConfig = { + text: 'Getting projects...', + method: 'get', + url: `${this.appUrl}/projects` + } + const response = await this.httpClient.request(axiosConfig) + this.projects = response.data + this.choices = projectsChoices(this.projects) + this.commands = projectsCommands + this.help = projectsCommandsHelp.join('\n ') + this.nodes = null + this.prompt = `${this.projects.length} projects` +} + + +async function getProjectData() { + const { projectId } = this.projects[this.projectIndex] + const axiosConfig = { + text: `Getting ${projectId}'s status...`, + method: 'get', + url: `${this.appUrl}/projects/${projectId}` + } + const { data: { nodeSummary } } = await this.httpClient.request(axiosConfig) + axiosConfig.text = `Getting ${projectId}'s properties...` + axiosConfig.url += '?filter=properties' + const { data } = await this.httpClient.request(axiosConfig) + logProject(data, nodeSummary) + this.nodes = nodeSummary.map(node => node.id) + this.choices = this.nodes + this.commands = projectCommands + this.help = projectCommandsHelp.join('\n ') + this.nodeIndex = -1 + this.prompt = projectId +} + + +async function newProject() { + const fileNameInput = await inquirer.prompt([{ + type: 'file-tree-selection', + name: 'projectFile', + message: 'Choose a project file...', + }]) + const formData = new FormData() + formData.append('file', fs.createReadStream(fileNameInput.projectFile)) + const axiosConfig = { + text: `Creating project...`, + method: 'post', + url: `${this.appUrl}/projects`, + headers: formData.getHeaders(), + data: formData + } + const { data: { id } } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Project ${id} created!`)) + await this.getProjectData(id) +} + + +async function deleteProject() { + const { projectId } = this.projects[this.projectIndex] + const axiosConfig = { + text: `Deleting project ${projectId}...`, + method: 'delete', + url: `${this.appUrl}/projects/${projectId}` + } + const { statusText } = await this.httpClient.request(axiosConfig) + console.log(chalk.bgGreen(`Delete ${statusText}`)) + this.choices = projectsChoices(this.projects) +} + + +function projectsChoices(projects) { + return projects.map(project => { + const { + projectId, + numberOfNodes, + integrationPhaseSummary: { + cancelled, + failed, + inProgress, + successful, + suspended, + }, + } = project + const statusList = [ + successful && successful + '✅', + inProgress && inProgress + '⌛', + suspended && suspended + '⌚', + cancelled && cancelled + '❌', + failed && failed + '⛔', + ] + const space = ' '.repeat(Math.max(14, projectId.length + 2) - projectId.length) + return `${chalk.bold(projectId)}${space}(${numberOfNodes}) ${statusList.filter(item => item).join(' ')}` + }) +} + + +module.exports = { getProjects, getProjectData, deleteProject, newProject } diff --git a/lib/applications/TopologyBrowser/TopologyBrowser.js b/lib/applications/TopologyBrowser/TopologyBrowser.js new file mode 100755 index 0000000..0087dec --- /dev/null +++ b/lib/applications/TopologyBrowser/TopologyBrowser.js @@ -0,0 +1,154 @@ +const logAttributes = require('../../../util/logAttributes') +const inputHandler = require('./inputHandler') +const alarms = require('./commands/alarms') +const sync = require('./commands/sync') +const initialPrompt = require('./commands/initialPrompt') +const nextObjects = require('./commands/nextObjects') +const nextAttributes = require('./commands/nextAttributes') +const setIdByCommand = require('./commands/setIdByCommand') +const show = require('./commands/show') +const up = require('./commands/up') +const config = require('./commands/config') +const end = require('./commands/end') +const setAttribute = require('./commands/setAttribute') +const description = require('./commands/description') +const get = require('./commands/get') +const set = require('./commands/set') +const commit = require('./commands/commit') +const home = require('./commands/home') +const search = require('./commands/search') +const goToFdn = require('./commands/goToFdn') +const createNext = require('../../../util/createNext') +const { mainHelp, configHelp } = require('./help') + +const ENM = require('../../components/ENM') +const chalk = require('chalk') + +class TopologyBrowser extends ENM { + constructor(username, password, url) { + super(username, password, url) + this.objectUrl = '/persistentObject/' + this.alarmUrl = '/alarmcontroldisplayservice/alarmMonitoring/alarmoperations/' + this.nodeTypesUrl = '/modelInfo/model/nodeTypes/' + this.nodeSearchUrl = '/managedObjects/search/v2' + this.nodePoIdsUrl = '/managedObjects/getPosByPoIds' + + this.currentPoId = 0 + this.nextPoId = 1 + this.fdn = '' + this.childrens = [] + this.poIds = [] + this.isConfig = false + this.attributes = [] + this.nextVariants = null + this.attributesData = [] + this.attribute = null + this.networkDetails = null + this.configSet = [] + this.includeNonPersistent = false + this.configCommands = ['commit', 'check', 'end', 'exit'] + this.help = mainHelp + } + + async initialPrompt() { + await initialPrompt.call(this) + } + + async next(input) { + await this.nextVariants(input) + return createNext.call(this, input ? input : '') + } + + async nextObjects(input) { + return await nextObjects.call(this, input) + } + + async nextAttributes(input) { + return await nextAttributes.call(this, input) + } + + setIdByCommand(command) { + return setIdByCommand.call(this, command) + } + + async show(filter) { + await show.call(this, filter) + } + + up() { + return up.call(this) + } + + async config(fdn) { + this.help = configHelp + return await config.call(this, fdn) + } + + end() { + this.help = mainHelp + end.call(this) + } + + setAttribute(attribute) { + return setAttribute.call(this, attribute) + } + + description() { + description.call(this) + } + + get(fdn) { + get.call(this, fdn) + } + + async set() { + await set.call(this) + } + + async commit(fdn) { + return await commit.call(this, fdn) + } + + check() { + logAttributes(this.fdn.replace(/\((\w+)\)/g, ''), this.configSet) + } + + home() { + home.call(this) + } + + async search() { + await search.call(this) + } + + async goToFdn(fromFdn, targetFdn) { + return await goToFdn.call(this, fromFdn, targetFdn) + } + + persistent() { + this.includeNonPersistent = !this.includeNonPersistent + console.log(chalk.yellow(`Include Non Persistent Atrributes Set to: ${this.includeNonPersistent}`)) + } + + async alarms(fdn) { + await alarms.call(this, fdn) + } + + async sync(fdn) { + await sync.call(this, fdn) + } + + async inputHandler() { + await inputHandler.call(this) + } + + getPrompt() { + if (this.fdn.length >= 67) { + return `...${this.fdn.slice(-65)}` + } + return this.fdn + } +} + + +module.exports = TopologyBrowser \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/alarms.js b/lib/applications/TopologyBrowser/commands/alarms.js old mode 100644 new mode 100755 similarity index 66% rename from lib/components/TopologyBrowser/commands/alarms.js rename to lib/applications/TopologyBrowser/commands/alarms.js index 2e72b2f..89c7599 --- a/lib/components/TopologyBrowser/commands/alarms.js +++ b/lib/applications/TopologyBrowser/commands/alarms.js @@ -1,127 +1,127 @@ -const colors = require('colors') -const inquirer = require('inquirer') - -const requestWrapper = require('../../util/requestWrapper') -const logAlarm = require('../../util/logAlarm') -const eventTimeToString = require('../../util/eventTimeToString') - -const closeAlarms = { name: 'Close Alarms'.yellow, value: -1} - - -function logTotal(total) { - if (total === 0) { - console.log(`Total Alarms: ${total}`.green) - return - } - console.log(`Total Alarms: ${total}`.yellow) -} - - -function parsePoIdResponse(response) { - let total - let eventPoIds - response.data.forEach(item => { - if (item.eventPoIds) { - eventPoIds = item.eventPoIds - return - } - if (typeof item.total === 'number') { - total = item.total - return - } - }) - return [total, eventPoIds] -} - - -async function getPoIds(url, nodes) { - const axiosConfig = { - method: 'post', - url, - data: { - filters: '', - category: 'All', - nodes, - recordLimit: 5000, - tableSettings: 'fdn#true#false,alarmingObject#true#false,presentSeverity#true#false,eventTime#true#false,insertTime#true#false,specificProblem#true#false', - timestamp: + new Date(), - sortCriteria: [ - { - attribute: 'insertTime', - mode: 'desc' - } - ], - advFilters: '' - } - } - const response = await requestWrapper(axiosConfig, 'Getting Alarms...') - if (!Array.isArray(response.data)) return - const [total, eventPoIds] = parsePoIdResponse(response) - logTotal(total) - if (eventPoIds) return eventPoIds.toString() -} - - -async function getFields(url, eventPoIds) { - const axiosConfig = { - method: 'post', - url, - data: { - eventPoIds, - tableSettings: '', - timestamp: + new Date(), - filters: '', - category: 'All' - } - } - const response = await requestWrapper(axiosConfig, 'Getting Alarms Data...') - return response.data -} - - -async function alarmChoices(alarmList, input) { - const filter = input ? input : '' - return alarmList - .map(al => { - const { eventPoIdAsLong, alarmingObject, presentSeverity, eventTime, specificProblem } = al - return { - name: `${presentSeverity}\t${eventTimeToString(eventTime)}\t${alarmingObject}\t${specificProblem}`, - value: eventPoIdAsLong - } - }) - .filter(al => al.name.toLowerCase().includes(filter.toLowerCase())) - .concat(closeAlarms) -} - - -async function alarmsLoop(alarmList) { - while (true) { - const input = await inquirer.prompt([ - { - type: 'autocomplete', - name: 'alarm', - message: 'Select Alarm:', - pageSize: 10, - source: async (answers, input) => await alarmChoices(alarmList, input) - } - ]) - if (input.alarm === closeAlarms.value) break - logAlarm(alarmList, input.alarm) - } -} - - -async function alarms(fdn) { - const meContextFind = fdn.match(/(NetworkElement|MeContext)=([\w-]+),?/) - if (!meContextFind) { - console.log('No alarming object in FDN!'.yellow) - return - } - const eventPoIds = await getPoIds(`${this.alarmUrl}eventpoids`, meContextFind[2]) - if (!eventPoIds) return - const alarmList = await getFields(`${this.alarmUrl}getalarmlist/fields`, eventPoIds) - await alarmsLoop(alarmList) -} - - +const chalk = require('chalk') +const inquirer = require('inquirer') + +const logAlarm = require('../../../../util/logAlarm') +const eventTimeToString = require('../../../../util/eventTimeToString') + +const closeAlarms = { name: chalk.yellow('Close Alarms'), value: -1} + + +function logTotal(total) { + if (total === 0) { + console.log(chalk.green(`Total Alarms: ${total}`)) + return + } + console.log(chalk.yellow(`Total Alarms: ${total}`)) +} + + +function parsePoIdResponse(response) { + let total + let eventPoIds + response.data.forEach(item => { + if (item.eventPoIds) { + eventPoIds = item.eventPoIds + return + } + if (typeof item.total === 'number') { + total = item.total + return + } + }) + return [total, eventPoIds] +} + + +async function getPoIds(url, nodes) { + const axiosConfig = { + text: 'Getting Alarms...', + method: 'post', + url, + data: { + filters: '', + category: 'All', + nodes, + recordLimit: 5000, + tableSettings: 'fdn#true#false,alarmingObject#true#false,presentSeverity#true#false,eventTime#true#false,insertTime#true#false,specificProblem#true#false', + timestamp: + new Date(), + sortCriteria: [ + { + attribute: 'insertTime', + mode: 'desc' + } + ], + advFilters: '' + } + } + const response = await this.httpClient.request(axiosConfig) + if (!Array.isArray(response.data)) return + const [total, eventPoIds] = parsePoIdResponse(response) + logTotal(total) + if (eventPoIds) return eventPoIds.toString() +} + + +async function getFields(url, eventPoIds) { + const axiosConfig = { + text: 'Getting Alarms Data...', + method: 'post', + url, + data: { + eventPoIds, + tableSettings: '', + timestamp: + new Date(), + filters: '', + category: 'All' + } + } + const response = await this.httpClient.request(axiosConfig) + return response.data +} + + +function alarmChoices(alarmList, input) { + const filter = input ? input : '' + return alarmList + .map(al => { + const { eventPoIdAsLong, alarmingObject, presentSeverity, eventTime, specificProblem } = al + return { + name: `${presentSeverity}\t${eventTimeToString(eventTime)}\t${alarmingObject}\t${specificProblem}`, + value: eventPoIdAsLong + } + }) + .filter(al => al.name.toLowerCase().includes(filter.toLowerCase())) + .concat(closeAlarms) +} + + +async function alarmsLoop(alarmList) { + while (true) { + const input = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'alarm', + message: 'Select Alarm:', + pageSize: 10, + source: async (answers, input) => alarmChoices(alarmList, input) + } + ]) + if (input.alarm === closeAlarms.value) break + logAlarm(alarmList, input.alarm) + } +} + + +async function alarms() { + const meContextFind = this.fdn.match(/(NetworkElement|MeContext)=([\w-]+),?/) + if (!meContextFind) { + throw new Error('No alarming object in FDN!') + } + const eventPoIds = await getPoIds.call(this, `${this.alarmUrl}eventpoids`, meContextFind[2]) + if (!eventPoIds) return + const alarmList = await getFields.call(this, `${this.alarmUrl}getalarmlist/fields`, eventPoIds) + await alarmsLoop(alarmList) +} + + module.exports = alarms \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/commit.js b/lib/applications/TopologyBrowser/commands/commit.js new file mode 100755 index 0000000..f0e58cf --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/commit.js @@ -0,0 +1,35 @@ +const logAttributes = require('../../../../util/logAttributes') +const logCommit = require('../../../../util/logCommit') + + +async function commit() { + if (!this.configSet.length) { + throw new Error('Configuration is empty❗') + } + this.fdn = this.fdn.replace(/\((\w+)\)/g, '') + logAttributes(this.fdn, this.configSet) + const axiosConfig = { + text: 'Commiting Config...', + method: 'put', + url: `${this.objectUrl}${this.currentPoId}`, + data: { + poId: this.currentPoId, + fdn: this.fdn, + attributes: this.configSet, + }, + headers: { + 'Content-Type': 'application/json' + } + } + const response = await this.httpClient.request(axiosConfig) + if (response.data) { + logCommit(response.data) + } else { + throw new Error('No data or response❗') + } + this.configSet.length = 0 + this.end() +} + + +module.exports = commit \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/config.js b/lib/applications/TopologyBrowser/commands/config.js old mode 100644 new mode 100755 similarity index 64% rename from lib/components/TopologyBrowser/commands/config.js rename to lib/applications/TopologyBrowser/commands/config.js index 8da2107..f29de29 --- a/lib/components/TopologyBrowser/commands/config.js +++ b/lib/applications/TopologyBrowser/commands/config.js @@ -1,39 +1,38 @@ -const requestWrapper = require('../../util/requestWrapper') -const logDetails = require('../../util/logDetails') - - -async function config(fdn) { - this.isConfig = true - const axiosConfig = { - method: 'get', - url: `${this.objectUrl}${this.currentPoId}`, - params: { - includeNonPersistent: this.includeNonPersistent, - stringifyLong: true - } - } - const responseA = await requestWrapper(axiosConfig, 'Reading Attributes...') - if (!responseA.data.attributes) { - console.log('Can\'t read attributes'.red) - return fdn - } - const { attributes, namespace, namespaceVersion, neType, neVersion, networkDetails, type} = responseA.data - axiosConfig.url = `${this.objectUrl}model/${neType}/${neVersion}/${namespace}/${type}/${namespaceVersion}/attributes` - axiosConfig.params = { - includeNonPersistent: this.includeNonPersistent - } - const responseD = await requestWrapper(axiosConfig, 'Reading Attributes Data...') - if (!responseD.data.attributes) { - console.log('Can\'t read attributes data'.red) - return fdn - } - this.networkDetails = networkDetails - logDetails(networkDetails) - this.attributes = attributes - this.nextVariants = async (input) => await this.nextAttributes(input) - this.attributesData = responseD.data.attributes - return `${fdn}(config)` -} - - +const logDetails = require('../../../../util/logDetails') + + +async function config() { + this.isConfig = true + const axiosConfig = { + text: 'Reading Attributes...', + method: 'get', + url: `${this.objectUrl}${this.currentPoId}`, + params: { + includeNonPersistent: this.includeNonPersistent, + stringifyLong: true + } + } + const responseA = await this.httpClient.request(axiosConfig) + if (!responseA.data.attributes) { + throw new Error('Can\'t read attributes') + } + const { attributes, namespace, namespaceVersion, neType, neVersion, networkDetails, type} = responseA.data + axiosConfig.text = 'Reading Attributes Data...' + axiosConfig.url = `${this.objectUrl}model/${neType}/${neVersion}/${namespace}/${type}/${namespaceVersion}/attributes` + axiosConfig.params = { + includeNonPersistent: this.includeNonPersistent + } + const responseD = await this.httpClient.request(axiosConfig) + if (!responseD.data.attributes) { + throw new Error('Can\'t read attributes data') + } + this.networkDetails = networkDetails + logDetails(networkDetails) + this.attributes = attributes + this.nextVariants = async (input) => await this.nextAttributes(input) + this.attributesData = responseD.data.attributes + this.fdn = `${this.fdn}(config)` +} + + module.exports = config \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/description.js b/lib/applications/TopologyBrowser/commands/description.js new file mode 100755 index 0000000..8de1798 --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/description.js @@ -0,0 +1,21 @@ +const chalk = require('chalk') +const logAttributeData = require('../../../../util/logAttributeData') + + +function description() { + const attributeData = this.attributesData.find(item => item.key === this.attribute) + if (attributeData) { + logAttributeData(attributeData) + if (attributeData.complexRef) { + console.log(`${chalk.magenta(attributeData.type)} + ${chalk.cyan(attributeData.complexRef.key)}: ${chalk.grey(attributeData.complexRef.description)} + `) + attributeData.complexRef.attributes.forEach(attr => logAttributeData(attr)) + } + } else { + throw new Error('Attribute Not Found❗') + } +} + + +module.exports = description \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/end.js b/lib/applications/TopologyBrowser/commands/end.js old mode 100644 new mode 100755 similarity index 53% rename from lib/components/TopologyBrowser/commands/end.js rename to lib/applications/TopologyBrowser/commands/end.js index ba075f5..c28cfa0 --- a/lib/components/TopologyBrowser/commands/end.js +++ b/lib/applications/TopologyBrowser/commands/end.js @@ -1,9 +1,11 @@ -function end() { - this.isConfig = false - this.attribute = null - this.configSet.length = 0 - this.nextVariants = async (input) => await this.nextObjects(input) -} - - +function end() { + this.fdn = this.fdn.replace(/\((\w+)\)/g, '') + this.isConfig = false + this.attribute = null + this.configSet.length = 0 + this.configCommands = this.configCommands.filter(cmd => !['get', 'set', 'description'].includes(cmd) ) + this.nextVariants = async (input) => await this.nextObjects(input) +} + + module.exports = end \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/get.js b/lib/applications/TopologyBrowser/commands/get.js new file mode 100755 index 0000000..979676d --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/get.js @@ -0,0 +1,23 @@ +const logAttributes = require('../../../../util/logAttributes') +const banner = require('../../../../util/banner') +const chalk = require('chalk') + + +function get() { + const syncStatus = this.networkDetails.find(item => item.key === 'syncStatus') + if (syncStatus && syncStatus.value === 'UNSYNCHRONIZED') { + console.log(chalk.yellow(` + ❗ ${syncStatus.key}: ${syncStatus.value} ❗`)) + } + const attribute = this.attributes.find(item => item.key === this.attribute) + if (!attribute) { + throw new Error(`Attribute not Found: ${this.attribute}`) + } + const attributeData = this.attributesData.find(item => item.key === this.attribute) + logAttributes(this.fdn, [attribute]) + console.log(` ${chalk.green('Type: ') + attributeData['type']} ${attributeData['defaultValue'] ? chalk.yellow('Default: ') + attributeData['defaultValue'] : ''} + `) + if (attributeData.constraints && attributeData.constraints.orderingConstraint) banner(attributeData) +} + +module.exports = get diff --git a/lib/components/TopologyBrowser/commands/fdn.js b/lib/applications/TopologyBrowser/commands/goToFdn.js old mode 100644 new mode 100755 similarity index 56% rename from lib/components/TopologyBrowser/commands/fdn.js rename to lib/applications/TopologyBrowser/commands/goToFdn.js index cd54082..4408bc9 --- a/lib/components/TopologyBrowser/commands/fdn.js +++ b/lib/applications/TopologyBrowser/commands/goToFdn.js @@ -1,28 +1,29 @@ -const requestWrapper = require('../../util/requestWrapper') - - -async function fdn(fromFdn, targetFdn) { - const axiosConfig = { - method: 'get', - url: `${this.objectUrl}fdn/${targetFdn}`, - } - const response1 = await requestWrapper(axiosConfig, 'Browsing to FDN...') - if (!response1.data.fdn) return fromFdn - this.poIds.length = 0 - const { fdn, poId } = response1.data - this.currentPoId = poId - this.nextPoId = poId - this.nextVariants = async (input) => await this.nextObjects(input) - axiosConfig.url = `${this.objectUrl}network/${this.currentPoId}/subTrees` - const response2 = await requestWrapper(axiosConfig, 'Building FDN path...') - if (response2.data) { - if (response2.data.treeNodes.length > 1) { - response2.data.treeNodes.slice(0, -1).forEach((node) => { - this.poIds.push(node.poId) - }) - } - this.childrens = null - } - return fdn -} -module.exports = fdn +async function goToFdn(targetFdn) { + if (!targetFdn) { + throw new Error('Valid FDN must be supplied❗') + } + const axiosConfig = { + text: 'Browsing to FDN...', + method: 'get', + url: `${this.objectUrl}fdn/${targetFdn}`, + } + const response1 = await this.httpClient.request(axiosConfig) + this.poIds.length = 0 + const { fdn, poId } = response1.data + this.currentPoId = poId + this.nextPoId = poId + this.nextVariants = async (input) => await this.nextObjects(input) + axiosConfig.text = 'Building FDN path...' + axiosConfig.url = `${this.objectUrl}network/${this.currentPoId}/subTrees` + const response2 = await this.httpClient.request(axiosConfig) + if (response2.data) { + if (response2.data.treeNodes.length > 1) { + response2.data.treeNodes.slice(0, -1).forEach((node) => { + this.poIds.push(node.poId) + }) + } + this.childrens = [] + } + this.fdn = fdn +} +module.exports = goToFdn diff --git a/lib/components/TopologyBrowser/commands/home.js b/lib/applications/TopologyBrowser/commands/home.js old mode 100644 new mode 100755 similarity index 78% rename from lib/components/TopologyBrowser/commands/home.js rename to lib/applications/TopologyBrowser/commands/home.js index b7c1683..cdcf81e --- a/lib/components/TopologyBrowser/commands/home.js +++ b/lib/applications/TopologyBrowser/commands/home.js @@ -1,8 +1,9 @@ -function home() { - this.nextPoId = this.poIds.shift() - this.poIds.length = 0 - this.nextVariants = async (input) => await this.nextObjects(input) -} - - -module.exports = home +function home() { + this.fdn = this.fdn.split(',', 1)[0] + this.nextPoId = this.poIds.shift() + this.poIds.length = 0 + this.nextVariants = async (input) => await this.nextObjects(input) +} + + +module.exports = home diff --git a/lib/components/TopologyBrowser/commands/initialPrompt.js b/lib/applications/TopologyBrowser/commands/initialPrompt.js old mode 100644 new mode 100755 similarity index 60% rename from lib/components/TopologyBrowser/commands/initialPrompt.js rename to lib/applications/TopologyBrowser/commands/initialPrompt.js index 7abb3dc..af3699e --- a/lib/components/TopologyBrowser/commands/initialPrompt.js +++ b/lib/applications/TopologyBrowser/commands/initialPrompt.js @@ -1,23 +1,22 @@ -const colors = require('colors') -const requestWrapper = require('../../util/requestWrapper') - - -async function initialPrompt() { - const axiosConfig = { - method: 'get', - url: `${this.objectUrl}network/-1?relativeDepth=0:-2&childDepth=1` - } - const response = await requestWrapper(axiosConfig, 'Starting Topology Browser...') - if (!response.data.treeNodes) { - console.log('Nothing in initial promt!'.red) - return - } - const { moType, moName, poId } = response.data.treeNodes[0] - this.currentPoId = poId - this.nextPoId = poId - this.nextVariants = async (input) => await this.nextObjects(input) - return `${moType}=${moName}` -} - - +const chalk = require("chalk") + + +async function initialPrompt() { + const axiosConfig = { + text: 'Starting Topology Browser...', + method: 'get', + url: `${this.objectUrl}network/-1?relativeDepth=0:-2&childDepth=1` + } + const response = await this.httpClient.request(axiosConfig) + if (!response.data.treeNodes) { + throw new Error('Nothing in initial promt‼') + } + const { moType, moName, poId } = response.data.treeNodes[0] + this.currentPoId = poId + this.nextPoId = poId + this.nextVariants = async (input) => await this.nextObjects(input) + this.fdn = `${moType}=${moName}` +} + + module.exports = initialPrompt \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/nextAttributes.js b/lib/applications/TopologyBrowser/commands/nextAttributes.js new file mode 100755 index 0000000..30ebedc --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/nextAttributes.js @@ -0,0 +1,14 @@ + + +async function nextAttributes(input) { + const filter = input ? input : '' + this.commands = this.configCommands + .filter(item => item.toLowerCase().includes(filter.toLowerCase())) + this.choices = this.attributes + .map(item => item.key) + .filter(item => item.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => a > b ? 1 : -1) +} + + +module.exports = nextAttributes \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/nextObjects.js b/lib/applications/TopologyBrowser/commands/nextObjects.js new file mode 100755 index 0000000..79c396d --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/nextObjects.js @@ -0,0 +1,57 @@ +const chalk = require("chalk") +const logError = require('../../../../util/logError') + +const otherCommands = ['show', 'config', 'up', 'home', 'fdn', 'search', 'persistent', 'alarms', 'sync', 'exit'] + + +function getSyncStatus(child) { + if (child.syncStatus === 'SYNCHRONIZED') return '\t\t✅' + if (child.syncStatus === 'UNSYNCHRONIZED') return '\t\t⏳' + return child.syncStatus ? '\t\t❓' : '' +} + + +function getRadioAccessTechnology(child) { + return child.radioAccessTechnology ? ' ' + chalk.bold.cyan(child.radioAccessTechnology.join(' ')) : '' +} + + +async function networkRequest() { + try { + const axiosConfig = { + text: 'Loading network data...', + method: 'get', + url: `${this.objectUrl}network/${this.currentPoId}` + } + const { data: { treeNodes } } = await this.httpClient.request(axiosConfig) + if (treeNodes) { + this.childrens = treeNodes[0].childrens + } + } catch (error) { + logError(error) + } +} + + +async function nextObjects(input){ + const filter = input ? input : '' + if (this.currentPoId !== this.nextPoId || this.childrens.length === 0) { + this.currentPoId = this.nextPoId + this.poIds.push(this.currentPoId) + await networkRequest.call(this) + } + this.commands = otherCommands.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) + this.choices = this.childrens + .map(child => { + const st = getSyncStatus(child) + const rt = getRadioAccessTechnology(child) + const ne = child.neType ? ' ' + chalk.dim.gray(child.neType) : '' + return `${child.moType}=${child.moName}${st}${rt}${ne}` + }) + .filter(child => child.toLowerCase().includes(filter.toLowerCase())) + .concat(filter.startsWith('show') ? [filter] : []) + .concat(filter.startsWith('fdn') ? [filter] : []) +} + + +module.exports = nextObjects \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/search.js b/lib/applications/TopologyBrowser/commands/search.js new file mode 100755 index 0000000..75462fd --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/search.js @@ -0,0 +1,127 @@ +const chalk = require('chalk') +const inquirer = require('inquirer') +const { isValidNodeName } = require('../../../../util/validation') + + +async function getNodeTypes() { + const axiosConfig = { + text: 'Getting Node Types...', + method: 'get', + url: this.nodeTypesUrl, + } + const response = await this.httpClient.request(axiosConfig) + return response.data +} + + +async function getPoIds(query) { + const axiosConfig = { + text: 'Searching Nodes...', + method: 'get', + url: this.nodeSearchUrl, + headers: { + 'X-Netex-Scoping-Panel': true, + }, + params: { + query, + orderby: 'moName', + orderdirection: 'asc', + } + } + const { data: { objects } } = await this.httpClient.request(axiosConfig) + return objects.map(item => item.id) +} + + +async function getNodesData(poList) { + const axiosConfig = { + text: 'Getting Nodes Data...', + method: 'post', + url: this.nodePoIdsUrl, + data: { + poList, + defaultMappings: ["syncStatus","managementState","radioAccessTechnology","parentNeType"], + attributeMappings:[{"moType":"MeContext","attributeNames":["neType"]}] + } + } + const response = await this.httpClient.request(axiosConfig) + return response.data +} + + +function resultOutput(result) { + console.log('') + result.forEach(item => { + const { + fdn, + attributes, + cmSyncStatus, + radioAccessTechnology, + } = item + console.log(`${cmSyncStatus === 'SYNCHRONIZED' ? '✅' : '⏳'} ${fdn} ${chalk.dim.grey(attributes.neType)} ${chalk.bold.cyan(radioAccessTechnology.join(' '))}`) + }) + console.log('') +} + + +function filterTypes(input, nodeTypes){ + const filter = input ? input : '' + return nodeTypes.filter( + item => item + .toLowerCase() + .includes(filter.toLowerCase()) + ) +} + + +function buildQuery(nodeName, nodeType, rat) { + const ratFilter = rat.length !== 0 + ? ` filter by radioAccessTechnology ${rat.length === 1 ? 'containing ' : 'contains any of '}${rat.join(',')}` + : '' + const typeFilter = nodeType === 'ALL' + ? `with name = ${nodeName}` + : `of type ${nodeType} where name = ${nodeName}` + return `select all nodes ${nodeName && typeFilter}${ratFilter}` +} + + +async function search() { + const nodeTypes = await getNodeTypes.call(this) + nodeTypes.push('ALL') + const { nodeName, nodeType, rat } = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'nodeType', + message: 'Select Node Type (default ALL):', + pageSize: 10, + default: 'ALL', + source: async (answers, input) => filterTypes(input, nodeTypes) + }, + { + type: 'checkbox', + name: 'rat', + message: 'Select Radio Access Technologies (default all):', + choices: [ + {name: '2G'}, + {name: '3G'}, + {name: '4G'}, + {name: '5G'}, + ] + }, + { + type: 'input', + name: 'nodeName', + message: 'Type a Node Name:', + validate: isValidNodeName + } + ]) + const poList = await getPoIds.call(this, buildQuery(nodeName, nodeType, rat)) + if (poList.length === 0) { + throw new Error('Nodes not found❗') + } + const result = await getNodesData.call(this, poList) + resultOutput(result) +} + + +module.exports = search \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/set.js b/lib/applications/TopologyBrowser/commands/set.js old mode 100644 new mode 100755 similarity index 65% rename from lib/components/TopologyBrowser/commands/set.js rename to lib/applications/TopologyBrowser/commands/set.js index 543ee32..3a6e569 --- a/lib/components/TopologyBrowser/commands/set.js +++ b/lib/applications/TopologyBrowser/commands/set.js @@ -1,28 +1,26 @@ -const colors = require('colors') -const inputByType = require('../inputValue') - - -async function set() { - const attributeData = this.attributesData.filter(item => item.key === this.attribute)[0] - if (!attributeData) return - if (attributeData.writeBehavior === 'NOT_ALLOWED' || attributeData.immutable) { - console.log('Attribute Is ReadOnly'.yellow) - return - } - if (this.isConfig) { - const found = this.configSet.filter(item => item.key === this.attribute)[0] - const { value } = await inputByType(attributeData) - if (found) { - found.value = value - } else { - this.configSet.push( - { - key: this.attribute, - value, - datatype: attributeData.type, - } - ) - } - } -} -module.exports = set +const inputByType = require('../inputValue') + + +async function set() { + const attributeData = this.attributesData.find(item => item.key === this.attribute) + if (!attributeData) return + if (attributeData.writeBehavior === 'NOT_ALLOWED' || attributeData.immutable) { + throw new Error('Attribute Is ReadOnly') + } + if (this.isConfig) { + const found = this.configSet.find(item => item.key === this.attribute) + const { value } = await inputByType(attributeData) + if (found) { + found.value = value + } else { + this.configSet.push( + { + key: this.attribute, + value, + datatype: attributeData.type, + } + ) + } + } +} +module.exports = set diff --git a/lib/applications/TopologyBrowser/commands/setAttribute.js b/lib/applications/TopologyBrowser/commands/setAttribute.js new file mode 100755 index 0000000..8575ef8 --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/setAttribute.js @@ -0,0 +1,12 @@ +function setAttribute(attribute) { + if (!this.attributes) return false + if (!this.attributes.find(item => item.key === attribute)) return false + if (!this.attribute) { + ['get', 'set', 'description'].forEach(cmd => this.configCommands.push(cmd)) + } + this.attribute = attribute + return true +} + + +module.exports = setAttribute \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/setIdByCommand.js b/lib/applications/TopologyBrowser/commands/setIdByCommand.js old mode 100644 new mode 100755 similarity index 60% rename from lib/components/TopologyBrowser/commands/setIdByCommand.js rename to lib/applications/TopologyBrowser/commands/setIdByCommand.js index 902837d..e40e554 --- a/lib/components/TopologyBrowser/commands/setIdByCommand.js +++ b/lib/applications/TopologyBrowser/commands/setIdByCommand.js @@ -1,11 +1,11 @@ -function setIdByCommand(command) { - const nextChild = this.childrens.filter(child => `${child.moType}=${child.moName}` === command)[0] - if (nextChild) { - this.nextPoId = nextChild.poId - return true - } - return false -} - - +function setIdByCommand(command) { + const nextChild = this.childrens.find(child => `${child.moType}=${child.moName}` === command) + if (nextChild) { + this.nextPoId = nextChild.poId + return true + } + return false +} + + module.exports = setIdByCommand \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/show.js b/lib/applications/TopologyBrowser/commands/show.js old mode 100644 new mode 100755 similarity index 69% rename from lib/components/TopologyBrowser/commands/show.js rename to lib/applications/TopologyBrowser/commands/show.js index b98a318..453b5a8 --- a/lib/components/TopologyBrowser/commands/show.js +++ b/lib/applications/TopologyBrowser/commands/show.js @@ -1,20 +1,20 @@ -const requestWrapper = require('../../util/requestWrapper') -const logAttributes = require('../../util/logAttributes') - - -async function show(filter) { - const axiosConfig = { - method: 'get', - url: `${this.objectUrl}${this.currentPoId}`, - params: { - includeNonPersistent: this.includeNonPersistent, - stringifyLong: true - } - } - const response = await requestWrapper(axiosConfig) - if (!response.data.fdn || !response.data.attributes) return - logAttributes(response.data.fdn, response.data.attributes.filter(item => item.key.match(filter))) -} - - +const logAttributes = require('../../../../util/logAttributes') + + +async function show(filter) { + const axiosConfig = { + text: 'Getting Atrributes...', + method: 'get', + url: `${this.objectUrl}${this.currentPoId}`, + params: { + includeNonPersistent: this.includeNonPersistent, + stringifyLong: true + } + } + const response = await this.httpClient.request(axiosConfig) + if (!response.data.fdn || !response.data.attributes) return + logAttributes(response.data.fdn, response.data.attributes.filter(item => item.key.match(filter))) +} + + module.exports = show \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/commands/sync.js b/lib/applications/TopologyBrowser/commands/sync.js new file mode 100755 index 0000000..cc94bf0 --- /dev/null +++ b/lib/applications/TopologyBrowser/commands/sync.js @@ -0,0 +1,26 @@ +const chalk = require("chalk") + + +async function sync() { + const meContextFind = this.fdn.match(/(NetworkElement|MeContext)=([\w-]+),?/) + if (!meContextFind) { + throw new Error('No sync object in FDN!') + } + const actionUrl = `${this.objectUrl}v1/perform-mo-action/NetworkElement=${meContextFind[2]},CmFunction=1?actionName=sync` + const axiosConfig = { + text: 'Initiate Node Sync...', + method: 'post', + url: actionUrl, + headers: { + 'Content-Type': 'application/json' + }, + } + const response = await this.httpClient.request(axiosConfig) + console.log(` + ${chalk.bold(response.data.body)} + ${chalk.green(response.data.title)} + `) +} + + +module.exports = sync \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/up.js b/lib/applications/TopologyBrowser/commands/up.js old mode 100644 new mode 100755 similarity index 60% rename from lib/components/TopologyBrowser/commands/up.js rename to lib/applications/TopologyBrowser/commands/up.js index 1334dff..510a662 --- a/lib/components/TopologyBrowser/commands/up.js +++ b/lib/applications/TopologyBrowser/commands/up.js @@ -1,12 +1,13 @@ -function up() { - if (this.poIds.length > 1) { - this.poIds.pop() - this.nextPoId = this.poIds.pop() - this.currentPoId = this.poIds[this.poIds.length - 1] - return true - } - return false -} - - -module.exports = up +function up() { + if (this.poIds.length > 1) { + this.poIds.pop() + this.nextPoId = this.poIds.pop() + this.currentPoId = this.poIds[this.poIds.length - 1] + this.fdn = this.fdn.split(',').slice(0, -1).join(',') + return + } + throw new Error('There\'s no way up!') +} + + +module.exports = up diff --git a/lib/applications/TopologyBrowser/help.js b/lib/applications/TopologyBrowser/help.js new file mode 100755 index 0000000..455c4c9 --- /dev/null +++ b/lib/applications/TopologyBrowser/help.js @@ -0,0 +1,25 @@ +const mainHelp = [ + '', + 'show [] - shows current object\'s attributes filtered with regex', + 'config - enters config mode', + 'up - navigate up one level', + 'fdn [] - navigate to FDN', + 'home - navigate to root folder', + 'alarms - show alarms', + 'sync - initiate node CM synchronization', + 'persistent - toggle persistent attributes inclusion', + 'exit - logout and exit application', +].join('\n') + +const configHelp = [ + '', + 'set - set attribute\'s value', + 'get - get attribute\'s value', + 'commit - commiting changes to the network', + 'check - view configuration changes', + 'end - exit config mode without commiting', + 'exit - logout and exit application', +].join('\n') + + +module.exports = { mainHelp, configHelp } \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/inputHandler.js b/lib/applications/TopologyBrowser/inputHandler.js new file mode 100755 index 0000000..4ea3e0a --- /dev/null +++ b/lib/applications/TopologyBrowser/inputHandler.js @@ -0,0 +1,109 @@ +const inquirer = require('inquirer') +const chalk = require('chalk') + +const logError = require('../../../util/logError') +const { isEmpty } = require('../../../util/validation') + + +inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) + + +function commandOther(tplg, command) { + const spl = command.split(/\s+/).find(item => item) + if (tplg.setIdByCommand(spl)) { + tplg.fdn = `${tplg.fdn},${spl}` + } else if (tplg.setAttribute(command)) { + tplg.fdn = tplg.fdn.replace(/\((\w+)\)/g, `(${command})`) + } else { + throw new Error('Command Unrecognized❗') + } +} + + +async function handleCommand(tplg, command) { + const [cmd, param] = command.split(/\s+/) + const cmdMatch = cmd.match(/\[(\w+)\]/) + const cmdName = cmdMatch ? cmdMatch[1] : cmd + switch (cmdName) { + case 'exit': + tplg.fdn = '' + break + case 'show': + await tplg.show(param ? param.trim() : '') + break + case 'config': + await tplg.config() + break + case 'set': + await tplg.set() + break + case 'commit': + await tplg.commit() + break + case 'up': + tplg.up() + break + case 'get': + tplg.get() + break + case 'check': + tplg.check() + break + case 'end': + tplg.end() + break + case 'home': + tplg.home() + break + case 'search': + await tplg.search() + break + case 'description': + tplg.description() + break + case 'persistent': + tplg.persistent() + break + case 'fdn': + await tplg.goToFdn(param ? param.trim() : '') + break + case 'alarms': + await tplg.alarms() + break + case 'sync': + await tplg.sync() + break + + default: + commandOther(tplg, command) + } +} + + +async function inputHandler() { + await this.initialPrompt() + while (true) { + try { + const input = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'command', + message: chalk.blue(this.getPrompt()), + pageSize: 10, + prefix: '', + suffix: this.isConfig ? chalk.blue('#') : chalk.blue('>'), + validate: isEmpty, + source: async (answers, input) => await this.next(input), + emptyText: this.help, + } + ]) + await handleCommand(this, input.command) + if (!this.fdn) break + } catch (error) { + logError(error) + } + } +} + + +module.exports = inputHandler \ No newline at end of file diff --git a/lib/components/TopologyBrowser/inputValue.js b/lib/applications/TopologyBrowser/inputValue.js old mode 100644 new mode 100755 similarity index 77% rename from lib/components/TopologyBrowser/inputValue.js rename to lib/applications/TopologyBrowser/inputValue.js index d53ce34..1898c40 --- a/lib/components/TopologyBrowser/inputValue.js +++ b/lib/applications/TopologyBrowser/inputValue.js @@ -1,156 +1,157 @@ -const colors = require('colors') -const inquirer = require('inquirer') -const banner = require('../util/banner') -const { isValidNumber, isValidString, checkValueRangeConstraints } = require('../util/validation') - - -async function inputInteger(attributeData) { - const message = `${attributeData.key.yellow} { ${attributeData.unit ? attributeData.unit : 'parrots'.gray} } (${attributeData.type}): ` - const input = await inquirer.prompt([ - { - type: 'input', - name: 'value', - suffix: '?'.green, - message, - default: attributeData.defaultValue, - validate: input => isValidNumber(input, attributeData.constraints), - } - ]) - return +input.value -} - - -async function inputEnumRef(attributeData) { - const message = `Select Value For ${attributeData.key.yellow}: ` - const input = await inquirer.prompt([ - { - type: 'list', - name: 'value', - suffix: '?'.green, - message, - choices: attributeData.enumeration.enumMembers.map(item => ({ name: `${item.key} (${item.description})`, value: item.key, short: item.key })), - default: attributeData.defaultValue, - } - ]) - return input.value -} - - -async function inputBoolean(attributeData) { - const variants = ['true', 'false'] - if (attributeData.constraints.nullable) variants.push('null') - const message = `Select Value For ${attributeData.key.yellow}:` - const input = await inquirer.prompt([ - { - type: 'list', - name: 'value', - suffix: '?'.green, - message, - choices: variants, - default: String(attributeData.defaultValue), - } - ]) - return { - true: true, - false: false, - null: null - }[input.value] -} - - -async function inputString(attributeData) { - const message = `${attributeData.key.yellow} (${attributeData.type}): ` - const input = await inquirer.prompt([ - { - type: 'input', - name: 'value', - suffix: '?'.green, - message, - default: attributeData.defaultValue, - validate: input => isValidString(input, attributeData.constraints), - } - ]) - return input.value -} - - -async function inputList(attributeData) { - const message = `${attributeData.key.yellow} List Of (${attributeData.listReference.type}) Size: ` - const result = [] - const { value } = await inquirer.prompt([ - { - type: 'number', - name: 'value', - suffix: '?'.green, - message, - default: 'null', - validate: input => +input > 0 || input === 'null', - } - ]) - if (value === 'null') return null - const checkResult = checkValueRangeConstraints(value, attributeData.constraints) - if (checkResult) return checkResult - if (attributeData.constraints.orderingConstraint) banner(attributeData) - await inputListValues(attributeData, value, result) - return result.value ? result.value : result -} - - -async function inputListValues(attributeData, listSize, result) { - for (let i = 0; i < listSize; i++) { - let input - if (!attributeData.listReference.key) { - attributeData.listReference.key = `${attributeData.key} [${i}]` - input = await inputByType(attributeData.listReference) - attributeData.listReference.key = null - } else { - input = await inputByType(attributeData.listReference) - } - if (attributeData.constraints.uniqueMembers) { - if (result.indexOf(input.value) !== -1) { - console.log('>>Array Values Should Be Unique'.red) - i-- - continue - } - } - result.push(input.value) - } -} - - -async function inputComplexRef(attributeData) { - const result = [] - const attributes = attributeData.complexRef.attributes.slice() - while (attributes.length > 0) { - const item = attributes.shift() - const { value } = await inputByType(item) - result.push({ - key: item.key, - value, - datatype: item.type - }) - } - return result -} - - -async function inputByType(typeReference) { - const inputs = { - INTEGER: inputInteger, - SHORT: inputInteger, - ENUM_REF: inputEnumRef, - BOOLEAN: inputBoolean, - STRING: inputString, - MO_REF: inputString, - LIST: inputList, - COMPLEX_REF: inputComplexRef - } - if (inputs[typeReference.type]) { - const input = await inputs[typeReference.type](typeReference) - return { value: input } - } - banner(typeReference) -} - - -module.exports = inputByType +const chalk = require('chalk') +const inquirer = require('inquirer') +const banner = require('../../../util/banner') +const { isValidNumber, isValidString, checkValueRangeConstraints } = require('../../../util/validation') + + +async function inputInteger(attributeData) { + const message = `${chalk.yellow(attributeData.key)} { ${attributeData.unit ? attributeData.unit : chalk.gray('parrots')} } (${attributeData.type}): ` + const input = await inquirer.prompt([ + { + type: 'input', + name: 'value', + suffix: chalk.green('?'), + message, + default: attributeData.defaultValue, + validate: input => isValidNumber(input, attributeData.constraints), + } + ]) + return +input.value +} + + +async function inputEnumRef(attributeData) { + const message = `Select Value For ${chalk.yellow(attributeData.key)}: ` + const input = await inquirer.prompt([ + { + type: 'list', + name: 'value', + suffix: chalk.green('?'), + message, + choices: attributeData.enumeration.enumMembers.map(item => ({ name: `${item.key} (${item.description})`, value: item.key, short: item.key })), + default: attributeData.defaultValue, + } + ]) + return input.value +} + + +async function inputBoolean(attributeData) { + const variants = ['true', 'false'] + if (attributeData.constraints.nullable) variants.push('null') + const message = `Select Value For ${chalk.yellow(attributeData.key)}:` + const input = await inquirer.prompt([ + { + type: 'list', + name: 'value', + suffix: chalk.green('?'), + message, + choices: variants, + default: String(attributeData.defaultValue), + } + ]) + return { + true: true, + false: false, + null: null + }[input.value] +} + + +async function inputString(attributeData) { + const message = `${chalk.yellow(attributeData.key)} (${attributeData.type}): ` + const input = await inquirer.prompt([ + { + type: 'input', + name: 'value', + suffix: chalk.green('?'), + message, + default: attributeData.defaultValue, + validate: input => isValidString(input, attributeData.constraints), + } + ]) + return input.value +} + + +async function inputList(attributeData) { + const message = `${chalk.yellow(attributeData.key)} List Of (${attributeData.listReference.type}) Size: ` + const result = [] + const { value } = await inquirer.prompt([ + { + type: 'number', + name: 'value', + suffix: chalk.green('?'), + message, + default: 'null', + validate: input => +input > 0 || input === 'null', + } + ]) + if (value === 'null') return null + const checkResult = checkValueRangeConstraints(value, attributeData.constraints) + if (checkResult) return checkResult + if (attributeData.constraints.orderingConstraint) banner(attributeData) + await inputListValues(attributeData, value, result) + return result.value ? result.value : result +} + + +async function inputListValues(attributeData, listSize, result) { + for (let i = 0; i < listSize; i++) { + let input + if (!attributeData.listReference.key) { + attributeData.listReference.key = `${attributeData.key} [${i}]` + input = await inputByType(attributeData.listReference) + attributeData.listReference.key = null + } else { + input = await inputByType(attributeData.listReference) + } + if (attributeData.constraints.uniqueMembers) { + if (result.indexOf(input.value) !== -1) { + console.log(chalk.red('>>Array Values Should Be Unique')) + i-- + continue + } + } + result.push(input.value) + } +} + + +async function inputComplexRef(attributeData) { + const result = [] + const attributes = attributeData.complexRef.attributes.slice() + while (attributes.length > 0) { + const item = attributes.shift() + const { value } = await inputByType(item) + result.push({ + key: item.key, + value, + datatype: item.type + }) + } + return result +} + + +async function inputByType(typeReference) { + const inputs = { + INTEGER: inputInteger, + SHORT: inputInteger, + LONG: inputInteger, + ENUM_REF: inputEnumRef, + BOOLEAN: inputBoolean, + STRING: inputString, + MO_REF: inputString, + LIST: inputList, + COMPLEX_REF: inputComplexRef + } + if (inputs[typeReference.type]) { + const input = await inputs[typeReference.type](typeReference) + return { value: input } + } + banner(typeReference) +} + + +module.exports = inputByType diff --git a/lib/components/AxiosHttpClient/AxiosHttpClient.js b/lib/components/AxiosHttpClient.js old mode 100644 new mode 100755 similarity index 80% rename from lib/components/AxiosHttpClient/AxiosHttpClient.js rename to lib/components/AxiosHttpClient.js index 36d64e5..e248fff --- a/lib/components/AxiosHttpClient/AxiosHttpClient.js +++ b/lib/components/AxiosHttpClient.js @@ -1,48 +1,52 @@ -const axios = require('axios') -const chalk = require('chalk') -const https = require('https') -const axiosCookieJarSupport = require('axios-cookiejar-support').default -const tough = require('tough-cookie') -const SpinnerWithCounter = require('../../../util/SpinnerWithCounter') - - -axiosCookieJarSupport(axios) - -let spinner = new SpinnerWithCounter() - - -function beforeRequest(config) { - const {text = 'Executing request...', ...newConfig} = config - spinner.start(text) - return newConfig -} - - -function errorRequest(error) { - spinner.fail() - return Promise.reject(error) -} - - -function beforeResponse(response) { - spinner.succeed() - return response -} - - -function axiosHttpClient(url) { - const axiosClient = axios.create({ - baseURL: url, - httpsAgent: new https.Agent({ - rejectUnauthorized: false - }), - withCredentials: true, - jar: new tough.CookieJar(), - }) - axiosClient.interceptors.request.use(beforeRequest, errorRequest) - axiosClient.interceptors.response.use(beforeResponse, errorRequest) - return axiosClient -} - - +const axios = require('axios') +const https = require('https') +const axiosCookieJarSupport = require('axios-cookiejar-support').default +const tough = require('tough-cookie') +const logError = require('../../util/logError') +const SpinnerWithCounter = require('./SpinnerWithCounter') + + +axiosCookieJarSupport(axios) + +let spinner = new SpinnerWithCounter() + + +function beforeRequest(config) { + const {text = 'Executing request...', ...newConfig} = config + spinner.start(text) + return newConfig +} + + +function errorRequest(error) { + spinner.fail() + if (401 === error.response.status) { + logError(error) + process.exit(1) + } + return Promise.reject(error) +} + + +function beforeResponse(response) { + spinner.succeed() + return response +} + + +function axiosHttpClient(url) { + const axiosClient = axios.create({ + baseURL: url, + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }), + withCredentials: true, + jar: new tough.CookieJar(), + }) + axiosClient.interceptors.request.use(beforeRequest, errorRequest) + axiosClient.interceptors.response.use(beforeResponse, errorRequest) + return axiosClient +} + + module.exports = axiosHttpClient \ No newline at end of file diff --git a/lib/components/ENM/ENM.js b/lib/components/ENM.js old mode 100644 new mode 100755 similarity index 82% rename from lib/components/ENM/ENM.js rename to lib/components/ENM.js index 5e63c06..e995025 --- a/lib/components/ENM/ENM.js +++ b/lib/components/ENM.js @@ -1,32 +1,33 @@ -const chalk = require('chalk') -const axiosHttpClient = require('../AxiosHttpClient/AxiosHttpClient') - -class ENM { - constructor(username, password, url) { - this.logoutUrl = '/logout' - this.loginUrl = `/login?IDToken1=${username}&IDToken2=${password}` - this.httpClient = axiosHttpClient(url) - } - - async login() { - const axiosConfig = { - text: 'Login in...', - method: 'post', - url: this.loginUrl - } - const response = await this.httpClient.request(axiosConfig) - return response.data - } - - async logout() { - const axiosConfig = { - text: 'Logout...', - method: 'get', - url: this.logoutUrl - } - await this.httpClient.request(axiosConfig) - } -} - - +const axiosHttpClient = require('./AxiosHttpClient') + +class ENM { + constructor(username, password, url) { + this.logoutUrl = '/logout' + this.loginUrl = `/login?IDToken1=${username}&IDToken2=${password}` + this.commands = null + this.choices = null + this.httpClient = axiosHttpClient(url) + } + + async login() { + const axiosConfig = { + text: 'Login in...', + method: 'post', + url: this.loginUrl + } + const response = await this.httpClient.request(axiosConfig) + return response.data + } + + async logout() { + const axiosConfig = { + text: 'Logout...', + method: 'get', + url: this.logoutUrl + } + await this.httpClient.request(axiosConfig) + } +} + + module.exports = ENM \ No newline at end of file diff --git a/util/SpinnerWithCounter.js b/lib/components/SpinnerWithCounter.js old mode 100644 new mode 100755 similarity index 94% rename from util/SpinnerWithCounter.js rename to lib/components/SpinnerWithCounter.js index 3bda723..c574cec --- a/util/SpinnerWithCounter.js +++ b/lib/components/SpinnerWithCounter.js @@ -1,35 +1,35 @@ -const ora = require('ora') - -class SpinnerWithCounter { - constructor() { - this.spinner = null - this.counter = 0 - } - - start(text) { - if (!this.spinner) { - this.spinner = ora(text) - this.spinner.start() - } - this.counter = ++this.counter - } - - succeed() { - this.counter = --this.counter - if (this.spinner && this.counter === 0) { - this.spinner.succeed() - this.spinner = null - } - } - - fail() { - this.counter = --this.counter - if (this.spinner && this.counter === 0) { - this.spinner.fail() - this.spinner = null - } - } -} - - +const ora = require('ora') + +class SpinnerWithCounter { + constructor() { + this.spinner = null + this.counter = 0 + } + + start(text) { + if (!this.spinner) { + this.spinner = ora(text) + this.spinner.start() + } + this.counter = ++this.counter + } + + succeed() { + this.counter = --this.counter + if (this.spinner && this.counter === 0) { + this.spinner.succeed() + this.spinner = null + } + } + + fail() { + this.counter = --this.counter + if (this.spinner && this.counter === 0) { + this.spinner.fail() + this.spinner = null + } + } +} + + module.exports = SpinnerWithCounter \ No newline at end of file diff --git a/lib/components/TopologyBrowser/TopologyBrowser.js b/lib/components/TopologyBrowser/TopologyBrowser.js deleted file mode 100644 index 8e823e6..0000000 --- a/lib/components/TopologyBrowser/TopologyBrowser.js +++ /dev/null @@ -1,132 +0,0 @@ -const logAttributes = require('../util/logAttributes') -const inputHandler = require('./inputHandler') -const alarms = require('../commands/alarms') -const sync = require('../commands/sync') -const initialPrompt = require('../commands/initialPrompt') -const nextObjects = require('../commands/nextObjects') -const nextAttributes = require('../commands/nextAttributes') -const setIdByCommand = require('../commands/setIdByCommand') -const show = require('../commands/show') -const up = require('../commands/up') -const config = require('../commands/config') -const end = require('../commands/end') -const setAttribute = require('../commands/setAttribute') -const description = require('../commands/description') -const get = require('../commands/get') -const set = require('../commands/set') -const commit = require('../commands/commit') -const home = require('../commands/home') -const fdn = require('../commands/fdn') - -const ENM = require('../ENM/ENM') - -class TopologyBrowser extends ENM { - constructor(username, password, url) { - super(username, password, url) - this.objectUrl = '/persistentObject/' - this.alarmUrl = '/alarmcontroldisplayservice/alarmMonitoring/alarmoperations/' - - this.currentPoId = 0 - this.nextPoId = 1 - this.childrens = null - this.poIds = [] - this.isConfig = false - this.attributes = null - this.nextVariants = null - this.attributesData = null - this.attribute = null - this.networkDetails = null - this.configSet = [] - this.includeNonPersistent = false - this.configCommands = ['commit', 'check', 'end', 'persistent', 'exit'] - this.help = 'No results...' - } - - async initialPrompt() { - return await initialPrompt.call(this) - } - - async next(input) { - return await this.nextVariants(input) - } - - async nextObjects(input) { - return await nextObjects.call(this, input) - } - - async nextAttributes(input) { - return await nextAttributes.call(this, input) - } - - setIdByCommand(command) { - return setIdByCommand.call(this, command) - } - - async show(filter) { - await show.call(this, filter) - } - - up() { - return up.call(this) - } - - async config(fdn) { - return await config.call(this, fdn) - } - - end() { - end.call(this) - } - - setAttribute(attribute) { - return setAttribute.call(this, attribute) - } - - description() { - description.call(this) - } - - get(fdn) { - get.call(this, fdn) - } - - async set() { - await set.call(this) - } - - async commit(fdn) { - return await commit.call(this, fdn) - } - - check(fdn) { - logAttributes(fdn, this.configSet) - } - - home() { - home.call(this) - } - - async fdn(fromFdn, targetFdn) { - return await fdn.call(this, fromFdn, targetFdn) - } - - persistent() { - this.includeNonPersistent = !this.includeNonPersistent - console.log(`Include Non Persistent Atrributes Set to: ${this.includeNonPersistent}`.yellow) - } - - async alarms(fdn) { - await alarms.call(this, fdn) - } - - async sync(fdn) { - await sync.call(this, fdn) - } - - async inputHandler() { - await inputHandler.call(this) - } -} - - -module.exports = TopologyBrowser \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/commit.js b/lib/components/TopologyBrowser/commands/commit.js deleted file mode 100644 index 27c0b2f..0000000 --- a/lib/components/TopologyBrowser/commands/commit.js +++ /dev/null @@ -1,34 +0,0 @@ -const colors = require('colors') -const logAttributes = require('../../util/logAttributes') -const requestWrapper = require('../../util/requestWrapper') -const logCommit = require('../../util/logCommit') - - -async function commit(fdn) { - logAttributes(fdn, this.configSet) - this.configSet.forEach(item => delete item.from) - const axiosConfig = { - method: 'put', - url: `${this.objectUrl}${this.currentPoId}`, - data: { - poId: this.currentPoId, - fdn, - attributes: this.configSet, - }, - headers: { - 'Content-Type': 'application/json' - } - } - const response = await requestWrapper(axiosConfig, 'Commiting Config...') - if (response.data) { - logCommit(response.data) - } else { - console.log('No data or response!'.red) - } - this.configSet.length = 0 - this.end() - return fdn -} - - -module.exports = commit \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/description.js b/lib/components/TopologyBrowser/commands/description.js deleted file mode 100644 index 018c35f..0000000 --- a/lib/components/TopologyBrowser/commands/description.js +++ /dev/null @@ -1,21 +0,0 @@ -const colors = require('colors') -const logAttributeData = require('../../util/logAttributeData') - - -function description() { - const attributeData = this.attributesData.filter(item => item.key === this.attribute)[0] - if (attributeData) { - logAttributeData(attributeData) - if (attributeData.complexRef) { - console.log(`${attributeData.type.magenta} - ${attributeData.complexRef.key.cyan}: ${attributeData.complexRef.description.grey} - `) - attributeData.complexRef.attributes.forEach(attr => logAttributeData(attr)) - } - } else { - console.log('Attribute Not Found!'.yellow) - } -} - - -module.exports = description \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/get.js b/lib/components/TopologyBrowser/commands/get.js deleted file mode 100644 index b3610b8..0000000 --- a/lib/components/TopologyBrowser/commands/get.js +++ /dev/null @@ -1,20 +0,0 @@ -const colors = require('colors') -const logAttributes = require('../../util/logAttributes') -const banner = require('../../util/banner') - - -function get(fdn) { - const syncStatus = this.networkDetails.filter(item => item.key === 'syncStatus')[0] - if (syncStatus && syncStatus.value === 'UNSYNCHRONIZED') { - console.log(` - ❗ ${syncStatus.key}: ${syncStatus.value} ❗`.yellow) - } - const attribute = this.attributes.filter(item => item.key === this.attribute)[0] - const attributeData = this.attributesData.filter(item => item.key === this.attribute)[0] - logAttributes(fdn, [attribute]) - console.log(` ${'Type: '.green + attributeData['type']} ${attributeData['defaultValue'] ? 'Default: '.yellow + attributeData['defaultValue'] : ''} - `) - if (attributeData.constraints && attributeData.constraints.orderingConstraint) banner(attributeData) -} - -module.exports = get diff --git a/lib/components/TopologyBrowser/commands/nextAttributes.js b/lib/components/TopologyBrowser/commands/nextAttributes.js deleted file mode 100644 index 1b0bbb6..0000000 --- a/lib/components/TopologyBrowser/commands/nextAttributes.js +++ /dev/null @@ -1,14 +0,0 @@ -async function nextAttributes(input) { - const filter = input ? input : '' - let result = this.attributes.map(item => item.key).sort((a, b) => a > b ? 1 : -1) - .concat(this.configCommands) - .filter(item => item.toLowerCase().includes(filter.toLowerCase())) - if (result.includes(filter)) { - result = result.filter(item => item !== filter) - result.unshift(filter) - } - return result -} - - -module.exports = nextAttributes \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/nextObjects.js b/lib/components/TopologyBrowser/commands/nextObjects.js deleted file mode 100644 index 80605b8..0000000 --- a/lib/components/TopologyBrowser/commands/nextObjects.js +++ /dev/null @@ -1,34 +0,0 @@ -const requestWrapper = require('../../util/requestWrapper') - -const otherCommands = ['show', 'config', 'up', 'home', 'fdn', 'persistent', 'alarms', 'sync', 'exit'] - - -async function nextObjects(input){ - const filter = input ? input : '' - if (this.currentPoId !== this.nextPoId || !this.childrens) { - this.currentPoId = this.nextPoId - this.poIds.push(this.currentPoId) - const axiosConfig = { - method: 'get', - url: `${this.objectUrl}network/${this.currentPoId}` - } - const response = await requestWrapper(axiosConfig) - if (response.data.treeNodes) { - this.childrens = response.data.treeNodes[0].childrens - } - } - let result = this.childrens - .map(child => `${child.moType}=${child.moName}`) - .concat(otherCommands) - .filter(child => child.toLowerCase().includes(filter.toLowerCase())) - .concat(filter.startsWith('show') ? [filter] : []) - .concat(filter.startsWith('fdn') ? [filter] : []) - if (result.includes(filter)) { - result = result.filter(item => item !== filter) - result.unshift(filter) - } - return result -} - - -module.exports = nextObjects \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/setAttribute.js b/lib/components/TopologyBrowser/commands/setAttribute.js deleted file mode 100644 index e2c496e..0000000 --- a/lib/components/TopologyBrowser/commands/setAttribute.js +++ /dev/null @@ -1,14 +0,0 @@ -function setAttribute(attribute) { - if (!this.attributes) return false - if (!this.attributes.filter(item => item.key === attribute)[0]) return false - if (!this.attribute) { - this.configCommands.push('get') - this.configCommands.push('set') - this.configCommands.push('description') - } - this.attribute = attribute - return true -} - - -module.exports = setAttribute \ No newline at end of file diff --git a/lib/components/TopologyBrowser/commands/sync.js b/lib/components/TopologyBrowser/commands/sync.js deleted file mode 100644 index 9935762..0000000 --- a/lib/components/TopologyBrowser/commands/sync.js +++ /dev/null @@ -1,30 +0,0 @@ -const colors = require('colors') - -const requestWrapper = require('../../util/requestWrapper') - - -async function sync(fdn) { - const meContextFind = fdn.match(/(NetworkElement|MeContext)=([\w-]+),?/) - if (!meContextFind) { - console.log('No sync object in FDN!'.yellow) - return - } - const actionUrl = `${this.objectUrl}v1/perform-mo-action/NetworkElement=${meContextFind[2]},CmFunction=1?actionName=sync` - const axiosConfig = { - method: 'post', - url: actionUrl, - headers: { - 'Content-Type': 'application/json' - }, - } - const response = await requestWrapper(axiosConfig, 'Initiate Node Sync...') - if (response.status === 200) { - console.log(` - ${response.data.body.bold} - ${response.data.title.green} - `) - } -} - - -module.exports = sync \ No newline at end of file diff --git a/lib/components/TopologyBrowser/inputHandler.js b/lib/components/TopologyBrowser/inputHandler.js deleted file mode 100644 index 849c77f..0000000 --- a/lib/components/TopologyBrowser/inputHandler.js +++ /dev/null @@ -1,136 +0,0 @@ -const inquirer = require('inquirer') -const colors = require('colors') - -const logError = require('../util/logError') -const { isEmpty } = require('../util/validation') - - -inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) - - -function buildPrompt(fdn) { - if (fdn.length >= 67) { - return { prefix: '...', prompt: fdn.slice(-65) } - } - return { prefix: '', prompt: fdn } -} - - -function commndUp(tplg, fdn) { - if (tplg.up()) { - fdn = fdn.split(',').slice(0, -1).join(',') - } else { - console.log('There\'s no way up!'.yellow) - } - return fdn -} - - -function commandOther(tplg, fdn, command) { - if (tplg.setIdByCommand(command)) { - fdn = `${fdn},${command}` - } else if (tplg.setAttribute(command)) { - fdn = fdn.replace(/\((\w+)\)/g, `(${command})`) - } else { - console.log('Command Unrecognized❗'.red) - } - return fdn -} - - -async function handleCommand(tplg, fdn, command) { - const [cmd, param] = command.split(/\s+/) - switch (cmd) { - case 'exit': - return - case 'show': - await tplg.show(param ? param.trim() : '') - break - case 'config': - fdn = await tplg.config(fdn) - break - case 'set': - await tplg.set() - break - case 'commit': - fdn = await tplg.commit(fdn.replace(/\((\w+)\)/g, '')) - break - case 'up': - fdn = commndUp(tplg, fdn) - break - case 'get': - tplg.get(fdn) - break - case 'check': - tplg.check(fdn.replace(/\((\w+)\)/g, '')) - break - case 'end': - tplg.end() - fdn = fdn.replace(/\((\w+)\)/g, '') - break - case 'home': - tplg.home() - fdn = fdn.split(',', 1)[0] - break - case 'description': - tplg.description() - break - case 'persistent': - tplg.persistent() - break - case 'fdn': - fdn = await tplg.fdn(fdn, param ? param.trim() : '') - break - case 'alarms': - await tplg.alarms(fdn) - break - case 'sync': - await tplg.sync(fdn) - break - - default: - fdn = commandOther(tplg, fdn, command) - } - return fdn -} - - -async function inputHandlerLoop(tplg) { - let prompt = await tplg.initialPrompt() - let fdn = prompt - let prefix = '' - while (true) { - try { - const input = await inquirer.prompt([ - { - type: 'autocomplete', - name: 'command', - message: tplg.isConfig ? prompt.blue.underline : prompt.blue, - pageSize: 10, - prefix: prefix.gray, - suffix: tplg.isConfig ? '#'.blue : '>'.blue, - validate: isEmpty, - source: async (answers, input) => await tplg.next(input), - emptyText: prvn.help, - } - ]) - fdn = await handleCommand(tplg, fdn, input.command) - if (!fdn) break - ({ prefix, prompt } = buildPrompt(fdn)) - } catch (error) { - logError(error) - } - } -} - - -async function inputHandler() { - try { - await inputHandlerLoop(this) - } catch (error) { - logError(error) - } -} - - -module.exports = inputHandler \ No newline at end of file diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 670af69..dc5cd98 --- a/package.json +++ b/package.json @@ -1,43 +1,43 @@ -{ - "name": "enm-cli", - "version": "1.0.0", - "description": "This is a cli application for Ericsson Network Manager (ENM)", - "bin": "./bin/enm-cli.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "Ericsson", - "ENM", - "Network", - "Manager", - "LTE", - "3G", - "4G", - "5G", - "Automation", - "GSM", - "UMTS", - "OSS", - "Cellular", - "REST", - "AutoProvisioning", - "TopologyBrowser", - "API" - ], - "author": "vvsviridov", - "license": "MIT", - "dependencies": { - "axios": "^0.21.2", - "axios-cookiejar-support": "^1.0.1", - "chalk": "^4.1.2", - "commander": "^9.1.0", - "dotenv": "^16.0.0", - "form-data": "^4.0.0", - "inquirer": "^8.2.2", - "inquirer-autocomplete-prompt": "^2.0.0", - "inquirer-file-tree-selection-prompt": "^1.0.18", - "ora": "^5.4.1", - "tough-cookie": "^4.0.0" - } -} +{ + "name": "enm-cli", + "version": "1.0.0", + "description": "This is a cli application for Ericsson Network Manager (ENM)", + "bin": "./bin/enm-cli.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Ericsson", + "ENM", + "Network", + "Manager", + "LTE", + "3G", + "4G", + "5G", + "Automation", + "GSM", + "UMTS", + "OSS", + "Cellular", + "REST", + "AutoProvisioning", + "TopologyBrowser", + "API" + ], + "author": "vvsviridov", + "license": "MIT", + "dependencies": { + "axios": "^0.21.2", + "axios-cookiejar-support": "^1.0.1", + "chalk": "^4.1.2", + "commander": "^9.1.0", + "dotenv": "^16.0.0", + "form-data": "^4.0.0", + "inquirer": "^8.2.2", + "inquirer-autocomplete-prompt": "^2.0.0", + "inquirer-file-tree-selection-prompt": "^1.0.18", + "ora": "^5.4.1", + "tough-cookie": "^4.0.0" + } +} diff --git a/util/banner.js b/util/banner.js new file mode 100755 index 0000000..740f95d --- /dev/null +++ b/util/banner.js @@ -0,0 +1,11 @@ + +function banner(params) { + console.log(` + If you see this, pls, send me this message: + ${JSON.stringify(params, null, 2)} + If you see this, pls, send me this message: + `) +} + + +module.exports = banner \ No newline at end of file diff --git a/util/createNext.js b/util/createNext.js old mode 100644 new mode 100755 index f6a7174..004960f --- a/util/createNext.js +++ b/util/createNext.js @@ -1,17 +1,17 @@ -const inquirer = require('inquirer') - - -function createNext(filter) { - const separator = new inquirer.Separator() - const commands = this.commands.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) - const choices = this.choices.filter(choice => choice.toLowerCase().includes(filter.toLowerCase())) - return [ - separator, - ...commands.map(cmd => `[${cmd}]`), - separator, - ...choices, - ] -} - - +const inquirer = require('inquirer') + + +function createNext(filter) { + const separator = new inquirer.Separator() + const commands = this.commands.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) + const choices = this.choices.filter(choice => choice.toLowerCase().includes(filter.toLowerCase())) + return [ + ...choices, + separator, + ...commands.map(cmd => `[${cmd}]`), + separator, + ] +} + + module.exports = createNext \ No newline at end of file diff --git a/util/eventTimeToString.js b/util/eventTimeToString.js new file mode 100755 index 0000000..9982dfd --- /dev/null +++ b/util/eventTimeToString.js @@ -0,0 +1,10 @@ + + +function eventTimeToString(eventTime) { + if (!eventTime) return '' + return new Date(eventTime).toISOString().slice(0, -1).split('T').join(' ') + // return `${eventDateTime.toLocaleDateString()} ${eventDateTime.toLocaleTimeString()}` +} + + +module.exports = eventTimeToString \ No newline at end of file diff --git a/util/logAlarm.js b/util/logAlarm.js new file mode 100755 index 0000000..77c2e7d --- /dev/null +++ b/util/logAlarm.js @@ -0,0 +1,26 @@ +const chalk = require('chalk') +const eventTimeToString = require('./eventTimeToString') + + + +const timeValues = [ + 'eventTime', + 'insertTime', + 'ceaseTime', + 'ackTime', +] + + +function logAlarm(alarmList, eventPoId) { + const alarm = alarmList.filter(item => item.eventPoIdAsLong === eventPoId)[0] + timeValues.forEach(value => alarm[value] = eventTimeToString(alarm[value])) + console.log( + JSON.stringify(alarm, null, 2) + .replace(/["(){}\[\]]/mg, '') + .replace(/,$/mg, '') + .replace(/^(\s{2}\w+):/mg, chalk.green('$1:')) + ) +} + + +module.exports = logAlarm \ No newline at end of file diff --git a/util/logAttributeData.js b/util/logAttributeData.js new file mode 100755 index 0000000..59cefd2 --- /dev/null +++ b/util/logAttributeData.js @@ -0,0 +1,84 @@ +const chalk = require('chalk') + + +function logDefaultValue(value) { + return value ? ` default: ${value}` : '' +} + + +function logAttribute(key, attribute, output) { + let attrName = key.replace(/([A-Z])/g, ' $1') + if (attribute !== undefined && attribute !== '') { + output.push(`${chalk.blue(attrName.toLocaleUpperCase())} + ${attribute} + `) + } +} + + +function logConstraints(constraints, output) { + output.push(`${chalk.blue(Object.keys({constraints}).pop().toLocaleUpperCase())}`) + if (constraints.valueRangeConstraints) { + constraints.valueRangeConstraints.forEach(item => { + output.push(` ${chalk.yellow('Range')}: ${item.minValue}..${item.maxValue}`) + }) + } + ['nullable', 'validContentRegex', 'valueResolution'].forEach(key => { + if (Object.keys(constraints).includes(key)) { + output.push(` ${chalk.yellow(key.replace(/([A-Z])/g, ' $1').replace(/^([a-z])/g, (l) => l.toUpperCase()))}: ${constraints[key]}`) + } + }) +} + + +function logEnumeration(enumeration, output) { + output.push(`${chalk.blue(Object.keys({enumeration}).pop().toLocaleUpperCase())} + ${chalk.cyan(enumeration.key)} + ${enumeration.description}`) + enumeration.enumMembers.forEach(item => output.push(` ${chalk.yellow(item.key)} (${item.value}): -- ${chalk.gray(item.description)}`)) +} + + +function logList(listReference, output) { + output.push(`${chalk.blue(Object.keys({listReference}).pop().toLocaleUpperCase())} + ${listReference.type}`) + if (listReference.constraints){ + logConstraints(listReference.constraints, output) + } +} + + +function logAttributeData(attributeData) { + const attributeDataKeys = [ + 'key', + 'type', + 'defaultValue', + 'description', + 'trafficDisturbances', + 'unit', + 'multiplicationFactor', + 'immutable', + 'precondition', + 'dependencies', + 'sideEffects', + 'activeChoiceCase', + ] + + const output = [` +${chalk.yellow.bold(attributeData['key'])}: ${chalk.green(attributeData['type'])} ${logDefaultValue(attributeData['defaultValue'])} + `] + attributeDataKeys.slice(3).forEach((key) => logAttribute(key, attributeData[key], output)) + if (attributeData.constraints) { + logConstraints(attributeData.constraints, output) + } + if (attributeData.enumeration) { + logEnumeration(attributeData.enumeration, output) + } + if (attributeData.listReference) { + logList(attributeData.listReference, output) + } + console.log(output.join('\n') + '\n') +} + + +module.exports = logAttributeData \ No newline at end of file diff --git a/util/logAttributes.js b/util/logAttributes.js new file mode 100755 index 0000000..bce52c5 --- /dev/null +++ b/util/logAttributes.js @@ -0,0 +1,34 @@ +const chalk = require('chalk') + + +function transformAttributes(element) { + if (Array.isArray(element)) { + return element.map(item => transformAttributes(item)) + } + if (Array.isArray(element.value)) { + return { [element.key]: transformAttributes(element.value) } + } + return element.key ? { [element.key]: element.value } : element +} + + +function colorize(attributes) { + const sorted = attributes.sort ? attributes.sort((a, b) => a.key < b.key ? -1 : 1) : attributes + return JSON.stringify(transformAttributes(sorted), null, 1) + .replace(/["(){}\[\]]/mg, '') + .replace(/^\s*,*\n/mg, '') + .replace(/,$/mg, '') + .replace(/^(\s{2}\w+):/mg, chalk.green('$1:')) + .replace(/^(\s{4}\w+):/mg, chalk.yellow('$1:')) + .replace(/^(\s{5}\w+):/mg, chalk.cyan('$1:')) +} + + +function logAttributes(fdn, attributes) { + const output = ` + ${chalk.yellow.bold('FDN')}: ${chalk.bold(fdn)} +${colorize(attributes)}` + console.log(output) +} + +module.exports = logAttributes \ No newline at end of file diff --git a/util/logCommit.js b/util/logCommit.js new file mode 100755 index 0000000..ebd1831 --- /dev/null +++ b/util/logCommit.js @@ -0,0 +1,10 @@ +const chalk = require('chalk') + + +function logCommit(commitResult) { + if (commitResult.title === 'Success') { + console.log(chalk.green(commitResult.title)) + } +} + +module.exports = logCommit \ No newline at end of file diff --git a/util/logDetails.js b/util/logDetails.js new file mode 100755 index 0000000..1882e55 --- /dev/null +++ b/util/logDetails.js @@ -0,0 +1,11 @@ +const chalk = require('chalk') + + +function logDetails(networkDetails) { + const output = networkDetails.map(details => ` ${chalk.gray(details.key)}: ${details.value === 'UNSYNCHRONIZED' ? '⌛ ' + chalk.yellow(details.value): chalk.gray(details.value)}`) + console.log(` +${output.join('\n')} + `) +} + +module.exports = logDetails \ No newline at end of file diff --git a/util/logError.js b/util/logError.js old mode 100644 new mode 100755 index e471621..5f05715 --- a/util/logError.js +++ b/util/logError.js @@ -1,29 +1,53 @@ -const chalk = require('chalk') - -function logError(err) { - if (err.response && err.response.data && err.response.data.errorTitle) { - const { - errorTitle = 'Error', - errorBody = 'No error body', - errorDetails = null - } = err.response.data - console.log(` - ⚠️ ${chalk.bold.bgRed(errorTitle)} - ${chalk.yellow(errorBody || '')}${errorDetails ? '\n' + errorDetails.toString() : ''} - `) - } else { - const { - name = 'Error', - message = 'No error message', - stack = null, - } = err - console.log(` - ⛔ ${chalk.bold.bgRed(name)} - ${chalk.yellow(message)} - ${chalk.dim(stack && process.env.NODE_ENV === 'development' ? stack : '')} - `) - } -} - - -module.exports = logError +const chalk = require('chalk') + +function logError(err) { + try { + if (!err.response) { + const { + name = 'Error', + message = 'No error message', + stack = null, + } = err + console.log(` + ⛔ ${chalk.bold.bgRed(name)} + ${chalk.yellow(message)}${chalk.dim(stack && process.env.NODE_ENV === 'development' ? '\n' + stack : '')} + `) + return + } + if (err.response.data) { + const { data } = err.response + // other http error + let errorTitle = `${err.response.status}: ${err.response.statusText}` + let errorBody = data.code + let errorDetails = data.message + if (typeof data !== 'object') { + errorBody = data + } + if (data.errorCode) { + errorBody = `${data.errorCode}: ${data.userMessage.title}` + errorDetails = data.userMessage.body + } + // prvn error + if (data.errorTitle) { + errorTitle = data.errorTitle + errorBody = data.errorBody + errorDetails = data.errorDetails + } + // tplg error + if (data.title) { + errorTitle = `${data.errorCode}: ${data.title}` + errorBody = data.body + errorDetails = data.detail + } + console.log(` + ⚠️ ${chalk.bold.bgRed(errorTitle)} + ${chalk.yellow(errorBody)}${errorDetails ? '\n' + errorDetails.toString() : ''} + `) + } + } catch (error) { + console.log(error) + } +} + + +module.exports = logError diff --git a/util/logNode.js b/util/logNode.js old mode 100644 new mode 100755 index 27067aa..b0bb859 --- a/util/logNode.js +++ b/util/logNode.js @@ -1,49 +1,49 @@ -const chalk = require('chalk') - - -function logNodeStatus(statusEntries, indent = 4) { - if (!statusEntries) { - throw new Error('No node status entries!') - } - console.log('') - statusEntries.forEach(entry => { - const { task, progress, timestamp, additionalInfo } = entry - console.log(`${' '.repeat(indent)}${chalk.cyan(task)} ${progress} at ${chalk.italic.gray(timestamp)}`) - if (additionalInfo) { - additionalInfo.trim().split('\n').forEach(info => { - console.log(`${' '.repeat(indent + 2) + chalk.dim(info)}`) - }) - } - }) - console.log('') -} - - -function logNodeProperties(attributes, attributeGroups, indent = 4) { - if (!attributeGroups) { - throw new Error('No node attribute groups!') - } - console.log('') - logAttributes(attributes) - console.log('') - attributeGroups.forEach(attributeGroup => { - const { type, properties } = attributeGroup - console.log(`${' '.repeat(indent)}${chalk.bold.yellow(type + '↓')}`) - logAttributes(properties, indent + 2) - console.log('') - }) -} - - -function logAttributes(attributes, indent = 4) { - if (!attributes) { - throw new Error('No node attributes!') - } - attributes.forEach(attribute => { - const { name, value } = attribute - console.log(`${' '.repeat(indent)}${name ? chalk.bold.cyan(name + ': ') : ''}${value}`) - }) -} - - +const chalk = require('chalk') + + +function logNodeStatus(statusEntries, indent = 4) { + if (!statusEntries) { + throw new Error('No node status entries❗') + } + console.log('') + statusEntries.forEach(entry => { + const { task, progress, timestamp, additionalInfo } = entry + console.log(`${' '.repeat(indent)}${chalk.cyan(task)} ${progress} at ${chalk.italic.gray(timestamp)}`) + if (additionalInfo) { + additionalInfo.trim().split('\n').forEach(info => { + console.log(`${' '.repeat(indent + 2) + chalk.dim(info)}`) + }) + } + }) + console.log('') +} + + +function logNodeProperties(attributes, attributeGroups, indent = 4) { + if (!attributeGroups) { + throw new Error('No node attribute groups❗') + } + console.log('') + logAttributes(attributes) + console.log('') + attributeGroups.forEach(attributeGroup => { + const { type, properties } = attributeGroup + console.log(`${' '.repeat(indent)}${chalk.bold.yellow(type + '↓')}`) + logAttributes(properties, indent + 2) + console.log('') + }) +} + + +function logAttributes(attributes, indent = 4) { + if (!attributes) { + throw new Error('No node attributes❗') + } + attributes.forEach(attribute => { + const { name, value } = attribute + console.log(`${' '.repeat(indent)}${name ? chalk.bold.cyan(name + ': ') : ''}${value}`) + }) +} + + module.exports = { logNodeStatus, logNodeProperties } \ No newline at end of file diff --git a/util/logProject.js b/util/logProject.js old mode 100644 new mode 100755 index c28f278..edb9472 --- a/util/logProject.js +++ b/util/logProject.js @@ -1,36 +1,36 @@ -const chalk = require('chalk') - - -function logProject(data, nodeSummary) { - if (!data || !nodeSummary) { - throw new Error('No project data or node summary!') - } - const { - id: projectId, - description, - creator, - creationDate, - nodes, - } = data - console.log(` - ${chalk.italic.yellowBright('Project id')} : ${chalk.bold.inverse(projectId)} - ${chalk.italic.yellowBright('Author')} : ${chalk.underline(creator)} - ${chalk.italic.yellowBright('Creation Date')} : ${chalk.dim.gray(creationDate)} - ${chalk.italic.yellowBright('Description')} : ${description} - ${chalk.italic.yellowBright('Nodes')} : - `) - nodes.forEach(node => { - const { id: nodeId, type, identifier, ipAddress } = node - const { status, state } = nodeSummary.find(item => item.id === nodeId) - console.log(` ${chalk.cyan(nodeId)} - ${type} - ${identifier} - ${ipAddress} - ${status === 'Successful' ? chalk.greenBright(status) : chalk.redBright(status) } - ${state} - `) - }) -} - - +const chalk = require('chalk') + + +function logProject(data, nodeSummary) { + if (!data || !nodeSummary) { + throw new Error('No project data or node summary!') + } + const { + id: projectId, + description, + creator, + creationDate, + nodes, + } = data + console.log(` + ${chalk.italic.yellowBright('Project id')} : ${chalk.bold.inverse(projectId)} + ${chalk.italic.yellowBright('Author')} : ${chalk.underline(creator)} + ${chalk.italic.yellowBright('Creation Date')} : ${chalk.dim.gray(creationDate)} + ${chalk.italic.yellowBright('Description')} : ${description} + ${chalk.italic.yellowBright('Nodes')} : + `) + nodes.forEach(node => { + const { id: nodeId, type, identifier, ipAddress } = node + const { status, state } = nodeSummary.find(item => item.id === nodeId) + console.log(` ${chalk.cyan(nodeId)} + ${type} + ${identifier} + ${ipAddress} + ${status === 'Successful' ? chalk.greenBright(status) : chalk.redBright(status) } + ${state} + `) + }) +} + + module.exports = logProject \ No newline at end of file diff --git a/util/validation.js b/util/validation.js old mode 100644 new mode 100755 index d7c3ceb..ee157c0 --- a/util/validation.js +++ b/util/validation.js @@ -1,10 +1,63 @@ -const chalk = require('chalk') - - -const isEmpty = input => (input === '' ? chalk.bgRed('Empty Inputs not Allowed') : true) - -const isValidHardwareId = input => (input.match(/[A-HJ-NP-Z0-9]{13}/) ? true : chalk.bgRed(`The Ericsson Hardware Serial Number frame consists of 13 alphanumeric characters. -Character set is the letters A-Z with the exception of O and I, and digits 0-9.`)) - - -module.exports = { isEmpty, isValidHardwareId } \ No newline at end of file +const chalk = require('chalk') + + +const isEmpty = input => (input === '' ? chalk.bgRed('Empty Inputs not Allowed') : true) + +const isValidHardwareId = input => (input.match(/[A-HJ-NP-Z0-9]{13}/) ? true : chalk.bgRed(`The Ericsson Hardware Serial Number frame consists of 13 alphanumeric characters. +Character set is the letters A-Z with the exception of O and I, and digits 0-9.`)) + +const isValidNumber = (input, constraints) => { + if (constraints) { + const test = input ? input : '' + if (!constraints.nullable && test === 'null') return chalk.red('Value Can\'t Be a null') + if (constraints.nullable && test === 'null') return true + if (!test.toString().match(/-?[0-9]+/)) return chalk.red('Input Is Not a Number') + const checkResult = checkValueRangeConstraints(+test, constraints) + if (checkResult) return checkResult + } + return true +} + +const isValidString = (input, constraints) => { + if (constraints) { + const test = input ? input : '' + if (!constraints.nullable && test === 'null') return chalk.red('Value Can\'t Be a null') + if (constraints.nullable && test === 'null') return true + if (constraints.validContentRegex && !test.match(constraints.validContentRegex)) return chalk.red('Input Doesn\'t Match RegEx') + const checkResult = checkValueRangeConstraints(test.length, constraints) + if (checkResult) return checkResult + } + return true +} + +const checkValueRangeConstraints = (value, constraints) => { + let min + let max + if (constraints.valueRangeConstraints) { + const inRange = constraints.valueRangeConstraints.some(item => { + min = item.minValue + max = item.maxValue + return min <= value && max >= value + }) + errStrArr = constraints.valueRangeConstraints.map(item => `${item.minValue}..${item.maxValue}`) + if (!inRange) return chalk.red(`Input is Outside Allowed Range: ${errStrArr.join(', ')}`) + } +} + + +const isValidNodeName = (input) => { + if (input.match('[a-zA-Z0-9-_.\\/:$]+') || input === '') { + return true + } + return chalk.red('Node Name is Invalid') +} + + +module.exports = { + isEmpty, + isValidHardwareId, + isValidNumber, + isValidString, + checkValueRangeConstraints, + isValidNodeName, +} \ No newline at end of file