first commit

This commit is contained in:
Vyacheslav.Sviridov
2022-05-13 18:13:36 +06:00
commit 1fb7a6f81b
37 changed files with 1991 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
.vscode
package-lock.json
.env

21
LICENSE Normal file
View File

@@ -0,0 +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
SOFTWARE.

194
README.md Normal file
View File

@@ -0,0 +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 <letters> ENM User Login
-p, --password <letters> ENM User Password
-u, --url <letters> 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

93
bin/enm-cli.js Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
const program = require('commander')
const pkg = require('../package.json')
const inquirer = require('inquirer')
require('dotenv').config()
const AutoProvisioning = require('../lib/components/AutoProvisioning/AutoProvisioning')
const TopologyBrowser = require('../lib/components/TopologyBrowser/TopologyBrowser')
const inputHandler = require('../lib/components/AutoProvisioning/inputHandler')
const logError = require('../util/logError')
program
.version(pkg.version)
.option('-l, --login <letters>', 'ENM User Login')
.option('-p, --password <letters>', 'ENM User Password')
.option('-a, --application <letters>', 'Start specified application')
.requiredOption('-u, --url <letters>', 'ENM Url')
.parse(process.argv)
const options = program.opts()
const applications = ['tplg', 'prvn']
async function promptUsername() {
if (process.env.LOGIN) return process.env.LOGIN
const input = await inquirer.prompt([
{
type: 'input',
name: 'value',
suffix: chalk.bgGreen('?'),
message: 'Type ENM login',
// validate: input => isValidNumber(input, attributeData.constraints),
}
])
return input.value
}
async function promptPassword() {
if (process.env.PASSWORD) return process.env.PASSWORD
const input = await inquirer.prompt([
{
type: 'password',
name: 'value',
message: `Type ${options.login}'s ENM password`,
// validate: input => isValidNumber(input, attributeData.constraints),
}
])
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())()

View File

@@ -0,0 +1,90 @@
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)
}
}
module.exports = AutoProvisioning

View File

@@ -0,0 +1,107 @@
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)
}
}
module.exports = inputHandler

View File

@@ -0,0 +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
}

View File

@@ -0,0 +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 }

View File

@@ -0,0 +1,48 @@
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
}
module.exports = axiosHttpClient

32
lib/components/ENM/ENM.js Normal file
View File

@@ -0,0 +1,32 @@
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)
}
}
module.exports = ENM

View File

@@ -0,0 +1,132 @@
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

View File

@@ -0,0 +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)
}
module.exports = alarms

View File

@@ -0,0 +1,34 @@
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

View File

@@ -0,0 +1,39 @@
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)`
}
module.exports = config

View File

@@ -0,0 +1,21 @@
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

View File

@@ -0,0 +1,9 @@
function end() {
this.isConfig = false
this.attribute = null
this.configSet.length = 0
this.nextVariants = async (input) => await this.nextObjects(input)
}
module.exports = end

View File

@@ -0,0 +1,28 @@
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

View File

@@ -0,0 +1,20 @@
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

View File

@@ -0,0 +1,8 @@
function home() {
this.nextPoId = this.poIds.shift()
this.poIds.length = 0
this.nextVariants = async (input) => await this.nextObjects(input)
}
module.exports = home

View File

@@ -0,0 +1,23 @@
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}`
}
module.exports = initialPrompt

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +1,34 @@
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

View File

@@ -0,0 +1,28 @@
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

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +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
}
module.exports = setIdByCommand

View File

@@ -0,0 +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)))
}
module.exports = show

View File

@@ -0,0 +1,30 @@
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

View File

@@ -0,0 +1,12 @@
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

View File

@@ -0,0 +1,136 @@
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

View File

@@ -0,0 +1,156 @@
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

43
package.json Normal file
View File

@@ -0,0 +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"
}
}

View File

@@ -0,0 +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
}
}
}
module.exports = SpinnerWithCounter

17
util/createNext.js Normal file
View File

@@ -0,0 +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,
]
}
module.exports = createNext

29
util/logError.js Normal file
View File

@@ -0,0 +1,29 @@
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

49
util/logNode.js Normal file
View File

@@ -0,0 +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}`)
})
}
module.exports = { logNodeStatus, logNodeProperties }

36
util/logProject.js Normal file
View File

@@ -0,0 +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}
`)
})
}
module.exports = logProject

10
util/validation.js Normal file
View File

@@ -0,0 +1,10 @@
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 }