From da8d1cbf1c2531b7a823a7aca1a8c77051b19d60 Mon Sep 17 00:00:00 2001 From: garciaale Date: Wed, 21 Oct 2020 15:06:16 -0300 Subject: [PATCH] Updates branch sol006 with master Change-Id: Ie684cefe015b1fd167d26af5911e1b216499507d Signed-off-by: garciaale --- .gitignore | 15 + Dockerfile | 8 +- README.md | 138 +- debian/py3dist-overrides | 15 + debian/python3-osmclient.postinst | 30 + devops-stages/stage-build.sh | 2 +- osmclient/client.py | 23 + osmclient/common/exceptions.py | 6 +- osmclient/common/http.py | 12 + osmclient/common/package_tool.py | 235 ++- osmclient/common/utils.py | 2 +- osmclient/common/wait.py | 241 ++- osmclient/scripts/osm.py | 3126 ++++++++++++++++++----------- osmclient/sol005/client.py | 44 +- osmclient/sol005/http.py | 146 +- osmclient/sol005/k8scluster.py | 80 +- osmclient/sol005/ns.py | 306 +-- osmclient/sol005/nsd.py | 181 +- osmclient/sol005/nsi.py | 179 +- osmclient/sol005/nst.py | 182 +- osmclient/sol005/osmrepo.py | 379 ++++ osmclient/sol005/package.py | 82 +- osmclient/sol005/pdud.py | 63 +- osmclient/sol005/project.py | 80 +- osmclient/sol005/repo.py | 79 +- osmclient/sol005/role.py | 81 +- osmclient/sol005/sdncontroller.py | 120 +- osmclient/sol005/user.py | 98 +- osmclient/sol005/vim.py | 144 +- osmclient/sol005/vnf.py | 27 +- osmclient/sol005/vnfd.py | 250 ++- osmclient/sol005/wim.py | 125 +- requirements.txt | 27 + setup.py | 3 +- snap/local/README.md | 47 + snap/snapcraft.yaml | 79 +- test-requirements.txt | 1 - tox.ini | 24 +- 38 files changed, 4372 insertions(+), 2308 deletions(-) create mode 100644 debian/py3dist-overrides create mode 100644 debian/python3-osmclient.postinst create mode 100644 osmclient/sol005/osmrepo.py create mode 100644 requirements.txt create mode 100644 snap/local/README.md diff --git a/.gitignore b/.gitignore index afae2d8..b4c9ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. *.pyc .cache deb_dist/ @@ -17,3 +31,4 @@ stage/ .tox/ snap/.snapcraft/ .vscode +build/ diff --git a/Dockerfile b/Dockerfile index e40b8bc..ef0175a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. # -FROM ubuntu:16.04 +FROM ubuntu:18.04 -RUN apt-get update && apt-get -y install git make python python3 \ - libcurl4-gnutls-dev libgnutls-dev tox python-dev python3-dev \ - debhelper python-setuptools python3-setuptools python-all python3-all \ +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install git \ + make python3 python3-pip libcurl4-openssl-dev libssl-dev tox python3-dev \ + debhelper python3-setuptools python3-all python-all python-pip \ apt-utils diff --git a/README.md b/README.md index bc1f89c..be8b0d2 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,71 @@ + # python-osmclient -A python client for osm orchestration -A test commit +OSM client library and console script -# Installation +## Installation + +### From git-repo -## python-osmclient -### Install dependencies ```bash -sudo apt-get install python-dev libcurl4-gnutls-dev python-pip libgnutls-dev python-prettytable   -sudo pip install pycurl +# Ubuntu 18.04 pre-requirements +sudo apt-get install python3-pip libcurl4-openssl-dev libssl-dev +# CentOS pre-requirements +# sudo yum install python3-pip libcurl-devel gnutls-devel +sudo -H python3 -m pip install python-magic +# Install OSM Information model +sudo -H python3 -m pip install git+https://osm.etsi.org/gerrit/osm/IM --upgrade +# Install OSM client from the git repo. +# You can install the latest client from master branch in this way: +sudo -H python3 -m pip install git+https://osm.etsi.org/gerrit/osm/osmclient +# You could also install a specific tag/version in this way +# sudo -H python3 -m pip install git+https://osm.etsi.org/gerrit/osm/osmclient@v7.0.0rc1 ``` -### Install python-osmclient - sudo pip install git+https://github.com/mfmarche/python-osmclient - +### From cloned repo (for developers) -## Snap -```bash -apt install snapd -snap install osmclient --channel=beta ``` +# Ubuntu 18.04 pre-requirements +sudo apt-get install python3-pip libcurl4-openssl-dev libssl-dev +# Centos pre-requirements +# sudo yum install python3-pip libcurl-devel gnutls-devel +sudo -H python3 -m pip install python-magic +# Install OSM Information model +sudo -H python3 -m pip install git+https://osm.etsi.org/gerrit/osm/IM --upgrade +# Clone the osmclient repo and install OSM client from the git repo. +git clone https://osm.etsi.org/gerrit/osm/osmclient +cd osmclient +python3 -m pip install --user -e . +# logout and login so that PATH can be updated. Executable osm will be found in /home/ubuntu/.local/bin +``` + +## Setup -# Setup -Set the OSM_HOSTNAME variable to the host of the osm server. +Set the OSM_HOSTNAME variable to the host of the OSM server (default: localhost). -Example ```bash -localhost$ export OSM_HOSTNAME=:8008 +localhost$ export OSM_HOSTNAME= ``` -# Examples +## Examples + +### upload vnfd -## upload vnfd ```bash localhost$ osm upload-package ubuntu_xenial_vnf.tar.gz {'transaction_id': 'ec12af77-1b91-4c84-b233-60f2c2c16d14'} @@ -44,7 +77,8 @@ localhost$ osm vnfd-list +--------------------+--------------------+ ``` -## upload nsd +### upload nsd + ```bash localhost$ osm upload-package ubuntu_xenial_ns.tar.gz {'transaction_id': 'b560c9cb-43e1-49ef-a2da-af7aab24ce9d'} @@ -55,7 +89,8 @@ localhost$ osm nsd-list | ubuntu_xenial_nsd | ubuntu_xenial_nsd | +-------------------+-------------------+ ``` -## vim-list + +### vim-list ```bash localhost$ osm vim-list @@ -66,8 +101,8 @@ localhost$ osm vim-list +-------------+-----------------+--------------------------------------+ ``` +### instantiate ns -## instantiate ns ```bash localhost$ osm ns-create ubuntu_xenial_nsd testns openstack-site {'success': ''} @@ -79,7 +114,58 @@ localhost$ osm ns-list +------------------+--------------------------------------+-------------------+--------------------+---------------+ ``` -# Bash Completion -python-osmclient uses [click](http://click.pocoo.org/5/). You can setup bash completion by putting this in your .bashrc: +## Using osmclient as a library to interact with OSM + +Assuming that you have installed python-osmclient package, it's pretty simple to write some Python code to interact with OSM. + +### Simple Python code to get the list of NS packages + +```python +from osmclient import client +from osmclient.common.exceptions import ClientException +hostname = "127.0.0.1" +myclient = client.Client(host=hostname, sol005=True) +resp = myclient.nsd.list() +print yaml.safe_dump(resp, indent=4, default_flow_style=False) +``` + +### Simple Python code to get the list of VNF packages from a specific user and project + +The code will print for each package a pretty table, then the full details in yaml + +```python +from osmclient import client +from osmclient.common.exceptions import ClientException +import yaml +from prettytable import PrettyTable +hostname = "127.0.0.1" +user = admin +password = admin +project = admin +kwargs = {} +if user is not None: + kwargs['user']=user +if password is not None: + kwargs['password']=password +if project is not None: + kwargs['project']=project +myclient = client.Client(host=hostname, sol005=True, **kwargs) +resp = myclient.vnfd.list() +print yaml.safe_dump(resp, indent=4, default_flow_style=False) +``` + +## Enable autocompletion + +You can enable autocompletion in OSM client by creating a file osm-complete.sh in the following way: + +```bash +mkdir -p $HOME/.bash_completion.d +_OSM_COMPLETE=source osm > $HOME/.bash_completion.d/osm-complete.sh +``` + +Then you can add the following to your $HOME/.bashrc file: + +```bash +. .bash_completion.d/osm-complete.sh +``` - eval "$(_OSM_COMPLETE=source osm)" diff --git a/debian/py3dist-overrides b/debian/py3dist-overrides new file mode 100644 index 0000000..e6059cd --- /dev/null +++ b/debian/py3dist-overrides @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +python-magic diff --git a/debian/python3-osmclient.postinst b/debian/python3-osmclient.postinst new file mode 100644 index 0000000..75ec83f --- /dev/null +++ b/debian/python3-osmclient.postinst @@ -0,0 +1,30 @@ +#!/bin/bash + +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +echo "POST INSTALL OSMCLIENT" +#Install pyangbind, required for python3-osm-im +python3 -m pip install pyangbind verboselogs +#configure autocomplete for osmclient +[ -z "$SUDO_USER" ] && SUDO_USER="$USER" +su $SUDO_USER -c 'mkdir -p $HOME/.bash_completion.d' +su $SUDO_USER -c '_OSM_COMPLETE=source osm > $HOME/.bash_completion.d/osm-complete.sh' + +if ! su $SUDO_USER -c 'grep -q bash_completion.d/osm-complete.sh ${HOME}/.bashrc' +then + echo " inserting .bash_completion.d/osm-complete.sh execution at .bashrc" + su $SUDO_USER -c 'echo ". ${HOME}/.bash_completion.d/osm-complete.sh" >> ${HOME}/.bashrc' +fi + diff --git a/devops-stages/stage-build.sh b/devops-stages/stage-build.sh index dd4af61..7b00243 100755 --- a/devops-stages/stage-build.sh +++ b/devops-stages/stage-build.sh @@ -13,5 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -rm -rf deb_dist +rm -rf deb_dist osmclient-*.tar.gz tox -e build,build3 diff --git a/osmclient/client.py b/osmclient/client.py index eccfee2..56570b4 100644 --- a/osmclient/client.py +++ b/osmclient/client.py @@ -21,9 +21,32 @@ OSM client entry point from osmclient.v1 import client as client from osmclient.sol005 import client as sol005client +import logging +import verboselogs +verboselogs.install() def Client(version=1, host=None, sol005=True, *args, **kwargs): + log_format_simple = "%(levelname)s %(message)s" + log_format_complete = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s" + log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S') + handler = logging.StreamHandler() + handler.setFormatter(log_formatter_simple) + logger = logging.getLogger('osmclient') + logger.setLevel(level=logging.WARNING) + logger.addHandler(handler) + verbose = kwargs.get('verbose',0) + if verbose>0: + log_formatter = logging.Formatter(log_format_complete, datefmt='%Y-%m-%dT%H:%M:%S') + #handler = logging.StreamHandler() + handler.setFormatter(log_formatter) + #logger.addHandler(handler) + if verbose==1: + logger.setLevel(level=logging.INFO) + elif verbose==2: + logger.setLevel(level=logging.VERBOSE) + elif verbose>2: + logger.setLevel(level=logging.DEBUG) if not sol005: if version == 1: return client.Client(host, *args, **kwargs) diff --git a/osmclient/common/exceptions.py b/osmclient/common/exceptions.py index 0be2778..12de003 100644 --- a/osmclient/common/exceptions.py +++ b/osmclient/common/exceptions.py @@ -19,5 +19,9 @@ class ClientException(Exception): pass -class NotFound(ClientException): +class OsmHttpException(ClientException): + pass + + +class NotFound(OsmHttpException): pass diff --git a/osmclient/common/http.py b/osmclient/common/http.py index 17f82e3..b67b594 100644 --- a/osmclient/common/http.py +++ b/osmclient/common/http.py @@ -50,9 +50,13 @@ class Http(object): curl_cmd = self._get_curl_cmd(endpoint) curl_cmd.setopt(pycurl.HTTPGET, 1) curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + self._logger.info("Request METHOD: {} URL: {}".format("GET",self._url + endpoint)) curl_cmd.perform() + http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() if data.getvalue(): + self._logger.debug("Response DATA: {}".format(json.loads(data.getvalue().decode()))) return json.loads(data.getvalue().decode()) return None @@ -61,9 +65,13 @@ class Http(object): curl_cmd = self._get_curl_cmd(endpoint) curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE") curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + self._logger.info("Request METHOD: {} URL: {}".format("DELETE",self._url + endpoint)) curl_cmd.perform() + http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() if data.getvalue(): + self._logger.debug("Response DATA: {}".format(json.loads(data.getvalue().decode()))) return json.loads(data.getvalue().decode()) return None @@ -84,8 +92,12 @@ class Http(object): (pycurl.FORM_FILE, formfile[1])))]) + self._logger.info("Request METHOD: {} URL: {}".format("POST",self._url + endpoint)) curl_cmd.perform() + http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() if data.getvalue(): + self._logger.debug("Response DATA: {}".format(json.loads(data.getvalue().decode()))) return json.loads(data.getvalue().decode()) return None diff --git a/osmclient/common/package_tool.py b/osmclient/common/package_tool.py index 7cc10be..19b1386 100644 --- a/osmclient/common/package_tool.py +++ b/osmclient/common/package_tool.py @@ -23,11 +23,15 @@ import tarfile import hashlib from osm_im.validation import Validation as validation_im from jinja2 import Environment, PackageLoader - +import subprocess +import shutil +import yaml +import logging class PackageTool(object): def __init__(self, client=None): self._client = client + self._logger = logging.getLogger('osmclient') def create(self, package_type, base_directory, package_name, override, image, vdus, vcpu, memory, storage, interfaces, vendor, detailed, netslice_subnets, netslice_vlds): @@ -50,7 +54,7 @@ class PackageTool(object): :return: status """ - + self._logger.debug("") # print("location: {}".format(osmclient.__path__)) file_loader = PackageLoader("osmclient") env = Environment(loader=file_loader) @@ -80,7 +84,7 @@ class PackageTool(object): self.create_files(structure["files"], output, package_type) return "Created" - def validate(self, base_directory): + def validate(self, base_directory, recursive=True): """ **Validate OSM Descriptors given a path** @@ -89,8 +93,12 @@ class PackageTool(object): :return: List of dict of validated descriptors. keys: type, path, valid, error """ + self._logger.debug("") table = [] - descriptors_paths = [f for f in glob.glob(base_directory + "/**/*.yaml", recursive=True)] + if recursive: + descriptors_paths = [f for f in glob.glob(base_directory + "/**/*.yaml", recursive=recursive)] + else: + descriptors_paths = [f for f in glob.glob(base_directory + "/*.yaml", recursive=recursive)] print("Base directory: {}".format(base_directory)) print("{} Descriptors found to validate".format(len(descriptors_paths))) for desc_path in descriptors_paths: @@ -105,7 +113,7 @@ class PackageTool(object): table.append({"type": desc_type, "path": desc_path, "valid": "ERROR", "error": str(e)}) return table - def build(self, package_folder, skip_validation=True): + def build(self, package_folder, skip_validation=False, skip_charm_build=False): """ **Creates a .tar.gz file given a package_folder** @@ -115,20 +123,23 @@ class PackageTool(object): :returns: message result for the build process """ - + self._logger.debug("") + package_folder = package_folder.rstrip('/') if not os.path.exists("{}".format(package_folder)): - return "Fail, package is not in the specified route" + return "Fail, package is not in the specified path" if not skip_validation: - results = self.validate(package_folder) - for result in results: - if result["valid"] != "OK": - return("There was an error validating the file: {} with error: {}".format(result["path"], - result["error"])) - self.calculate_checksum(package_folder) - with tarfile.open("{}.tar.gz".format(package_folder), mode='w:gz') as archive: - print("Adding File: {}".format(package_folder)) - archive.add('{}'.format(package_folder), recursive=True) - return "Created {}.tar.gz".format(package_folder) + print('Validating package {}'.format(package_folder)) + results = self.validate(package_folder, recursive=False) + if results: + for result in results: + if result["valid"] != "OK": + raise ClientException("There was an error validating the file {} with error: {}" + .format(result["path"], result["error"])) + print('Validation OK') + else: + raise ClientException("No descriptor file found in: {}".format(package_folder)) + charm_list = self.build_all_charms(package_folder, skip_charm_build) + return self.build_tarfile(package_folder, charm_list) def calculate_checksum(self, package_folder): """ @@ -138,19 +149,19 @@ class PackageTool(object): - package_folder: is the folder where we have the files to calculate the checksum :returns: None """ - files = [f for f in glob.glob(package_folder + "/**/*.*", recursive=True)] - checksum = open("{}/checksum.txt".format(package_folder), "w+") - for file_item in files: - if "checksum.txt" in file_item: - continue - # from https://www.quickprogrammingtips.com/python/how-to-calculate-md5-hash-of-a-file-in-python.html - md5_hash = hashlib.md5() - with open(file_item, "rb") as f: - # Read and update hash in chunks of 4K - for byte_block in iter(lambda: f.read(4096), b""): - md5_hash.update(byte_block) - checksum.write("{}\t{}\n".format(md5_hash.hexdigest(), file_item)) - checksum.close() + self._logger.debug("") + files = [f for f in glob.glob(package_folder + "/**/*.*", recursive=True) if os.path.isfile(f)] + with open("{}/checksums.txt".format(package_folder), "w+") as checksum: + for file_item in files: + if "checksums.txt" in file_item: + continue + # from https://www.quickprogrammingtips.com/python/how-to-calculate-md5-hash-of-a-file-in-python.html + md5_hash = hashlib.md5() + with open(file_item, "rb") as f: + # Read and update hash in chunks of 4K + for byte_block in iter(lambda: f.read(4096), b""): + md5_hash.update(byte_block) + checksum.write("{}\t{}\n".format(md5_hash.hexdigest(), file_item)) def create_folders(self, folders, package_type): """ @@ -215,6 +226,7 @@ class PackageTool(object): :return: None """ + self._logger.debug("") for file_item, file_package, file_type in files: if package_type == file_package: if file_type == "descriptor": @@ -234,6 +246,7 @@ class PackageTool(object): :return: Missing paths Dict """ + self._logger.debug("") missing_paths = {} folders = [] files = [] @@ -249,6 +262,42 @@ class PackageTool(object): return missing_paths + def build_all_charms(self, package_folder, skip_charm_build): + """ + **Read the descriptor file, check that the charms referenced are in the folder and compiles them** + + :params: + - packet_folder: is the location of the package + :return: Files and Folders not found. In case of override, it will return all file list + """ + self._logger.debug("") + listCharms = [] + descriptor_file = False + descriptors_paths = [f for f in glob.glob(package_folder + "/*.yaml")] + for file in descriptors_paths: + if file.endswith('nfd.yaml'): + descriptor_file = True + listCharms = self.charms_search(file, 'vnf') + if file.endswith('nsd.yaml'): + descriptor_file = True + listCharms = self.charms_search(file, 'ns') + print("List of charms in the descriptor: {}".format(listCharms)) + if not descriptor_file: + raise ClientException ('descriptor name is not correct in: {}'.format(package_folder)) + if listCharms and not skip_charm_build: + for charmName in listCharms: + if os.path.isdir('{}/charms/layers/{}'.format(package_folder,charmName)): + print('Building charm {}/charms/layers/{}'.format(package_folder, charmName)) + self.charm_build(package_folder, charmName) + print('Charm built: {}'.format(charmName)) + else: + if not os.path.isdir('{}/charms/{}'.format(package_folder,charmName)): + raise ClientException ('The charm: {} referenced in the descriptor file ' + 'is not present either in {}/charms or in {}/charms/layers'. + format(charmName, package_folder,package_folder)) + self._logger.debug("Return list of charms: {}".format(listCharms)) + return listCharms + def discover_folder_structure(self, base_directory, name, override): """ **Discover files and folders structure for OSM descriptors given a base_directory and name** @@ -259,6 +308,7 @@ class PackageTool(object): - override: is the flag used to indicate the creation of the list even if the file exist to override it :return: Files and Folders not found. In case of override, it will return all file list """ + self._logger.debug("") prefix = "{}/{}".format(base_directory, name) files_folders = {"folders": [("{}_ns".format(prefix), "ns"), ("{}_ns/icons".format(prefix), "ns"), @@ -284,3 +334,128 @@ class PackageTool(object): missing_files_folders = self.check_files_folders(files_folders, override) # print("Missing files and folders: {}".format(missing_files_folders)) return missing_files_folders + + def charm_build(self, charms_folder, build_name): + """ + Build the charms inside the package. + params: package_folder is the name of the folder where is the charms to compile. + build_name is the name of the layer or interface + """ + self._logger.debug("") + os.environ['JUJU_REPOSITORY'] = "{}/charms".format(charms_folder) + os.environ['CHARM_LAYERS_DIR'] = "{}/layers".format(os.environ['JUJU_REPOSITORY']) + os.environ['CHARM_INTERFACES_DIR'] = "{}/interfaces".format(os.environ['JUJU_REPOSITORY']) + os.environ['CHARM_BUILD_DIR'] = "{}/charms/builds".format(charms_folder) + if not os.path.exists(os.environ['CHARM_BUILD_DIR']): + os.makedirs(os.environ['CHARM_BUILD_DIR']) + src_folder = '{}/{}'.format(os.environ['CHARM_LAYERS_DIR'], build_name) + result = subprocess.run(["charm", "build", "{}".format(src_folder)]) + if result.returncode == 1: + raise ClientException("failed to build the charm: {}".format(src_folder)) + self._logger.verbose("charm {} built".format(src_folder)) + + def build_tarfile(self, package_folder, charm_list=None): + """ + Creates a .tar.gz file given a package_folder + params: package_folder is the name of the folder to be packaged + returns: .tar.gz name + """ + self._logger.debug("") + cwd = None + try: + directory_name, package_name = self.create_temp_dir(package_folder, charm_list) + cwd = os.getcwd() + os.chdir(directory_name) + self.calculate_checksum(package_name) + with tarfile.open("{}.tar.gz".format(package_name), mode='w:gz') as archive: + print("Adding File: {}".format(package_name)) + archive.add('{}'.format(package_name), recursive=True) + # return "Created {}.tar.gz".format(package_folder) + # self.build("{}".format(os.path.basename(package_folder))) + os.chdir(cwd) + cwd = None + created_package = "{}/{}.tar.gz".format(os.path.dirname(package_folder) or '.', package_name) + os.rename("{}/{}.tar.gz".format(directory_name, package_name), + created_package) + os.rename("{}/{}/checksums.txt".format(directory_name, package_name), + "{}/checksums.txt".format(package_folder)) + print("Package created: {}".format(created_package)) + return created_package + except Exception as exc: + raise ClientException('failure during build of targz file (create temp dir, calculate checksum, ' + 'tar.gz file): {}'.format(exc)) + finally: + if cwd: + os.chdir(cwd) + shutil.rmtree(os.path.join(package_folder, "tmp")) + + def create_temp_dir(self, package_folder, charm_list=None): + """ + Method to create a temporary folder where we can move the files in package_folder + """ + self._logger.debug("") + ignore_patterns = ('.gitignore') + ignore = shutil.ignore_patterns(ignore_patterns) + directory_name = os.path.abspath(package_folder) + package_name = os.path.basename(directory_name) + directory_name += "/tmp" + os.makedirs("{}/{}".format(directory_name, package_name), exist_ok=True) + self._logger.debug("Makedirs DONE: {}/{}".format(directory_name, package_name)) + for item in os.listdir(package_folder): + self._logger.debug("Item: {}".format(item)) + if item != "tmp": + s = os.path.join(package_folder, item) + d = os.path.join(os.path.join(directory_name, package_name), item) + if os.path.isdir(s): + if item == "charms": + os.makedirs(d, exist_ok=True) + s_builds = os.path.join(s, "builds") + for charm in charm_list: + self._logger.debug("Copying charm {}".format(charm)) + if charm in os.listdir(s): + s_charm = os.path.join(s, charm) + elif charm in os.listdir(s_builds): + s_charm = os.path.join(s_builds, charm) + else: + raise ClientException('The charm {} referenced in the descriptor file ' + 'could not be found in {}/charms or in {}/charms/builds'. + format(charm, package_folder, package_folder)) + d_temp = os.path.join(d, charm) + self._logger.debug("Copying tree: {} -> {}".format(s_charm, d_temp)) + shutil.copytree(s_charm, d_temp, symlinks=True, ignore=ignore) + self._logger.debug("DONE") + else: + self._logger.debug("Copying tree: {} -> {}".format(s,d)) + shutil.copytree(s, d, symlinks=True, ignore=ignore) + self._logger.debug("DONE") + else: + if item in ignore_patterns: + continue + self._logger.debug("Copying file: {} -> {}".format(s,d)) + shutil.copy2(s, d) + self._logger.debug("DONE") + return directory_name, package_name + + def charms_search(self, descriptor_file, desc_type): + self._logger.debug("") + dict = {} + list = [] + with open("{}".format(descriptor_file)) as yaml_desc: + dict = yaml.safe_load(yaml_desc) + for k1, v1 in dict.items(): + for k2, v2 in v1.items(): + for entry in v2: + if '{}-configuration'.format(desc_type) in entry: + name = entry['{}-configuration'.format(desc_type)] + for k3, v3 in name.items(): + if 'charm' in v3: + list.append((v3['charm'])) + if 'vdu' in entry: + name = entry['vdu'] + for vdu in name: + if 'vdu-configuration' in vdu: + for k4, v4 in vdu['vdu-configuration'].items(): + if 'charm' in v4: + list.append((v4['charm'])) + return list + diff --git a/osmclient/common/utils.py b/osmclient/common/utils.py index 43287e9..ec0e0b0 100644 --- a/osmclient/common/utils.py +++ b/osmclient/common/utils.py @@ -71,7 +71,7 @@ def get_key_val_from_pkg(descriptor_file): for k1, v1 in list(dict.items()): if not k1.endswith('-catalog'): continue - for k2, v2 in list(v1.items()): + for k2, v2 in v1.items(): if not k2.endswith('nsd') and not k2.endswith('vnfd'): continue diff --git a/osmclient/common/wait.py b/osmclient/common/wait.py index e3152af..bb9a82a 100644 --- a/osmclient/common/wait.py +++ b/osmclient/common/wait.py @@ -18,10 +18,10 @@ OSM API handling for the '--wait' option """ -from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import ClientException, NotFound import json -from time import sleep -import sys +from time import sleep, time +from sys import stderr # Declare a constant for each module, to allow customizing each timeout in the future TIMEOUT_GENERIC_OPERATION = 600 @@ -34,165 +34,150 @@ TIMEOUT_NS_OPERATION = 3600 POLLING_TIME_INTERVAL = 5 MAX_DELETE_ATTEMPTS = 3 + def _show_detailed_status(old_detailed_status, new_detailed_status): if new_detailed_status is not None and new_detailed_status != old_detailed_status: - sys.stderr.write("detailed-status: {}\n".format(new_detailed_status)) + stderr.write("detailed-status: {}\n".format(new_detailed_status)) return new_detailed_status else: return old_detailed_status + def _get_finished_states(entity): - # Note that the member name is either: - # 'operationState' (NS, NSI) - # '_admin.'operationalState' (VIM, WIM, SDN) - # For NS and NSI, 'operationState' may be one of: - # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK - # For VIM, WIM, SDN: '_admin.operationalState' may be one of: - # operationalState: ENABLED, DISABLED, ERROR, PROCESSING + """ + Member name is either: + operationState' (NS, NSI) + '_admin.'operationalState' (VIM, WIM, SDN) + For NS and NSI, 'operationState' may be one of: + PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK + For VIM, WIM, SDN: '_admin.operationalState' may be one of: + ENABLED, DISABLED, ERROR, PROCESSING + + :param entity: can be NS, NSI, or other + :return: two tuples with status completed strings, status failed string + """ if entity == 'NS' or entity == 'NSI': - return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED'] + return ('COMPLETED', 'PARTIALLY_COMPLETED'), ('FAILED_TEMP', 'FAILED') else: - return ['ENABLED', 'ERROR'] + return ('ENABLED', ), ('ERROR', ) + def _get_operational_state(resp, entity): - # Note that the member name is either: - # 'operationState' (NS) - # 'operational-status' (NSI) - # '_admin.'operationalState' (other) + """ + The member name is either: + 'operationState' (NS) + 'operational-status' (NSI) + '_admin.'operationalState' (other) + :param resp: descriptor of the get response + :param entity: can be NS, NSI, or other + :return: status of the operation + """ if entity == 'NS' or entity == 'NSI': return resp.get('operationState') else: return resp.get('_admin', {}).get('operationalState') + def _op_has_finished(resp, entity): - # This function returns: - # 0 on success (operation has finished) - # 1 on pending (operation has not finished) - # -1 on error (bad response) - # - finished_states = _get_finished_states(entity) + """ + Indicates if operation has finished ok or is processing + :param resp: descriptor of the get response + :param entity: can be NS, NSI, or other + :return: + True on success (operation has finished) + False on pending (operation has not finished) + raise Exception if unexpected response, or ended with error + """ + finished_states_ok, finished_states_error = _get_finished_states(entity) if resp: - operationalState = _get_operational_state(resp, entity) - if operationalState: - if operationalState in finished_states: - return 0 - return 1 - return -1 - -def _get_detailed_status(resp, entity, detailed_status_deleted): - if detailed_status_deleted: - return detailed_status_deleted - if entity == 'NS' or entity == 'NSI': + op_state = _get_operational_state(resp, entity) + if op_state: + if op_state in finished_states_ok: + return True + elif op_state in finished_states_error: + raise ClientException("Operation failed with status '{}'".format(op_state)) + return False + raise ClientException('Unexpected response from server: {} '.format(resp)) + + +def _get_detailed_status(resp, entity): + """ + For VIM, WIM, SDN, 'detailed-status' is either: + - a leaf node to '_admin' (operations NOT supported) + - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI) + :param resp: content of the get response + :param entity: can be NS, NSI, or other + :return: + """ + if entity in ('NS', 'NSI'): # For NS and NSI, 'detailed-status' is a JSON "root" member: return resp.get('detailed-status') else: - # For VIM, WIM, SDN, 'detailed-status' is either: - # - a leaf node to '_admin' (operations NOT supported) - # - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI) - # https://osm.etsi.org/gerrit/#/c/7767 : LCM support for operations - # https://osm.etsi.org/gerrit/#/c/7734 : NBI support for current_operation ops = resp.get('_admin', {}).get('operations') - op_index = resp.get('_admin', {}).get('current_operation') - if ops and op_index: + current_op = resp.get('_admin', {}).get('current_operation') + if ops and current_op is not None: # Operations are supported, verify operation index - if isinstance(op_index, (int)) or op_index.isdigit(): - op_index = int(op_index) - if op_index > 0 and op_index < len(ops) and ops[op_index] and ops[op_index]["detailed-status"]: - return ops[op_index]["detailed-status"] + if isinstance(ops, dict) and current_op in ops: + return ops[current_op].get("detailed-status") + elif isinstance(ops, list) and isinstance(current_op, int) or current_op.isdigit(): + current_op = int(current_op) + if current_op >= 0 and current_op < len(ops) and ops[current_op] and ops[current_op]["detailed-status"]: + return ops[current_op]["detailed-status"] # operation index is either non-numeric or out-of-range return 'Unexpected error when getting detailed-status!' else: # Operations are NOT supported return resp.get('_admin', {}).get('detailed-status') -def _has_delete_error(resp, entity, deleteFlag, delete_attempts_left): - if deleteFlag and delete_attempts_left: - state = _get_operational_state(resp, entity) - if state and state == 'ERROR': - return True - return False def wait_for_status(entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False): - # Arguments: - # entity_label: String describing the entities using '--wait': - # 'NS', 'NSI', 'SDNC', 'VIM', 'WIM' - # entity_id: The ID for an existing entity, the operation ID for an entity to create. - # timeout: See section at top of this file for each value of TIMEOUT__OPERATION - # apiUrlStatus: The endpoint to get the Response including 'detailed-status' - # http_cmd: callback to HTTP command. - # Passing this callback as an argument avoids importing the 'http' module here. + """ + Wait until operation ends, making polling every 5s. Prints detailed status when it changes + :param entity_label: String describing the entities using '--wait': 'NS', 'NSI', 'SDNC', 'VIM', 'WIM' + :param entity_id: The ID for an existing entity, the operation ID for an entity to create. + :param timeout: Timeout in seconds + :param apiUrlStatus: The endpoint to get the Response including 'detailed-status' + :param http_cmd: callback to HTTP command. (Normally the get method) + :param deleteFlag: If this is a delete operation + :return: None, exception if operation fails or timeout + """ # Loop here until the operation finishes, or a timeout occurs. - time_left = timeout + time_to_finish = time() + timeout detailed_status = None - detailed_status_deleted = None - time_to_return = False - delete_attempts_left = MAX_DELETE_ATTEMPTS - wait_for_404 = False - try: - while True: + retries = 0 + max_retries = 1 + while True: + try: http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id)) - resp = '' - if resp_unicode: - resp = json.loads(resp_unicode) - # print('HTTP CODE: {}'.format(http_code)) - # print('RESP: {}'.format(resp)) - # print('URL: {}/{}'.format(apiUrlStatus, entity_id)) - if deleteFlag and http_code == 404: - # In case of deletion, '404 Not Found' means successfully deleted - # Display 'detailed-status: Deleted' and return - time_to_return = True - detailed_status_deleted = 'Deleted' - elif deleteFlag and http_code in (200, 201, 202, 204): - # In case of deletion and HTTP Status = 20* OK, deletion may be PROCESSING or COMPLETED - # If this is the case, we should keep on polling until 404 (deleted) is returned. - wait_for_404 = True - elif http_code not in (200, 201, 202, 204): - raise ClientException(str(resp)) - if not time_to_return: - # Get operation status - op_status = _op_has_finished(resp, entity_label) - if op_status == -1: - # An error occurred - raise ClientException('unexpected response from server - {} '.format( - str(resp))) - elif op_status == 0: - # If there was an error upon deletion, try again to delete the same instance - # If the error is the same, there is probably nothing we can do but exit with error. - # If the error is different (i.e. 404), the instance was probably already corrupt, that is, - # operation(al)State was probably ERROR before deletion. - # In such a case, even if the previous state was ERROR, the deletion was successful, - # so detailed-status should be set to Deleted. - if _has_delete_error(resp, entity_label, deleteFlag, delete_attempts_left): - delete_attempts_left -= 1 - else: - # Operation has finished, either with success or error - if deleteFlag: - delete_attempts_left -= 1 - if not wait_for_404 and delete_attempts_left < MAX_DELETE_ATTEMPTS: - time_to_return = True - else: - time_to_return = True - new_detailed_status = _get_detailed_status(resp, entity_label, detailed_status_deleted) - # print('DETAILED-STATUS: {}'.format(new_detailed_status)) - # print('DELETE-ATTEMPTS-LEFT: {}'.format(delete_attempts_left)) - if not new_detailed_status: - new_detailed_status = 'In progress' - # TODO: Change LCM to provide detailed-status more up to date - # At the moment of this writing, 'detailed-status' may return different strings - # from different resources: - # /nslcm/v1/ns_lcm_op_occs/ ---> '' - # /nslcm/v1/ns_instances_content/ ---> 'deleting charms' - detailed_status = _show_detailed_status(detailed_status, new_detailed_status) - if time_to_return: + retries = 0 + except NotFound: + if deleteFlag: + _show_detailed_status(detailed_status, 'Deleted') return - time_left -= POLLING_TIME_INTERVAL + raise + except ClientException: + if retries >= max_retries or time() < time_to_finish: + raise + retries += 1 sleep(POLLING_TIME_INTERVAL) - if time_left <= 0: - # There was a timeout, so raise an exception - raise ClientException('operation timeout, waited for {} seconds'.format(timeout)) - except ClientException as exc: - message="Operation failed for {}:\nerror:\n{}".format( - entity_label, - str(exc)) - raise ClientException(message) + continue + + resp = '' + if resp_unicode: + resp = json.loads(resp_unicode) + + new_detailed_status = _get_detailed_status(resp, entity_label) + # print('DETAILED-STATUS: {}'.format(new_detailed_status)) + if not new_detailed_status: + new_detailed_status = 'In progress' + detailed_status = _show_detailed_status(detailed_status, new_detailed_status) + + # Get operation status + if _op_has_finished(resp, entity_label): + return + + if time() >= time_to_finish: + # There was a timeout, so raise an exception + raise ClientException('operation timeout after {} seconds'.format(timeout)) + sleep(POLLING_TIME_INTERVAL) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 18b8033..3ef81ac 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -20,7 +20,7 @@ OSM shell/cli import click from osmclient import client -from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import ClientException, NotFound from prettytable import PrettyTable import yaml import json @@ -29,8 +29,14 @@ import pycurl import os import textwrap import pkg_resources +import logging +from datetime import datetime +# Global variables + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=160) + def wrap_text(text, width): wrapper = textwrap.TextWrapper(width=width) lines = text.splitlines() @@ -53,6 +59,7 @@ def check_client_version(obj, what, version='sol005'): :return: - :raises ClientError: if the specified version does not match the client version """ + logger.debug("") fullclassname = obj.__module__ + "." + obj.__class__.__name__ message = 'The following commands or options are only supported with the option "--sol005": {}'.format(what) if version == 'v1': @@ -62,9 +69,7 @@ def check_client_version(obj, what, version='sol005'): return -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=160) - -@click.group(context_settings=CONTEXT_SETTINGS) +@click.group(context_settings=dict(help_option_names=['-h', '--help'], max_content_width=160)) @click.option('--hostname', default="127.0.0.1", envvar='OSM_HOSTNAME', @@ -90,6 +95,24 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=16 envvar='OSM_PROJECT', help='project (defaults to admin). ' + 'Also can set OSM_PROJECT in environment') +@click.option('-v', '--verbose', count=True, + help='increase verbosity (-v INFO, -vv VERBOSE, -vvv DEBUG)') +@click.option('--all-projects', + default=None, + is_flag=True, + help='include all projects') +@click.option('--public/--no-public', default=None, + help='flag for public items (packages, instances, VIM accounts, etc.)') +@click.option('--project-domain-name', 'project_domain_name', + default=None, + envvar='OSM_PROJECT_DOMAIN_NAME', + help='project domain name for keystone authentication (default to None). ' + + 'Also can set OSM_PROJECT_DOMAIN_NAME in environment') +@click.option('--user-domain-name', 'user_domain_name', + default=None, + envvar='OSM_USER_DOMAIN_NAME', + help='user domain name for keystone authentication (default to None). ' + + 'Also can set OSM_USER_DOMAIN_NAME in environment') #@click.option('--so-port', # default=None, # envvar='OSM_SO_PORT', @@ -111,13 +134,16 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=16 # help='hostname of RO server. ' + # 'Also can set OSM_RO_PORT in environment') @click.pass_context -def cli(ctx, hostname, user, password, project): +def cli_osm(ctx, **kwargs): + global logger + hostname = kwargs.pop("hostname", None) if hostname is None: print(( "either hostname option or OSM_HOSTNAME " + "environment variable needs to be specified")) exit(1) - kwargs={} + # Remove None values + kwargs = {k: v for k, v in kwargs.items() if v is not None} # if so_port is not None: # kwargs['so_port']=so_port # if so_project is not None: @@ -127,25 +153,31 @@ def cli(ctx, hostname, user, password, project): # if ro_port is not None: # kwargs['ro_port']=ro_port sol005 = os.getenv('OSM_SOL005', True) - if user is not None: - kwargs['user']=user - if password is not None: - kwargs['password']=password - if project is not None: - kwargs['project']=project - +# if user is not None: +# kwargs['user']=user +# if password is not None: +# kwargs['password']=password +# if project is not None: +# kwargs['project']=project +# if all_projects: +# kwargs['all_projects']=all_projects +# if public is not None: +# kwargs['public']=public ctx.obj = client.Client(host=hostname, sol005=sol005, **kwargs) + logger = logging.getLogger('osmclient') #################### # LIST operations #################### -@cli.command(name='ns-list', short_help='list all NS instances') +@cli_osm.command(name='ns-list', short_help='list all NS instances') @click.option('--filter', default=None, help='restricts the list to the NS instances matching the filter.') +@click.option('--long', is_flag=True, + help='get more details of the NS (project, vim, deployment status, configuration status.') @click.pass_context -def ns_list(ctx, filter): +def ns_list(ctx, filter, long): """list all NS instances \b @@ -193,84 +225,270 @@ def ns_list(ctx, filter): --filter nsd.vendor=&nsd-ref= --filter nsd.constituent-vnfd.vnfd-id-ref= """ + def summarize_deployment_status(status_dict): + #Nets + summary = "" + if not status_dict: + return summary + n_nets = 0 + status_nets = {} + net_list = status_dict.get('nets',[]) + for net in net_list: + n_nets += 1 + if net['status'] not in status_nets: + status_nets[net['status']] = 1 + else: + status_nets[net['status']] +=1 + message = "Nets: " + for k,v in status_nets.items(): + message += "{}:{},".format(k,v) + message += "TOTAL:{}".format(n_nets) + summary += "{}".format(message) + #VMs and VNFs + n_vms = 0 + status_vms = {} + status_vnfs = {} + vnf_list = status_dict['vnfs'] + for vnf in vnf_list: + member_vnf_index = vnf['member_vnf_index'] + if member_vnf_index not in status_vnfs: + status_vnfs[member_vnf_index] = {} + for vm in vnf['vms']: + n_vms += 1 + if vm['status'] not in status_vms: + status_vms[vm['status']] = 1 + else: + status_vms[vm['status']] +=1 + if vm['status'] not in status_vnfs[member_vnf_index]: + status_vnfs[member_vnf_index][vm['status']] = 1 + else: + status_vnfs[member_vnf_index][vm['status']] += 1 + message = "VMs: " + for k,v in status_vms.items(): + message += "{}:{},".format(k,v) + message += "TOTAL:{}".format(n_vms) + summary += "\n{}".format(message) + summary += "\nNFs:" + for k,v in status_vnfs.items(): + total = 0 + message = "\n {} VMs: ".format(k) + for k2,v2 in v.items(): + message += "{}:{},".format(k2,v2) + total += v2 + message += "TOTAL:{}".format(total) + summary += message + return summary + + def summarize_config_status(ee_list): + summary = "" + if not ee_list: + return summary + n_ee = 0 + status_ee = {} + for ee in ee_list: + n_ee += 1 + if ee['elementType'] not in status_ee: + status_ee[ee['elementType']] = {} + status_ee[ee['elementType']][ee['status']] = 1 + continue + if ee['status'] in status_ee[ee['elementType']]: + status_ee[ee['elementType']][ee['status']] += 1 + else: + status_ee[ee['elementType']][ee['status']] = 1 + for elementType in ["KDU", "VDU", "PDU", "VNF", "NS"]: + if elementType in status_ee: + message = "" + total = 0 + for k,v in status_ee[elementType].items(): + message += "{}:{},".format(k,v) + total += v + message += "TOTAL:{}\n".format(total) + summary += "{}: {}".format(elementType, message) + summary += "TOTAL Exec. Env.: {}".format(n_ee) + return summary + + logger.debug("") if filter: check_client_version(ctx.obj, '--filter') resp = ctx.obj.ns.list(filter) else: resp = ctx.obj.ns.list() - table = PrettyTable( + if long: + table = PrettyTable( ['ns instance name', 'id', - 'operational status', - 'config status', - 'detailed status']) + 'date', + 'ns state', + 'current operation', + 'error details', + 'project', + 'vim (inst param)', + 'deployment status', + 'configuration status']) + project_list = ctx.obj.project.list() + vim_list = ctx.obj.vim.list() + else: + table = PrettyTable( + ['ns instance name', + 'id', + 'date', + 'ns state', + 'current operation', + 'error details']) for ns in resp: fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ if fullclassname == 'osmclient.sol005.client.Client': nsr = ns + logger.debug('NS info: {}'.format(nsr)) nsr_name = nsr['name'] nsr_id = nsr['_id'] + date = datetime.fromtimestamp(nsr['create-time']).strftime("%Y-%m-%dT%H:%M:%S") + ns_state = nsr.get('nsState', nsr['_admin']['nsState']) + if long: + deployment_status = summarize_deployment_status(nsr.get('deploymentStatus')) + config_status = summarize_config_status(nsr.get('configurationStatus')) + project_id = nsr.get('_admin').get('projects_read')[0] + project_name = '-' + for p in project_list: + if p['_id'] == project_id: + project_name = p['name'] + break + #project = '{} ({})'.format(project_name, project_id) + project = project_name + vim_id = nsr.get('datacenter') + vim_name = '-' + for v in vim_list: + if v['uuid'] == vim_id: + vim_name = v['name'] + break + #vim = '{} ({})'.format(vim_name, vim_id) + vim = vim_name + if 'currentOperation' in nsr: + current_operation = "{} ({})".format(nsr['currentOperation'],nsr['currentOperationID']) + else: + current_operation = "{} ({})".format(nsr['_admin'].get('current-operation','-'), nsr['_admin']['nslcmop']) + error_details = "N/A" + if ns_state == "BROKEN" or ns_state == "DEGRADED" or \ + ('currentOperation' not in nsr and nsr.get('errorDescription')): + error_details = "{}\nDetail: {}".format(nsr['errorDescription'], nsr['errorDetail']) else: nsopdata = ctx.obj.ns.get_opdata(ns['id']) nsr = nsopdata['nsr:nsr'] nsr_name = nsr['name-ref'] nsr_id = nsr['ns-instance-config-ref'] - opstatus = nsr['operational-status'] if 'operational-status' in nsr else 'Not found' - configstatus = nsr['config-status'] if 'config-status' in nsr else 'Not found' - detailed_status = nsr['detailed-status'] if 'detailed-status' in nsr else 'Not found' - detailed_status = wrap_text(text=detailed_status,width=50) - if configstatus == "config_not_needed": - configstatus = "configured (no charms)" - - table.add_row( - [nsr_name, - nsr_id, - opstatus, - configstatus, - detailed_status]) + date = '-' + project = '-' + deployment_status = nsr['operational-status'] if 'operational-status' in nsr else 'Not found' + ns_state = deployment_status + config_status = nsr.get('config-status', 'Not found') + current_operation = "Unknown" + error_details = nsr.get('detailed-status', 'Not found') + if config_status == "config_not_needed": + config_status = "configured (no charms)" + + if long: + table.add_row( + [nsr_name, + nsr_id, + date, + ns_state, + current_operation, + wrap_text(text=error_details,width=40), + project, + vim, + deployment_status, + config_status]) + else: + table.add_row( + [nsr_name, + nsr_id, + date, + ns_state, + current_operation, + wrap_text(text=error_details,width=40)]) table.align = 'l' print(table) + print('To get the history of all operations over a NS, run "osm ns-op-list NS_ID"') + print('For more details on the current operation, run "osm ns-op-show OPERATION_ID"') - -def nsd_list(ctx, filter): +def nsd_list(ctx, filter, long): + logger.debug("") if filter: check_client_version(ctx.obj, '--filter') resp = ctx.obj.nsd.list(filter) else: resp = ctx.obj.nsd.list() # print(yaml.safe_dump(resp)) - table = PrettyTable(['nsd name', 'id']) fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ if fullclassname == 'osmclient.sol005.client.Client': - for ns in resp: - name = ns['name'] if 'name' in ns else '-' - table.add_row([name, ns['_id']]) + if long: + table = PrettyTable(['nsd name', 'id', 'onboarding state', 'operational state', + 'usage state', 'date', 'last update']) + else: + table = PrettyTable(['nsd name', 'id']) + for nsd in resp: + name = nsd.get('name','-') + if long: + onb_state = nsd['_admin'].get('onboardingState','-') + op_state = nsd['_admin'].get('operationalState','-') + usage_state = nsd['_admin'].get('usageState','-') + date = datetime.fromtimestamp(nsd['_admin']['created']).strftime("%Y-%m-%dT%H:%M:%S") + last_update = datetime.fromtimestamp(nsd['_admin']['modified']).strftime("%Y-%m-%dT%H:%M:%S") + table.add_row([name, nsd['_id'], onb_state, op_state, usage_state, date, last_update]) + else: + table.add_row([name, nsd['_id']]) else: - for ns in resp: - table.add_row([ns['name'], ns['id']]) + table = PrettyTable(['nsd name', 'id']) + for nsd in resp: + table.add_row([nsd['name'], nsd['id']]) table.align = 'l' print(table) -@cli.command(name='nsd-list', short_help='list all NS packages') +@cli_osm.command(name='nsd-list', short_help='list all NS packages') @click.option('--filter', default=None, help='restricts the list to the NSD/NSpkg matching the filter') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def nsd_list1(ctx, filter): +def nsd_list1(ctx, filter, long): """list all NSD/NS pkg in the system""" - nsd_list(ctx, filter) + logger.debug("") + nsd_list(ctx, filter, long) -@cli.command(name='nspkg-list', short_help='list all NS packages') +@cli_osm.command(name='nspkg-list', short_help='list all NS packages') @click.option('--filter', default=None, help='restricts the list to the NSD/NSpkg matching the filter') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def nsd_list2(ctx, filter): +def nsd_list2(ctx, filter, long): """list all NS packages""" - nsd_list(ctx, filter) + logger.debug("") + nsd_list(ctx, filter, long) + +def pkg_repo_list(ctx, pkgtype, filter, repo, long): + resp = ctx.obj.osmrepo.pkg_list(pkgtype, filter, repo) + if long: + table = PrettyTable(['nfpkg name', 'vendor', 'version', 'latest', 'description', 'repository']) + else: + table = PrettyTable(['nfpkg name', 'repository']) + for vnfd in resp: + name = vnfd.get('name', '-') + repository = vnfd.get('repository') + if long: + vendor = vnfd.get('vendor') + version = vnfd.get('version') + description = vnfd.get('description') + latest = vnfd.get('latest') + table.add_row([name, vendor, version, latest, description, repository]) + else: + table.add_row([name, repository]) + table.align = 'l' + print(table) -def vnfd_list(ctx, nf_type, filter): +def vnfd_list(ctx, nf_type, filter, long): + logger.debug("") if nf_type: check_client_version(ctx.obj, '--nf_type') elif filter: @@ -293,87 +511,129 @@ def vnfd_list(ctx, nf_type, filter): else: resp = ctx.obj.vnfd.list() # print(yaml.safe_dump(resp)) - table = PrettyTable(['nfpkg name', 'id']) fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ if fullclassname == 'osmclient.sol005.client.Client': + if long: + table = PrettyTable(['nfpkg name', 'id', 'vendor', 'version', 'onboarding state', 'operational state', + 'usage state', 'date', 'last update']) + else: + table = PrettyTable(['nfpkg name', 'id']) for vnfd in resp: name = vnfd['name'] if 'name' in vnfd else '-' - table.add_row([name, vnfd['_id']]) + if long: + onb_state = vnfd['_admin'].get('onboardingState','-') + op_state = vnfd['_admin'].get('operationalState','-') + vendor = vnfd.get('vendor') + version = vnfd.get('version') + usage_state = vnfd['_admin'].get('usageState','-') + date = datetime.fromtimestamp(vnfd['_admin']['created']).strftime("%Y-%m-%dT%H:%M:%S") + last_update = datetime.fromtimestamp(vnfd['_admin']['modified']).strftime("%Y-%m-%dT%H:%M:%S") + table.add_row([name, vnfd['_id'], vendor, version, onb_state, op_state, usage_state, date, last_update]) + else: + table.add_row([name, vnfd['_id']]) else: + table = PrettyTable(['nfpkg name', 'id']) for vnfd in resp: table.add_row([vnfd['name'], vnfd['id']]) table.align = 'l' print(table) -@cli.command(name='vnfd-list', short_help='list all xNF packages (VNF, HNF, PNF)') +@cli_osm.command(name='vnfd-list', short_help='list all xNF packages (VNF, HNF, PNF)') @click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, help='restricts the list to the NF pkg matching the filter') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def vnfd_list1(ctx, nf_type, filter): +def vnfd_list1(ctx, nf_type, filter, long): """list all xNF packages (VNF, HNF, PNF)""" - vnfd_list(ctx, nf_type, filter) + logger.debug("") + vnfd_list(ctx, nf_type, filter, long) -@cli.command(name='vnfpkg-list', short_help='list all xNF packages (VNF, HNF, PNF)') +@cli_osm.command(name='vnfpkg-list', short_help='list all xNF packages (VNF, HNF, PNF)') @click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, help='restricts the list to the NFpkg matching the filter') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def vnfd_list2(ctx, nf_type, filter): +def vnfd_list2(ctx, nf_type, filter, long): """list all xNF packages (VNF, HNF, PNF)""" - vnfd_list(ctx, nf_type, filter) + logger.debug("") + vnfd_list(ctx, nf_type, filter, long) - -@cli.command(name='nfpkg-list', short_help='list all xNF packages (VNF, HNF, PNF)') +@cli_osm.command(name='nfpkg-list', short_help='list all xNF packages (VNF, HNF, PNF)') @click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, help='restricts the list to the NFpkg matching the filter') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def nfpkg_list(ctx, nf_type, filter): +def nfpkg_list(ctx, nf_type, filter, long): """list all xNF packages (VNF, HNF, PNF)""" - try: - check_client_version(ctx.obj, ctx.command.name) - vnfd_list(ctx, nf_type, filter) - except ClientException as e: - print(str(e)) - exit(1) - + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + vnfd_list(ctx, nf_type, filter, long) + # except ClientException as e: + # print(str(e)) + # exit(1) + +@cli_osm.command(name='vnfpkg-repo-list', short_help='list all xNF from OSM repositories') +@click.option('--filter', default=None, + help='restricts the list to the NFpkg matching the filter') +@click.option('--repo', default=None, + help='restricts the list to a particular OSM repository') +@click.option('--long', is_flag=True, help='get more details') +@click.pass_context +def nfpkg_repo_list1(ctx, filter, repo, long): + """list xNF packages from OSM repositories""" + pkgtype = 'vnf' + pkg_repo_list(ctx, pkgtype, filter, repo, long) -def vnf_list(ctx, ns, filter): - try: - if ns or filter: - if ns: - check_client_version(ctx.obj, '--ns') - if filter: - check_client_version(ctx.obj, '--filter') - resp = ctx.obj.vnf.list(ns, filter) - else: - resp = ctx.obj.vnf.list() - except ClientException as e: - print(str(e)) - exit(1) +@cli_osm.command(name='nfpkg-repo-list', short_help='list all xNF from OSM repositories') +@click.option('--filter', default=None, + help='restricts the list to the NFpkg matching the filter') +@click.option('--repo', default=None, + help='restricts the list to a particular OSM repository') +@click.option('--long', is_flag=True, help='get more details') +@click.pass_context +def nfpkg_repo_list2(ctx, filter, repo, long): + """list xNF packages from OSM repositories""" + pkgtype = 'vnf' + pkg_repo_list(ctx, pkgtype, filter, repo, long) + +def vnf_list(ctx, ns, filter, long): + # try: + if ns or filter: + if ns: + check_client_version(ctx.obj, '--ns') + if filter: + check_client_version(ctx.obj, '--filter') + resp = ctx.obj.vnf.list(ns, filter) + else: + resp = ctx.obj.vnf.list() + # except ClientException as e: + # print(str(e)) + # exit(1) fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ if fullclassname == 'osmclient.sol005.client.Client': - table = PrettyTable( - ['vnf id', - 'name', - 'ns id', - 'vnf member index', - 'vnfd name', - 'vim account id', - 'ip address']) + field_names = ['vnf id', 'name', 'ns id', 'vnf member index', + 'vnfd name', 'vim account id', 'ip address'] + if long: + field_names = ['vnf id', 'name', 'ns id', 'vnf member index', + 'vnfd name', 'vim account id', 'ip address', + 'date', 'last update'] + table = PrettyTable(field_names) for vnfr in resp: name = vnfr['name'] if 'name' in vnfr else '-' - table.add_row( - [vnfr['_id'], - name, - vnfr['nsr-id-ref'], - vnfr['member-vnf-index-ref'], - vnfr['vnfd-ref'], - vnfr['vim-account-id'], - vnfr['ip-address']]) + new_row = [vnfr['_id'], name, vnfr['nsr-id-ref'], + vnfr['member-vnf-index-ref'], vnfr['vnfd-ref'], + vnfr['vim-account-id'], vnfr['ip-address']] + if long: + date = datetime.fromtimestamp(vnfr['_admin']['created']).strftime("%Y-%m-%dT%H:%M:%S") + last_update = datetime.fromtimestamp(vnfr['_admin']['modified']).strftime("%Y-%m-%dT%H:%M:%S") + new_row.extend([date, last_update]) + table.add_row(new_row) else: table = PrettyTable( ['vnf name', @@ -393,22 +653,48 @@ def vnf_list(ctx, ns, filter): print(table) -@cli.command(name='vnf-list', short_help='list all NF instances') +@cli_osm.command(name='vnf-list', short_help='list all NF instances') @click.option('--ns', default=None, help='NS instance id or name to restrict the NF list') @click.option('--filter', default=None, help='restricts the list to the NF instances matching the filter.') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def vnf_list1(ctx, ns, filter): +def vnf_list1(ctx, ns, filter, long): """list all NF instances""" - vnf_list(ctx, ns, filter) + logger.debug("") + vnf_list(ctx, ns, filter, long) +@cli_osm.command(name='nsd-repo-list', short_help='list all NS from OSM repositories') +@click.option('--filter', default=None, + help='restricts the list to the NS matching the filter') +@click.option('--repo', default=None, + help='restricts the list to a particular OSM repository') +@click.option('--long', is_flag=True, help='get more details') +@click.pass_context +def nspkg_repo_list(ctx, filter, repo, long): + """list xNF packages from OSM repositories""" + pkgtype = 'ns' + pkg_repo_list(ctx, pkgtype, filter, repo, long) -@cli.command(name='nf-list', short_help='list all NF instances') +@cli_osm.command(name='nspkg-repo-list', short_help='list all NS from OSM repositories') +@click.option('--filter', default=None, + help='restricts the list to the NS matching the filter') +@click.option('--repo', default=None, + help='restricts the list to a particular OSM repository') +@click.option('--long', is_flag=True, help='get more details') +@click.pass_context +def nspkg_repo_list2(ctx, filter, repo, long): + """list xNF packages from OSM repositories""" + pkgtype = 'ns' + pkg_repo_list(ctx, pkgtype, filter, repo, long) + +@cli_osm.command(name='nf-list', short_help='list all NF instances') @click.option('--ns', default=None, help='NS instance id or name to restrict the NF list') @click.option('--filter', default=None, help='restricts the list to the NF instances matching the filter.') +@click.option('--long', is_flag=True, help='get more details') @click.pass_context -def nf_list(ctx, ns, filter): +def nf_list(ctx, ns, filter, long): """list all NF instances \b @@ -456,44 +742,86 @@ def nf_list(ctx, ns, filter): --filter vdur.ip-address= --filter vnfd-ref=,vdur.ip-address= """ - vnf_list(ctx, ns, filter) + logger.debug("") + vnf_list(ctx, ns, filter, long) -@cli.command(name='ns-op-list', short_help='shows the history of operations over a NS instance') +@cli_osm.command(name='ns-op-list', short_help='shows the history of operations over a NS instance') @click.argument('name') +@click.option('--long', is_flag=True, + help='get more details of the NS operation (date, ).') @click.pass_context -def ns_op_list(ctx, name): +def ns_op_list(ctx, name, long): """shows the history of operations over a NS instance NAME: name or ID of the NS instance """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.ns.list_op(name) - except ClientException as e: - print(str(e)) - exit(1) + def formatParams(params): + if params['lcmOperationType']=='instantiate': + params.pop('nsDescription') + params.pop('nsName') + params.pop('nsdId') + params.pop('nsr_id') + elif params['lcmOperationType']=='action': + params.pop('primitive') + params.pop('lcmOperationType') + params.pop('nsInstanceId') + return params + + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.ns.list_op(name) + # except ClientException as e: + # print(str(e)) + # exit(1) + + if long: + table = PrettyTable(['id', 'operation', 'action_name', 'operation_params', 'status', 'date', 'last update', 'detail']) + else: + table = PrettyTable(['id', 'operation', 'action_name', 'status', 'date', 'detail']) - table = PrettyTable(['id', 'operation', 'action_name', 'status']) #print(yaml.safe_dump(resp)) for op in resp: action_name = "N/A" if op['lcmOperationType']=='action': action_name = op['operationParams']['primitive'] - table.add_row([op['id'], op['lcmOperationType'], action_name, - op['operationState']]) + detail = "-" + if op['operationState'] == 'PROCESSING': + if op['queuePosition'] is not None and op['queuePosition'] > 0: + detail = "In queue. Current position: {}".format(op['queuePosition']) + elif op['lcmOperationType'] in ('instantiate', 'terminate'): + if op['stage']: + detail = op['stage'] + elif op['operationState'] in ('FAILED', 'FAILED_TEMP'): + detail = op.get('errorMessage','-') + date = datetime.fromtimestamp(op['startTime']).strftime("%Y-%m-%dT%H:%M:%S") + last_update = datetime.fromtimestamp(op['statusEnteredTime']).strftime("%Y-%m-%dT%H:%M:%S") + if long: + table.add_row([op['id'], + op['lcmOperationType'], + action_name, + wrap_text(text=json.dumps(formatParams(op['operationParams']),indent=2),width=50), + op['operationState'], + date, + last_update, + wrap_text(text=detail,width=50)]) + else: + table.add_row([op['id'], op['lcmOperationType'], action_name, + op['operationState'], date, wrap_text(text=detail or "",width=50)]) table.align = 'l' print(table) def nsi_list(ctx, filter): """list all Network Slice Instances""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.nsi.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nsi.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable( ['netslice instance name', 'id', @@ -518,31 +846,34 @@ def nsi_list(ctx, filter): print(table) -@cli.command(name='nsi-list', short_help='list all Network Slice Instances (NSI)') +@cli_osm.command(name='nsi-list', short_help='list all Network Slice Instances (NSI)') @click.option('--filter', default=None, help='restricts the list to the Network Slice Instances matching the filter') @click.pass_context def nsi_list1(ctx, filter): """list all Network Slice Instances (NSI)""" + logger.debug("") nsi_list(ctx, filter) -@cli.command(name='netslice-instance-list', short_help='list all Network Slice Instances (NSI)') +@cli_osm.command(name='netslice-instance-list', short_help='list all Network Slice Instances (NSI)') @click.option('--filter', default=None, help='restricts the list to the Network Slice Instances matching the filter') @click.pass_context def nsi_list2(ctx, filter): """list all Network Slice Instances (NSI)""" + logger.debug("") nsi_list(ctx, filter) def nst_list(ctx, filter): - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.nst.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nst.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) # print(yaml.safe_dump(resp)) table = PrettyTable(['nst name', 'id']) for nst in resp: @@ -552,31 +883,34 @@ def nst_list(ctx, filter): print(table) -@cli.command(name='nst-list', short_help='list all Network Slice Templates (NST)') +@cli_osm.command(name='nst-list', short_help='list all Network Slice Templates (NST)') @click.option('--filter', default=None, help='restricts the list to the NST matching the filter') @click.pass_context def nst_list1(ctx, filter): """list all Network Slice Templates (NST) in the system""" + logger.debug("") nst_list(ctx, filter) -@cli.command(name='netslice-template-list', short_help='list all Network Slice Templates (NST)') +@cli_osm.command(name='netslice-template-list', short_help='list all Network Slice Templates (NST)') @click.option('--filter', default=None, help='restricts the list to the NST matching the filter') @click.pass_context def nst_list2(ctx, filter): """list all Network Slice Templates (NST) in the system""" + logger.debug("") nst_list(ctx, filter) def nsi_op_list(ctx, name): - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.nsi.list_op(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nsi.list_op(name) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['id', 'operation', 'status']) for op in resp: table.add_row([op['id'], op['lcmOperationType'], @@ -585,7 +919,7 @@ def nsi_op_list(ctx, name): print(table) -@cli.command(name='nsi-op-list', short_help='shows the history of operations over a Network Slice Instance (NSI)') +@cli_osm.command(name='nsi-op-list', short_help='shows the history of operations over a Network Slice Instance (NSI)') @click.argument('name') @click.pass_context def nsi_op_list1(ctx, name): @@ -593,10 +927,11 @@ def nsi_op_list1(ctx, name): NAME: name or ID of the Network Slice Instance """ + logger.debug("") nsi_op_list(ctx, name) -@cli.command(name='netslice-instance-op-list', short_help='shows the history of operations over a Network Slice Instance (NSI)') +@cli_osm.command(name='netslice-instance-op-list', short_help='shows the history of operations over a Network Slice Instance (NSI)') @click.argument('name') @click.pass_context def nsi_op_list2(ctx, name): @@ -604,21 +939,23 @@ def nsi_op_list2(ctx, name): NAME: name or ID of the Network Slice Instance """ + logger.debug("") nsi_op_list(ctx, name) -@cli.command(name='pdu-list', short_help='list all Physical Deployment Units (PDU)') +@cli_osm.command(name='pdu-list', short_help='list all Physical Deployment Units (PDU)') @click.option('--filter', default=None, help='restricts the list to the Physical Deployment Units matching the filter') @click.pass_context def pdu_list(ctx, filter): """list all Physical Deployment Units (PDU)""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.pdu.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.pdu.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable( ['pdu name', 'id', @@ -647,25 +984,26 @@ def pdu_list(ctx, filter): #################### def nsd_show(ctx, name, literal): - try: - resp = ctx.obj.nsd.get(name) - # resp = ctx.obj.nsd.get_individual(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + resp = ctx.obj.nsd.get(name) + # resp = ctx.obj.nsd.get_individual(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(resp)) + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) for k, v in list(resp.items()): - table.add_row([k, json.dumps(v, indent=2)]) + table.add_row([k, wrap_text(text=json.dumps(v, indent=2),width=100)]) table.align = 'l' print(table) -@cli.command(name='nsd-show', short_help='shows the content of a NSD') +@cli_osm.command(name='nsd-show', short_help='shows the details of a NS package') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -675,10 +1013,11 @@ def nsd_show1(ctx, name, literal): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nsd_show(ctx, name, literal) -@cli.command(name='nspkg-show', short_help='shows the content of a NSD') +@cli_osm.command(name='nspkg-show', short_help='shows the details of a NS package') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -688,29 +1027,53 @@ def nsd_show2(ctx, name, literal): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nsd_show(ctx, name, literal) def vnfd_show(ctx, name, literal): - try: - resp = ctx.obj.vnfd.get(name) - # resp = ctx.obj.vnfd.get_individual(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + resp = ctx.obj.vnfd.get(name) + # resp = ctx.obj.vnfd.get_individual(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(resp)) + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) for k, v in list(resp.items()): - table.add_row([k, json.dumps(v, indent=2)]) + table.add_row([k, wrap_text(text=json.dumps(v, indent=2),width=100)]) table.align = 'l' print(table) -@cli.command(name='vnfd-show', short_help='shows the content of a VNFD') +def pkg_repo_show(ctx, pkgtype, name, repo, version, filter, literal): + logger.debug("") + # try: + resp = ctx.obj.osmrepo.pkg_get(pkgtype, name, repo, version, filter) + + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + pkgtype += 'd' + catalog = pkgtype + '-catalog' + full_catalog = pkgtype + ':' + catalog + if resp.get(catalog): + resp = resp.pop(catalog)[pkgtype][0] + elif resp.get(full_catalog): + resp = resp.pop(full_catalog)[pkgtype][0] + + table = PrettyTable(['field', 'value']) + for k, v in list(resp.items()): + table.add_row([k, wrap_text(text=json.dumps(v, indent=2), width=100)]) + table.align = 'l' + print(table) + +@cli_osm.command(name='vnfd-show', short_help='shows the details of a NF package') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -720,10 +1083,11 @@ def vnfd_show1(ctx, name, literal): NAME: name or ID of the VNFD/VNFpkg """ + logger.debug("") vnfd_show(ctx, name, literal) -@cli.command(name='vnfpkg-show', short_help='shows the content of a VNFD') +@cli_osm.command(name='vnfpkg-show', short_help='shows the details of a NF package') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -733,10 +1097,74 @@ def vnfd_show2(ctx, name, literal): NAME: name or ID of the VNFD/VNFpkg """ + logger.debug("") vnfd_show(ctx, name, literal) +@cli_osm.command(name='vnfpkg-repo-show', short_help='shows the details of a NF package in an OSM repository') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--repo', + required=True, + help='Repository name') +@click.argument('name') +@click.option('--filter', + help='filter by fields') +@click.option('--version', + default='latest', + help='package version') +@click.pass_context +def vnfd_show3(ctx, name, repo, version, literal=None, filter=None): + """shows the content of a VNFD in a repository + + NAME: name or ID of the VNFD/VNFpkg + """ + pkgtype = 'vnf' + pkg_repo_show(ctx, pkgtype, name, repo, version, filter, literal) + + +@cli_osm.command(name='nsd-repo-show', short_help='shows the details of a NS package in an OSM repository') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--repo', + required=True, + help='Repository name') +@click.argument('name') +@click.option('--filter', + help='filter by fields') +@click.option('--version', + default='latest', + help='package version') +@click.pass_context +def nsd_repo_show(ctx, name, repo, version, literal=None, filter=None): + """shows the content of a VNFD in a repository + + NAME: name or ID of the VNFD/VNFpkg + """ + pkgtype = 'ns' + pkg_repo_show(ctx, pkgtype, name, repo, version, filter, literal) + +@cli_osm.command(name='nspkg-repo-show', short_help='shows the details of a NS package in an OSM repository') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--repo', + required=True, + help='Repository name') +@click.argument('name') +@click.option('--filter', + help='filter by fields') +@click.option('--version', + default='latest', + help='package version') +@click.pass_context +def nsd_repo_show2(ctx, name, repo, version, literal=None, filter=None): + """shows the content of a VNFD in a repository + + NAME: name or ID of the VNFD/VNFpkg + """ + pkgtype = 'ns' + pkg_repo_show(ctx, pkgtype, name, repo, version, filter, literal) -@cli.command(name='nfpkg-show', short_help='shows the content of a NF Descriptor') +@cli_osm.command(name='nfpkg-show', short_help='shows the details of a NF package') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -746,10 +1174,33 @@ def nfpkg_show(ctx, name, literal): NAME: name or ID of the NFpkg """ + logger.debug("") vnfd_show(ctx, name, literal) -@cli.command(name='ns-show', short_help='shows the info of a NS instance') +@cli_osm.command(name='nfpkg-repo-show', short_help='shows the details of a NF package in an OSM repository') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--repo', + required=True, + help='Repository name') +@click.argument('name') +@click.option('--filter', + help='filter by fields') +@click.option('--version', + default='latest', + help='package version') +@click.pass_context +def vnfd_show4(ctx, name, repo, version, literal=None, filter=None): + """shows the content of a VNFD in a repository + + NAME: name or ID of the VNFD/VNFpkg + """ + pkgtype = 'vnf' + pkg_repo_show(ctx, pkgtype, name, repo, version, filter, literal) + + +@cli_osm.command(name='ns-show', short_help='shows the info of a NS instance') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -760,14 +1211,15 @@ def ns_show(ctx, name, literal, filter): NAME: name or ID of the NS instance """ - try: - ns = ctx.obj.ns.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + ns = ctx.obj.ns.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(ns)) + print(yaml.safe_dump(ns, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) @@ -787,7 +1239,7 @@ def ns_show(ctx, name, literal, filter): print(table) -@cli.command(name='vnf-show', short_help='shows the info of a VNF instance') +@cli_osm.command(name='vnf-show', short_help='shows the info of a VNF instance') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -799,51 +1251,80 @@ def vnf_show(ctx, name, literal, filter, kdu): NAME: name or ID of the VNF instance """ + def print_kdu_status(op_info_status): + """print KDU status properly formatted + """ + try: + op_status = yaml.safe_load(op_info_status) + if "namespace" in op_status and "info" in op_status and \ + "last_deployed" in op_status["info"] and "status" in op_status["info"] and \ + "code" in op_status["info"]["status"] and "resources" in op_status["info"]["status"] and \ + "seconds" in op_status["info"]["last_deployed"]: + last_deployed_time = datetime.fromtimestamp(op_status["info"]["last_deployed"]["seconds"]).strftime("%a %b %d %I:%M:%S %Y") + print("LAST DEPLOYED: {}".format(last_deployed_time)) + print("NAMESPACE: {}".format(op_status["namespace"])) + status_code = "UNKNOWN" + if op_status["info"]["status"]["code"]==1: + status_code = "DEPLOYED" + print("STATUS: {}".format(status_code)) + print() + print("RESOURCES:") + print(op_status["info"]["status"]["resources"]) + if "notes" in op_status["info"]["status"]: + print("NOTES:") + print(op_status["info"]["status"]["notes"]) + else: + print(op_info_status) + except Exception: + print(op_info_status) + + logger.debug("") if kdu: if literal: raise ClientException('"--literal" option is incompatible with "--kdu" option') if filter: raise ClientException('"--filter" option is incompatible with "--kdu" option') - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.vnf.get(name) - - if kdu: - ns_id = resp['nsr-id-ref'] - op_data={} - op_data['member_vnf_index'] = resp['member-vnf-index-ref'] - op_data['kdu_name'] = kdu - op_data['primitive'] = 'status' - op_data['primitive_params'] = {} - op_id = ctx.obj.ns.exec_op(ns_id, op_name='action', op_data=op_data, wait=False) - t = 0 - while t<30: - op_info = ctx.obj.ns.get_op(op_id) - if op_info['operationState'] == 'COMPLETED': - print(op_info['detailed-status']) - return - time.sleep(5) - t += 5 - print ("Could not determine KDU status") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.vnf.get(name) - if literal: - print(yaml.safe_dump(resp)) - return + if kdu: + ns_id = resp['nsr-id-ref'] + op_data={} + op_data['member_vnf_index'] = resp['member-vnf-index-ref'] + op_data['kdu_name'] = kdu + op_data['primitive'] = 'status' + op_data['primitive_params'] = {} + op_id = ctx.obj.ns.exec_op(ns_id, op_name='action', op_data=op_data, wait=False) + t = 0 + while t<30: + op_info = ctx.obj.ns.get_op(op_id) + if op_info['operationState'] == 'COMPLETED': + print_kdu_status(op_info['detailed-status']) + return + time.sleep(5) + t += 5 + print ("Could not determine KDU status") + return - table = PrettyTable(['field', 'value']) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return - for k, v in list(resp.items()): - if filter is None or filter in k: - table.add_row([k, wrap_text(text=json.dumps(v,indent=2),width=100)]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + table = PrettyTable(['field', 'value']) + for k, v in list(resp.items()): + if filter is None or filter in k: + table.add_row([k, wrap_text(text=json.dumps(v,indent=2),width=100)]) + table.align = 'l' + print(table) + # except ClientException as e: + # print(str(e)) + # exit(1) -#@cli.command(name='vnf-monitoring-show') + +#@cli_osm.command(name='vnf-monitoring-show') #@click.argument('vnf_name') #@click.pass_context #def vnf_monitoring_show(ctx, vnf_name): @@ -866,7 +1347,7 @@ def vnf_show(ctx, name, literal, filter, kdu): # print(table) -#@cli.command(name='ns-monitoring-show') +#@cli_osm.command(name='ns-monitoring-show') #@click.argument('ns_name') #@click.pass_context #def ns_monitoring_show(ctx, ns_name): @@ -889,7 +1370,7 @@ def vnf_show(ctx, name, literal, filter, kdu): # print(table) -@cli.command(name='ns-op-show', short_help='shows the info of a NS operation') +@cli_osm.command(name='ns-op-show', short_help='shows the info of a NS operation') @click.argument('id') @click.option('--filter', default=None) @click.option('--literal', is_flag=True, @@ -900,15 +1381,16 @@ def ns_op_show(ctx, id, filter, literal): ID: operation identifier """ - try: - check_client_version(ctx.obj, ctx.command.name) - op_info = ctx.obj.ns.get_op(id) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + op_info = ctx.obj.ns.get_op(id) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(op_info)) + print(yaml.safe_dump(op_info, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) @@ -920,16 +1402,17 @@ def ns_op_show(ctx, id, filter, literal): def nst_show(ctx, name, literal): - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.nst.get(name) - #resp = ctx.obj.nst.get_individual(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nst.get(name) + #resp = ctx.obj.nst.get_individual(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(resp)) + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) @@ -939,7 +1422,7 @@ def nst_show(ctx, name, literal): print(table) -@cli.command(name='nst-show', short_help='shows the content of a Network Slice Template (NST)') +@cli_osm.command(name='nst-show', short_help='shows the content of a Network Slice Template (NST)') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -949,10 +1432,11 @@ def nst_show1(ctx, name, literal): NAME: name or ID of the NST """ + logger.debug("") nst_show(ctx, name, literal) -@cli.command(name='netslice-template-show', short_help='shows the content of a Network Slice Template (NST)') +@cli_osm.command(name='netslice-template-show', short_help='shows the content of a Network Slice Template (NST)') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @click.argument('name') @@ -962,19 +1446,21 @@ def nst_show2(ctx, name, literal): NAME: name or ID of the NST """ + logger.debug("") nst_show(ctx, name, literal) def nsi_show(ctx, name, literal, filter): - try: - check_client_version(ctx.obj, ctx.command.name) - nsi = ctx.obj.nsi.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + nsi = ctx.obj.nsi.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(nsi)) + print(yaml.safe_dump(nsi, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) @@ -987,7 +1473,7 @@ def nsi_show(ctx, name, literal, filter): print(table) -@cli.command(name='nsi-show', short_help='shows the content of a Network Slice Instance (NSI)') +@cli_osm.command(name='nsi-show', short_help='shows the content of a Network Slice Instance (NSI)') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -998,10 +1484,11 @@ def nsi_show1(ctx, name, literal, filter): NAME: name or ID of the Network Slice Instance """ + logger.debug("") nsi_show(ctx, name, literal, filter) -@cli.command(name='netslice-instance-show', short_help='shows the content of a Network Slice Instance (NSI)') +@cli_osm.command(name='netslice-instance-show', short_help='shows the content of a Network Slice Instance (NSI)') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -1012,16 +1499,18 @@ def nsi_show2(ctx, name, literal, filter): NAME: name or ID of the Network Slice Instance """ + logger.debug("") nsi_show(ctx, name, literal, filter) def nsi_op_show(ctx, id, filter): - try: - check_client_version(ctx.obj, ctx.command.name) - op_info = ctx.obj.nsi.get_op(id) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + op_info = ctx.obj.nsi.get_op(id) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['field', 'value']) for k, v in list(op_info.items()): @@ -1031,7 +1520,7 @@ def nsi_op_show(ctx, id, filter): print(table) -@cli.command(name='nsi-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') +@cli_osm.command(name='nsi-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') @click.argument('id') @click.option('--filter', default=None) @click.pass_context @@ -1040,10 +1529,11 @@ def nsi_op_show1(ctx, id, filter): ID: operation identifier """ + logger.debug("") nsi_op_show(ctx, id, filter) -@cli.command(name='netslice-instance-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') +@cli_osm.command(name='netslice-instance-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') @click.argument('id') @click.option('--filter', default=None) @click.pass_context @@ -1052,10 +1542,11 @@ def nsi_op_show2(ctx, id, filter): ID: operation identifier """ + logger.debug("") nsi_op_show(ctx, id, filter) -@cli.command(name='pdu-show', short_help='shows the content of a Physical Deployment Unit (PDU)') +@cli_osm.command(name='pdu-show', short_help='shows the content of a Physical Deployment Unit (PDU)') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -1066,15 +1557,16 @@ def pdu_show(ctx, name, literal, filter): NAME: name or ID of the PDU """ - try: - check_client_version(ctx.obj, ctx.command.name) - pdu = ctx.obj.pdu.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + pdu = ctx.obj.pdu.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) if literal: - print(yaml.safe_dump(pdu)) + print(yaml.safe_dump(pdu, indent=4, default_flow_style=False)) return table = PrettyTable(['field', 'value']) @@ -1091,105 +1583,199 @@ def pdu_show(ctx, name, literal, filter): # CREATE operations #################### -def nsd_create(ctx, filename, overwrite): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nsd.create(filename, overwrite) - except ClientException as e: - print(str(e)) - exit(1) +def nsd_create(ctx, filename, overwrite, skip_charm_build, repo, vendor, version): + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + if repo: + filename = ctx.obj.osmrepo.get_pkg('ns', filename, repo, vendor, version) + ctx.obj.nsd.create(filename, overwrite=overwrite, skip_charm_build=skip_charm_build) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nsd-create', short_help='creates a new NSD/NSpkg') +@cli_osm.command(name='nsd-create', short_help='creates a new NSD/NSpkg') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrite deprecated, use override') +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='The charm will not be compiled, it is assumed to already exist') +@click.option('--repo', default=None, + help='[repository]: Repository name') +@click.option('--vendor', default=None, + help='[repository]: filter by vendor]') +@click.option('--version', default='latest', + help='[repository]: filter by version. Default: latest') @click.pass_context -def nsd_create1(ctx, filename, overwrite): - """creates a new NSD/NSpkg +def nsd_create1(ctx, filename, overwrite, skip_charm_build, repo, vendor, version): + """onboards a new NSpkg (alias of nspkg-create) (TO BE DEPRECATED) - FILENAME: NSD yaml file or NSpkg tar.gz file + \b + FILENAME: NF Package tar.gz file, NF Descriptor YAML file or NF Package folder + If FILENAME is a file (NF Package tar.gz or NF Descriptor YAML), it is onboarded. + If FILENAME is an NF Package folder, it is built and then onboarded. """ - nsd_create(ctx, filename, overwrite) + logger.debug("") + nsd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build, repo=repo, vendor=vendor, + version=version) -@cli.command(name='nspkg-create', short_help='creates a new NSD/NSpkg') +@cli_osm.command(name='nspkg-create', short_help='creates a new NSD/NSpkg') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrite deprecated, use override') +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='The charm will not be compiled, it is assumed to already exist') +@click.option('--repo', default=None, + help='[repository]: Repository name') +@click.option('--vendor', default=None, + help='[repository]: filter by vendor]') +@click.option('--version', default='latest', + help='[repository]: filter by version. Default: latest') @click.pass_context -def nsd_create2(ctx, filename, overwrite): - """creates a new NSD/NSpkg - - FILENAME: NSD yaml file or NSpkg tar.gz file +def nsd_pkg_create(ctx, filename, overwrite, skip_charm_build, repo, vendor, version): + """onboards a new NSpkg + \b + FILENAME: NF Package tar.gz file, NF Descriptor YAML file or NF Package folder + If FILENAME is a file (NF Package tar.gz or NF Descriptor YAML), it is onboarded. + If FILENAME is an NF Package folder, it is built and then onboarded. """ - nsd_create(ctx, filename, overwrite) - - -def vnfd_create(ctx, filename, overwrite): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.vnfd.create(filename, overwrite) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='vnfd-create', short_help='creates a new VNFD/VNFpkg') + logger.debug("") + nsd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build, repo=repo, vendor=vendor, + version=version) + + +def vnfd_create(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt, + repo, vendor, version): + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + if repo: + filename = ctx.obj.osmrepo.get_pkg('vnf', filename, repo, vendor, version) + ctx.obj.vnfd.create(filename, overwrite=overwrite, skip_charm_build=skip_charm_build, + override_epa=override_epa, override_nonepa=override_nonepa, + override_paravirt=override_paravirt) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='vnfd-create', short_help='creates a new VNFD/VNFpkg') @click.argument('filename') @click.option('--overwrite', 'overwrite', default=None, help='overwrite deprecated, use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='The charm will not be compiled, it is assumed to already exist') +@click.option('--override-epa', required=False, default=False, is_flag=True, + help='adds guest-epa parameters to all VDU') +@click.option('--override-nonepa', required=False, default=False, is_flag=True, + help='removes all guest-epa parameters from all VDU') +@click.option('--override-paravirt', required=False, default=False, is_flag=True, + help='overrides all VDU interfaces to PARAVIRT') +@click.option('--repo', default=None, + help='[repository]: Repository name') +@click.option('--vendor', default=None, + help='[repository]: filter by vendor]') +@click.option('--version', default='latest', + help='[repository]: filter by version. Default: latest') @click.pass_context -def vnfd_create1(ctx, filename, overwrite): +def vnfd_create1(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt, + repo,vendor, version): """creates a new VNFD/VNFpkg - - FILENAME: VNFD yaml file or VNFpkg tar.gz file + \b + FILENAME: NF Package tar.gz file, NF Descriptor YAML file or NF Package folder + If FILENAME is a file (NF Package tar.gz or NF Descriptor YAML), it is onboarded. + If FILENAME is an NF Package folder, it is built and then onboarded. """ - vnfd_create(ctx, filename, overwrite) + logger.debug("") + vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build, + override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt, + repo=repo, vendor=vendor, version=version) -@cli.command(name='vnfpkg-create', short_help='creates a new VNFD/VNFpkg') +@cli_osm.command(name='vnfpkg-create', short_help='creates a new VNFD/VNFpkg') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrite deprecated, use override') +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='The charm will not be compiled, it is assumed to already exist') +@click.option('--override-epa', required=False, default=False, is_flag=True, + help='adds guest-epa parameters to all VDU') +@click.option('--override-nonepa', required=False, default=False, is_flag=True, + help='removes all guest-epa parameters from all VDU') +@click.option('--override-paravirt', required=False, default=False, is_flag=True, + help='overrides all VDU interfaces to PARAVIRT') +@click.option('--repo', default=None, + help='[repository]: Repository name') +@click.option('--vendor', default=None, + help='[repository]: filter by vendor]') +@click.option('--version', default='latest', + help='[repository]: filter by version. Default: latest') @click.pass_context -def vnfd_create2(ctx, filename, overwrite): +def vnfd_create2(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt, + repo, vendor, version): """creates a new VNFD/VNFpkg - - FILENAME: VNFD yaml file or VNFpkg tar.gz file + \b + FILENAME: NF Package tar.gz file, NF Descriptor YAML file or NF Package folder + If FILENAME is a file (NF Package tar.gz or NF Descriptor YAML), it is onboarded. + If FILENAME is an NF Package folder, it is built and then onboarded. """ - vnfd_create(ctx, filename, overwrite) - + logger.debug("") + vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build, + override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt, + repo=repo, vendor=vendor, version=version) -@cli.command(name='nfpkg-create', short_help='creates a new NFpkg') +@cli_osm.command(name='nfpkg-create', short_help='creates a new NFpkg') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrite deprecated, use override') +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='The charm will not be compiled, it is assumed to already exist') +@click.option('--override-epa', required=False, default=False, is_flag=True, + help='adds guest-epa parameters to all VDU') +@click.option('--override-nonepa', required=False, default=False, is_flag=True, + help='removes all guest-epa parameters from all VDU') +@click.option('--override-paravirt', required=False, default=False, is_flag=True, + help='overrides all VDU interfaces to PARAVIRT') +@click.option('--repo', default=None, + help='[repository]: Repository name') +@click.option('--vendor', default=None, + help='[repository]: filter by vendor]') +@click.option('--version', default='latest', + help='[repository]: filter by version. Default: latest') @click.pass_context -def nfpkg_create(ctx, filename, overwrite): +def nfpkg_create(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt, + repo, vendor, version): """creates a new NFpkg - FILENAME: NF Descriptor yaml file or NFpkg tar.gz file + \b + FILENAME: NF Package tar.gz file, NF Descriptor YAML file or NF Package folder + If FILENAME is a file (NF Package tar.gz or NF Descriptor YAML), it is onboarded. + If FILENAME is an NF Package folder, it is built and then onboarded. """ - vnfd_create(ctx, filename, overwrite) + logger.debug("") + vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build, + override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt, + repo=repo, vendor=vendor, version=version) -@cli.command(name='ns-create', short_help='creates a new Network Service instance') +@cli_osm.command(name='ns-create', short_help='creates a new Network Service instance') @click.option('--ns_name', prompt=True, help='name of the NS instance') @click.option('--nsd_name', @@ -1212,8 +1798,8 @@ def nfpkg_create(ctx, filename, overwrite): required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def ns_create(ctx, nsd_name, @@ -1225,54 +1811,57 @@ def ns_create(ctx, config_file, wait): """creates a new NS instance""" - try: - if config_file: - check_client_version(ctx.obj, '--config_file') - if config: - raise ClientException('"--config" option is incompatible with "--config_file" option') - with open(config_file, 'r') as cf: - config=cf.read() - ctx.obj.ns.create( - nsd_name, - ns_name, - config=config, - ssh_keys=ssh_keys, - account=vim_account, - wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if config_file: + check_client_version(ctx.obj, '--config_file') + if config: + raise ClientException('"--config" option is incompatible with "--config_file" option') + with open(config_file, 'r') as cf: + config=cf.read() + ctx.obj.ns.create( + nsd_name, + ns_name, + config=config, + ssh_keys=ssh_keys, + account=vim_account, + wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) def nst_create(ctx, filename, overwrite): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nst.create(filename, overwrite) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.create(filename, overwrite) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nst-create', short_help='creates a new Network Slice Template (NST)') +@cli_osm.command(name='nst-create', short_help='creates a new Network Slice Template (NST)') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrites deprecated use override') -@click.option('--override', 'overwrite' ,default=None, +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') +@click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') @click.pass_context def nst_create1(ctx, filename, overwrite): """creates a new Network Slice Template (NST) - FILENAME: NST yaml file or NSTpkg tar.gz file + FILENAME: NST package folder, NST yaml file or NSTpkg tar.gz file """ + logger.debug("") nst_create(ctx, filename, overwrite) -@cli.command(name='netslice-template-create', short_help='creates a new Network Slice Template (NST)') +@cli_osm.command(name='netslice-template-create', short_help='creates a new Network Slice Template (NST)') @click.argument('filename') -@click.option('--overwrite', 'overwrite', default=None, - help='overwrites deprecated use override') +@click.option('--overwrite', 'overwrite', default=None, # hidden=True, + help='Deprecated. Use override') @click.option('--override', 'overwrite', default=None, help='overrides fields in descriptor, format: ' '"key1.key2...=value[;key3...=value;...]"') @@ -1282,26 +1871,28 @@ def nst_create2(ctx, filename, overwrite): FILENAME: NST yaml file or NSTpkg tar.gz file """ + logger.debug("") nst_create(ctx, filename, overwrite) def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" - try: - check_client_version(ctx.obj, ctx.command.name) - if config_file: - if config: - raise ClientException('"--config" option is incompatible with "--config_file" option') - with open(config_file, 'r') as cf: - config=cf.read() - ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, - account=vim_account, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='nsi-create', short_help='creates a new Network Slice Instance') + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + if config_file: + if config: + raise ClientException('"--config" option is incompatible with "--config_file" option') + with open(config_file, 'r') as cf: + config=cf.read() + ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, + account=vim_account, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='nsi-create', short_help='creates a new Network Slice Instance') @click.option('--nsi_name', prompt=True, help='name of the Network Slice Instance') @click.option('--nst_name', prompt=True, help='name of the Network Slice Template') @click.option('--vim_account', prompt=True, help='default VIM account id or name for the deployment') @@ -1325,15 +1916,16 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_fi required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" + logger.debug("") nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait=wait) -@cli.command(name='netslice-instance-create', short_help='creates a new Network Slice Instance') +@cli_osm.command(name='netslice-instance-create', short_help='creates a new Network Slice Instance') @click.option('--nsi_name', prompt=True, help='name of the Network Slice Instance') @click.option('--nst_name', prompt=True, help='name of the Network Slice Template') @click.option('--vim_account', prompt=True, help='default VIM account id or name for the deployment') @@ -1355,15 +1947,16 @@ def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_f required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" + logger.debug("") nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait=wait) -@cli.command(name='pdu-create', short_help='adds a new Physical Deployment Unit to the catalog') +@cli_osm.command(name='pdu-create', short_help='adds a new Physical Deployment Unit to the catalog') @click.option('--name', help='name of the Physical Deployment Unit') @click.option('--pdu_type', help='type of PDU (e.g. router, firewall, FW001)') @click.option('--interface', @@ -1372,55 +1965,59 @@ def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_f multiple=True) @click.option('--description', help='human readable description') @click.option('--vim_account', help='list of VIM accounts (in the same VIM) that can reach this PDU', multiple=True) -@click.option('--descriptor_file', default=None, help='PDU descriptor file (as an alternative to using the other arguments') +@click.option('--descriptor_file', default=None, + help='PDU descriptor file (as an alternative to using the other arguments') @click.pass_context def pdu_create(ctx, name, pdu_type, interface, description, vim_account, descriptor_file): """creates a new Physical Deployment Unit (PDU)""" - try: - check_client_version(ctx.obj, ctx.command.name) - pdu = {} - if not descriptor_file: - if not name: - raise ClientException('in absence of descriptor file, option "--name" is mandatory') - if not pdu_type: - raise ClientException('in absence of descriptor file, option "--pdu_type" is mandatory') - if not interface: - raise ClientException('in absence of descriptor file, option "--interface" is mandatory (at least once)') - if not vim_account: - raise ClientException('in absence of descriptor file, option "--vim_account" is mandatory (at least once)') - else: - with open(descriptor_file, 'r') as df: - pdu = yaml.safe_load(df.read()) - if name: pdu["name"] = name - if pdu_type: pdu["type"] = pdu_type - if description: pdu["description"] = description - if vim_account: pdu["vim_accounts"] = vim_account - if interface: - ifaces_list = [] - for iface in interface: - new_iface={k:v for k,v in [i.split('=') for i in iface.split(',')]} - new_iface["mgmt"] = (new_iface.get("mgmt","false").lower() == "true") - ifaces_list.append(new_iface) - pdu["interfaces"] = ifaces_list - ctx.obj.pdu.create(pdu) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + pdu = {} + if not descriptor_file: + if not name: + raise ClientException('in absence of descriptor file, option "--name" is mandatory') + if not pdu_type: + raise ClientException('in absence of descriptor file, option "--pdu_type" is mandatory') + if not interface: + raise ClientException('in absence of descriptor file, option "--interface" is mandatory (at least once)') + if not vim_account: + raise ClientException('in absence of descriptor file, option "--vim_account" is mandatory (at least once)') + else: + with open(descriptor_file, 'r') as df: + pdu = yaml.safe_load(df.read()) + if name: pdu["name"] = name + if pdu_type: pdu["type"] = pdu_type + if description: pdu["description"] = description + if vim_account: pdu["vim_accounts"] = vim_account + if interface: + ifaces_list = [] + for iface in interface: + new_iface={k:v for k,v in [i.split('=') for i in iface.split(',')]} + new_iface["mgmt"] = (new_iface.get("mgmt","false").lower() == "true") + ifaces_list.append(new_iface) + pdu["interfaces"] = ifaces_list + ctx.obj.pdu.create(pdu) + # except ClientException as e: + # print(str(e)) + # exit(1) + #################### # UPDATE operations #################### def nsd_update(ctx, name, content): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nsd.update(name, content) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsd.update(name, content) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nsd-update', short_help='updates a NSD/NSpkg') +@cli_osm.command(name='nsd-update', short_help='updates a NSD/NSpkg') @click.argument('name') @click.option('--content', default=None, help='filename with the NSD/NSpkg replacing the current one') @@ -1430,10 +2027,11 @@ def nsd_update1(ctx, name, content): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nsd_update(ctx, name, content) -@cli.command(name='nspkg-update', short_help='updates a NSD/NSpkg') +@cli_osm.command(name='nspkg-update', short_help='updates a NSD/NSpkg') @click.argument('name') @click.option('--content', default=None, help='filename with the NSD/NSpkg replacing the current one') @@ -1443,19 +2041,21 @@ def nsd_update2(ctx, name, content): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nsd_update(ctx, name, content) def vnfd_update(ctx, name, content): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.vnfd.update(name, content) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.vnfd.update(name, content) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='vnfd-update', short_help='updates a new VNFD/VNFpkg') +@cli_osm.command(name='vnfd-update', short_help='updates a new VNFD/VNFpkg') @click.argument('name') @click.option('--content', default=None, help='filename with the VNFD/VNFpkg replacing the current one') @@ -1465,10 +2065,11 @@ def vnfd_update1(ctx, name, content): NAME: name or ID of the VNFD/VNFpkg """ + logger.debug("") vnfd_update(ctx, name, content) -@cli.command(name='vnfpkg-update', short_help='updates a VNFD/VNFpkg') +@cli_osm.command(name='vnfpkg-update', short_help='updates a VNFD/VNFpkg') @click.argument('name') @click.option('--content', default=None, help='filename with the VNFD/VNFpkg replacing the current one') @@ -1478,10 +2079,11 @@ def vnfd_update2(ctx, name, content): NAME: VNFD yaml file or VNFpkg tar.gz file """ + logger.debug("") vnfd_update(ctx, name, content) -@cli.command(name='nfpkg-update', short_help='updates a NFpkg') +@cli_osm.command(name='nfpkg-update', short_help='updates a NFpkg') @click.argument('name') @click.option('--content', default=None, help='filename with the NFpkg replacing the current one') @@ -1491,19 +2093,21 @@ def nfpkg_update(ctx, name, content): NAME: NF Descriptor yaml file or NFpkg tar.gz file """ + logger.debug("") vnfd_update(ctx, name, content) def nst_update(ctx, name, content): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nst.update(name, content) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.update(name, content) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nst-update', short_help='updates a Network Slice Template (NST)') +@cli_osm.command(name='nst-update', short_help='updates a Network Slice Template (NST)') @click.argument('name') @click.option('--content', default=None, help='filename with the NST/NSTpkg replacing the current one') @@ -1513,10 +2117,11 @@ def nst_update1(ctx, name, content): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nst_update(ctx, name, content) -@cli.command(name='netslice-template-update', short_help='updates a Network Slice Template (NST)') +@cli_osm.command(name='netslice-template-update', short_help='updates a Network Slice Template (NST)') @click.argument('name') @click.option('--content', default=None, help='filename with the NST/NSTpkg replacing the current one') @@ -1526,6 +2131,7 @@ def nst_update2(ctx, name, content): NAME: name or ID of the NSD/NSpkg """ + logger.debug("") nst_update(ctx, name, content) @@ -1534,18 +2140,19 @@ def nst_update2(ctx, name, content): #################### def nsd_delete(ctx, name, force): - try: - if not force: - ctx.obj.nsd.delete(name) - else: - check_client_version(ctx.obj, '--force') - ctx.obj.nsd.delete(name, force) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if not force: + ctx.obj.nsd.delete(name) + else: + check_client_version(ctx.obj, '--force') + ctx.obj.nsd.delete(name, force) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nsd-delete', short_help='deletes a NSD/NSpkg') +@cli_osm.command(name='nsd-delete', short_help='deletes a NSD/NSpkg') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1554,10 +2161,11 @@ def nsd_delete1(ctx, name, force): NAME: name or ID of the NSD/NSpkg to be deleted """ + logger.debug("") nsd_delete(ctx, name, force) -@cli.command(name='nspkg-delete', short_help='deletes a NSD/NSpkg') +@cli_osm.command(name='nspkg-delete', short_help='deletes a NSD/NSpkg') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1566,22 +2174,24 @@ def nsd_delete2(ctx, name, force): NAME: name or ID of the NSD/NSpkg to be deleted """ + logger.debug("") nsd_delete(ctx, name, force) def vnfd_delete(ctx, name, force): - try: - if not force: - ctx.obj.vnfd.delete(name) - else: - check_client_version(ctx.obj, '--force') - ctx.obj.vnfd.delete(name, force) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if not force: + ctx.obj.vnfd.delete(name) + else: + check_client_version(ctx.obj, '--force') + ctx.obj.vnfd.delete(name, force) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='vnfd-delete', short_help='deletes a VNFD/VNFpkg') +@cli_osm.command(name='vnfd-delete', short_help='deletes a VNFD/VNFpkg') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1590,10 +2200,11 @@ def vnfd_delete1(ctx, name, force): NAME: name or ID of the VNFD/VNFpkg to be deleted """ + logger.debug("") vnfd_delete(ctx, name, force) -@cli.command(name='vnfpkg-delete', short_help='deletes a VNFD/VNFpkg') +@cli_osm.command(name='vnfpkg-delete', short_help='deletes a VNFD/VNFpkg') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1602,10 +2213,11 @@ def vnfd_delete2(ctx, name, force): NAME: name or ID of the VNFD/VNFpkg to be deleted """ + logger.debug("") vnfd_delete(ctx, name, force) -@cli.command(name='nfpkg-delete', short_help='deletes a NFpkg') +@cli_osm.command(name='nfpkg-delete', short_help='deletes a NFpkg') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1614,45 +2226,51 @@ def nfpkg_delete(ctx, name, force): NAME: name or ID of the NFpkg to be deleted """ + logger.debug("") vnfd_delete(ctx, name, force) -@cli.command(name='ns-delete', short_help='deletes a NS instance') +@cli_osm.command(name='ns-delete', short_help='deletes a NS instance') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.option('--config', default=None, + help="specific yaml configuration for the termination, e.g. '{autoremove: False, timeout_ns_terminate: " + "600, skip_terminate_primitives: True}'") @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context -def ns_delete(ctx, name, force, wait): +def ns_delete(ctx, name, force, config, wait): """deletes a NS instance NAME: name or ID of the NS instance to be deleted """ - try: - if not force: - ctx.obj.ns.delete(name, wait=wait) - else: - check_client_version(ctx.obj, '--force') - ctx.obj.ns.delete(name, force, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if not force: + ctx.obj.ns.delete(name, config=config, wait=wait) + else: + check_client_version(ctx.obj, '--force') + ctx.obj.ns.delete(name, force, config=config, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) def nst_delete(ctx, name, force): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nst.delete(name, force) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.delete(name, force) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nst-delete', short_help='deletes a Network Slice Template (NST)') +@cli_osm.command(name='nst-delete', short_help='deletes a Network Slice Template (NST)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1661,10 +2279,11 @@ def nst_delete1(ctx, name, force): NAME: name or ID of the NST/NSTpkg to be deleted """ + logger.debug("") nst_delete(ctx, name, force) -@cli.command(name='netslice-template-delete', short_help='deletes a Network Slice Template (NST)') +@cli_osm.command(name='netslice-template-delete', short_help='deletes a Network Slice Template (NST)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1673,37 +2292,40 @@ def nst_delete2(ctx, name, force): NAME: name or ID of the NST/NSTpkg to be deleted """ + logger.debug("") nst_delete(ctx, name, force) def nsi_delete(ctx, name, force, wait): - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nsi.delete(name, force, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsi.delete(name, force, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='nsi-delete', short_help='deletes a Network Slice Instance (NSI)') +@cli_osm.command(name='nsi-delete', short_help='deletes a Network Slice Instance (NSI)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def nsi_delete1(ctx, name, force, wait): """deletes a Network Slice Instance (NSI) NAME: name or ID of the Network Slice instance to be deleted """ + logger.debug("") nsi_delete(ctx, name, force, wait=wait) -@cli.command(name='netslice-instance-delete', short_help='deletes a Network Slice Instance (NSI)') +@cli_osm.command(name='netslice-instance-delete', short_help='deletes a Network Slice Instance (NSI)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1712,10 +2334,11 @@ def nsi_delete2(ctx, name, force, wait): NAME: name or ID of the Network Slice instance to be deleted """ + logger.debug("") nsi_delete(ctx, name, force, wait=wait) -@cli.command(name='pdu-delete', short_help='deletes a Physical Deployment Unit (PDU)') +@cli_osm.command(name='pdu-delete', short_help='deletes a Physical Deployment Unit (PDU)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -1724,19 +2347,20 @@ def pdu_delete(ctx, name, force): NAME: name or ID of the PDU to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.pdu.delete(name, force) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.pdu.delete(name, force) + # except ClientException as e: + # print(str(e)) + # exit(1) ################# # VIM operations ################# -@cli.command(name='vim-create', short_help='creates a new VIM account') +@cli_osm.command(name='vim-create', short_help='creates a new VIM account') @click.option('--name', prompt=True, help='Name to create datacenter') @@ -1761,7 +2385,7 @@ def pdu_delete(ctx, name, force): default='openstack', help='VIM type') @click.option('--description', - default='no description', + default=None, help='human readable description') @click.option('--sdn_controller', default=None, help='Name or id of the SDN controller associated to this VIM account') @click.option('--sdn_port_mapping', default=None, help="File describing the port mapping between compute nodes' ports and switch ports") @@ -1769,8 +2393,8 @@ def pdu_delete(ctx, name, force): required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def vim_create(ctx, name, @@ -1785,29 +2409,30 @@ def vim_create(ctx, sdn_port_mapping, wait): """creates a new VIM account""" - try: - if sdn_controller: - check_client_version(ctx.obj, '--sdn_controller') - if sdn_port_mapping: - check_client_version(ctx.obj, '--sdn_port_mapping') - vim = {} - vim['vim-username'] = user - vim['vim-password'] = password - vim['vim-url'] = auth_url - vim['vim-tenant-name'] = tenant - vim['vim-type'] = account_type - vim['description'] = description - vim['config'] = config - if sdn_controller or sdn_port_mapping: - ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait) - else: - ctx.obj.vim.create(name, vim, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if sdn_controller: + check_client_version(ctx.obj, '--sdn_controller') + if sdn_port_mapping: + check_client_version(ctx.obj, '--sdn_port_mapping') + vim = {} + vim['vim-username'] = user + vim['vim-password'] = password + vim['vim-url'] = auth_url + vim['vim-tenant-name'] = tenant + vim['vim-type'] = account_type + vim['description'] = description + vim['config'] = config + if sdn_controller or sdn_port_mapping: + ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait) + else: + ctx.obj.vim.create(name, vim, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='vim-update', short_help='updates a VIM account') +@cli_osm.command(name='vim-update', short_help='updates a VIM account') @click.argument('name') @click.option('--newname', help='New name for the VIM account') @click.option('--user', help='VIM username') @@ -1817,14 +2442,15 @@ def vim_create(ctx, @click.option('--config', help='VIM specific config parameters') @click.option('--account_type', help='VIM type') @click.option('--description', help='human readable description') -@click.option('--sdn_controller', default=None, help='Name or id of the SDN controller associated to this VIM account') +@click.option('--sdn_controller', default=None, help='Name or id of the SDN controller to be associated with this VIM' + 'account. Use empty string to disassociate') @click.option('--sdn_port_mapping', default=None, help="File describing the port mapping between compute nodes' ports and switch ports") @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def vim_update(ctx, name, @@ -1843,58 +2469,63 @@ def vim_update(ctx, NAME: name or ID of the VIM account """ - try: - check_client_version(ctx.obj, ctx.command.name) - vim = {} - if newname: vim['name'] = newname - if user: vim['vim_user'] = user - if password: vim['vim_password'] = password - if auth_url: vim['vim_url'] = auth_url - if tenant: vim['vim-tenant-name'] = tenant - if account_type: vim['vim_type'] = account_type - if description: vim['description'] = description - if config: vim['config'] = config - ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='vim-delete', short_help='deletes a VIM account') + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + vim = {} + if newname: vim['name'] = newname + if user: vim['vim_user'] = user + if password: vim['vim_password'] = password + if auth_url: vim['vim_url'] = auth_url + if tenant: vim['vim-tenant-name'] = tenant + if account_type: vim['vim_type'] = account_type + if description: vim['description'] = description + if config: vim['config'] = config + ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='vim-delete', short_help='deletes a VIM account') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def vim_delete(ctx, name, force, wait): """deletes a VIM account NAME: name or ID of the VIM account to be deleted """ - try: - if not force: - ctx.obj.vim.delete(name, wait=wait) - else: - check_client_version(ctx.obj, '--force') - ctx.obj.vim.delete(name, force, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + if not force: + ctx.obj.vim.delete(name, wait=wait) + else: + check_client_version(ctx.obj, '--force') + ctx.obj.vim.delete(name, force, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='vim-list', short_help='list all VIM accounts') +@cli_osm.command(name='vim-list', short_help='list all VIM accounts') #@click.option('--ro_update/--no_ro_update', # default=False, # help='update list from RO') @click.option('--filter', default=None, help='restricts the list to the VIM accounts matching the filter') +@click.option('--long', is_flag=True, + help='get more details of the NS (project, vim, deployment status, configuration status.') @click.pass_context -def vim_list(ctx, filter): +def vim_list(ctx, filter, long): """list all VIM accounts""" + logger.debug("") if filter: check_client_version(ctx.obj, '--filter') # if ro_update: @@ -1904,14 +2535,39 @@ def vim_list(ctx, filter): resp = ctx.obj.vim.list(filter) # else: # resp = ctx.obj.vim.list(ro_update) - table = PrettyTable(['vim name', 'uuid']) + if long: + table = PrettyTable(['vim name', 'uuid', 'project', 'operational state', 'error details']) + else: + table = PrettyTable(['vim name', 'uuid', 'operational state']) for vim in resp: - table.add_row([vim['name'], vim['uuid']]) + if long: + vim_details = ctx.obj.vim.get(vim['uuid']) + if 'vim_password' in vim_details: + vim_details['vim_password']='********' + logger.debug('VIM details: {}'.format(yaml.safe_dump(vim_details))) + vim_state = vim_details['_admin'].get('operationalState', '-') + error_details = 'N/A' + if vim_state == 'ERROR': + error_details = vim_details['_admin'].get('detailed-status', 'Not found') + project_list = ctx.obj.project.list() + vim_project_list = vim_details.get('_admin').get('projects_read') + project_id = 'None' + project_name = 'None' + if vim_project_list: + project_id = vim_project_list[0] + for p in project_list: + if p['_id'] == project_id: + project_name = p['name'] + break + table.add_row([vim['name'], vim['uuid'], '{} ({})'.format(project_name, project_id), + vim_state, wrap_text(text=error_details, width=80)]) + else: + table.add_row([vim['name'], vim['uuid'], vim['_admin'].get('operationalState', '-')]) table.align = 'l' print(table) -@cli.command(name='vim-show', short_help='shows the details of a VIM account') +@cli_osm.command(name='vim-show', short_help='shows the details of a VIM account') @click.argument('name') @click.pass_context def vim_show(ctx, name): @@ -1919,13 +2575,14 @@ def vim_show(ctx, name): NAME: name or ID of the VIM account """ - try: - resp = ctx.obj.vim.get(name) - if 'vim_password' in resp: - resp['vim_password']='********' - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + resp = ctx.obj.vim.get(name) + if 'vim_password' in resp: + resp['vim_password']='********' + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in list(resp.items()): @@ -1938,7 +2595,7 @@ def vim_show(ctx, name): # WIM operations #################### -@cli.command(name='wim-create', short_help='creates a new WIM account') +@cli_osm.command(name='wim-create', short_help='creates a new WIM account') @click.option('--name', prompt=True, help='Name for the WIM account') @@ -1957,15 +2614,17 @@ def vim_show(ctx, name): @click.option('--wim_type', help='WIM type') @click.option('--description', - default='no description', + default=None, help='human readable description') -@click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@click.option('--wim_port_mapping', default=None, + help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge " + "(WAN service endpoint id and info)") @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it ' + 'until the operation is completed, or timeout') @click.pass_context def wim_create(ctx, name, @@ -1979,27 +2638,28 @@ def wim_create(ctx, wim_port_mapping, wait): """creates a new WIM account""" - try: - check_client_version(ctx.obj, ctx.command.name) - # if sdn_controller: - # check_client_version(ctx.obj, '--sdn_controller') - # if sdn_port_mapping: - # check_client_version(ctx.obj, '--sdn_port_mapping') - wim = {} - if user: wim['user'] = user - if password: wim['password'] = password - if url: wim['wim_url'] = url - # if tenant: wim['tenant'] = tenant - wim['wim_type'] = wim_type - if description: wim['description'] = description - if config: wim['config'] = config - ctx.obj.wim.create(name, wim, wim_port_mapping, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='wim-update', short_help='updates a WIM account') + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + # if sdn_controller: + # check_client_version(ctx.obj, '--sdn_controller') + # if sdn_port_mapping: + # check_client_version(ctx.obj, '--sdn_port_mapping') + wim = {} + if user: wim['user'] = user + if password: wim['password'] = password + if url: wim['wim_url'] = url + # if tenant: wim['tenant'] = tenant + wim['wim_type'] = wim_type + if description: wim['description'] = description + if config: wim['config'] = config + ctx.obj.wim.create(name, wim, wim_port_mapping, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='wim-update', short_help='updates a WIM account') @click.argument('name') @click.option('--newname', help='New name for the WIM account') @click.option('--user', help='WIM username') @@ -2008,13 +2668,14 @@ def wim_create(ctx, @click.option('--config', help='WIM specific config parameters') @click.option('--wim_type', help='WIM type') @click.option('--description', help='human readable description') -@click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@click.option('--wim_port_mapping', default=None, + help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge " + "(WAN service endpoint id and info)") @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def wim_update(ctx, name, @@ -2031,66 +2692,68 @@ def wim_update(ctx, NAME: name or ID of the WIM account """ - try: - check_client_version(ctx.obj, ctx.command.name) - wim = {} - if newname: wim['name'] = newname - if user: wim['user'] = user - if password: wim['password'] = password - if url: wim['url'] = url - # if tenant: wim['tenant'] = tenant - if wim_type: wim['wim_type'] = wim_type - if description: wim['description'] = description - if config: wim['config'] = config - ctx.obj.wim.update(name, wim, wim_port_mapping, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='wim-delete', short_help='deletes a WIM account') + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + wim = {} + if newname: wim['name'] = newname + if user: wim['user'] = user + if password: wim['password'] = password + if url: wim['url'] = url + # if tenant: wim['tenant'] = tenant + if wim_type: wim['wim_type'] = wim_type + if description: wim['description'] = description + if config: wim['config'] = config + ctx.obj.wim.update(name, wim, wim_port_mapping, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='wim-delete', short_help='deletes a WIM account') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def wim_delete(ctx, name, force, wait): """deletes a WIM account NAME: name or ID of the WIM account to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.wim.delete(name, force, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.wim.delete(name, force, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='wim-list', short_help='list all WIM accounts') +@cli_osm.command(name='wim-list', short_help='list all WIM accounts') @click.option('--filter', default=None, help='restricts the list to the WIM accounts matching the filter') @click.pass_context def wim_list(ctx, filter): """list all WIM accounts""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.wim.list(filter) - table = PrettyTable(['wim name', 'uuid']) - for wim in resp: - table.add_row([wim['name'], wim['uuid']]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.wim.list(filter) + table = PrettyTable(['wim name', 'uuid']) + for wim in resp: + table.add_row([wim['name'], wim['uuid']]) + table.align = 'l' + print(table) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='wim-show', short_help='shows the details of a WIM account') +@cli_osm.command(name='wim-show', short_help='shows the details of a WIM account') @click.argument('name') @click.pass_context def wim_show(ctx, name): @@ -2098,14 +2761,15 @@ def wim_show(ctx, name): NAME: name or ID of the WIM account """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.wim.get(name) - if 'password' in resp: - resp['wim_password']='********' - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.wim.get(name) + if 'password' in resp: + resp['wim_password']='********' + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in list(resp.items()): @@ -2118,169 +2782,145 @@ def wim_show(ctx, name): # SDN controller operations #################### -@cli.command(name='sdnc-create', short_help='creates a new SDN controller') +@cli_osm.command(name='sdnc-create', short_help='creates a new SDN controller') @click.option('--name', prompt=True, help='Name to create sdn controller') @click.option('--type', prompt=True, help='SDN controller type') -@click.option('--sdn_controller_version', - help='SDN controller version') -@click.option('--ip_address', - prompt=True, - help='SDN controller IP address') -@click.option('--port', - prompt=True, - help='SDN controller port') -@click.option('--switch_dpid', - prompt=True, - help='Switch DPID (Openflow Datapath ID)') +@click.option('--sdn_controller_version', # hidden=True, + help='Deprecated. Use --config {version: sdn_controller_version}') +@click.option('--url', + help='URL in format http[s]://HOST:IP/') +@click.option('--ip_address', # hidden=True, + help='Deprecated. Use --url') +@click.option('--port', # hidden=True, + help='Deprecated. Use --url') +@click.option('--switch_dpid', # hidden=True, + help='Deprecated. Use --config {switch_id: DPID}') +@click.option('--config', + help='Extra information for SDN in yaml format, as {switch_id: identity used for the plugin (e.g. DPID: ' + 'Openflow Datapath ID), version: version}') @click.option('--user', help='SDN controller username') @click.option('--password', hide_input=True, confirmation_prompt=True, help='SDN controller password') -#@click.option('--description', -# default='no description', -# help='human readable description') +@click.option('--description', default=None, help='human readable description') @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') -@click.pass_context -def sdnc_create(ctx, - name, - type, - sdn_controller_version, - ip_address, - port, - switch_dpid, - user, - password, - wait): + help="do not return the control immediately, but keep it until the operation is completed, or timeout") +@click.pass_context +def sdnc_create(ctx, **kwargs): """creates a new SDN controller""" - sdncontroller = {} - sdncontroller['name'] = name - sdncontroller['type'] = type - sdncontroller['ip'] = ip_address - sdncontroller['port'] = int(port) - sdncontroller['dpid'] = switch_dpid - if sdn_controller_version: - sdncontroller['version'] = sdn_controller_version - if user: - sdncontroller['user'] = user - if password: - sdncontroller['password'] = password -# sdncontroller['description'] = description - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.create(name, sdncontroller, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - -@cli.command(name='sdnc-update', short_help='updates an SDN controller') + logger.debug("") + sdncontroller = {x: kwargs[x] for x in kwargs if kwargs[x] and + x not in ("wait", "ip_address", "port", "switch_dpid")} + if kwargs.get("port"): + print("option '--port' is deprecated, use '--url' instead") + sdncontroller["port"] = int(kwargs["port"]) + if kwargs.get("ip_address"): + print("option '--ip_address' is deprecated, use '--url' instead") + sdncontroller["ip"] = kwargs["ip_address"] + if kwargs.get("switch_dpid"): + print("option '--switch_dpid' is deprecated, use '--config={switch_id: id|DPID}' instead") + sdncontroller["dpid"] = kwargs["switch_dpid"] + if kwargs.get("sdn_controller_version"): + print("option '--sdn_controller_version' is deprecated, use '--config={version: SDN_CONTROLLER_VERSION}'" + " instead") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.sdnc.create(kwargs["name"], sdncontroller, wait=kwargs["wait"]) + # except ClientException as e: + # print(str(e)) + # exit(1) + +@cli_osm.command(name='sdnc-update', short_help='updates an SDN controller') @click.argument('name') @click.option('--newname', help='New name for the SDN controller') +@click.option('--description', default=None, help='human readable description') @click.option('--type', help='SDN controller type') -@click.option('--sdn_controller_version', help='SDN controller username') -@click.option('--ip_address', help='SDN controller IP address') -@click.option('--port', help='SDN controller port') -@click.option('--switch_dpid', help='Switch DPID (Openflow Datapath ID)') +@click.option('--url', help='URL in format http[s]://HOST:IP/') +@click.option('--config', help='Extra information for SDN in yaml format, as ' + '{switch_id: identity used for the plugin (e.g. DPID: ' + 'Openflow Datapath ID), version: version}') @click.option('--user', help='SDN controller username') @click.option('--password', help='SDN controller password') -#@click.option('--description', default=None, help='human readable description') -@click.option('--wait', - required=False, - default=False, - is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') -@click.pass_context -def sdnc_update(ctx, - name, - newname, - type, - sdn_controller_version, - ip_address, - port, - switch_dpid, - user, - password, - wait): +@click.option('--ip_address', help='Deprecated. Use --url') # hidden=True +@click.option('--port', help='Deprecated. Use --url') # hidden=True +@click.option('--switch_dpid', help='Deprecated. Use --config {switch_dpid: DPID}') # hidden=True +@click.option('--sdn_controller_version', help='Deprecated. Use --config {version: VERSION}') # hidden=True +@click.option('--wait', required=False, default=False, is_flag=True, + help='do not return the control immediately, but keep it until the operation is completed, or timeout') +@click.pass_context +def sdnc_update(ctx, **kwargs): """updates an SDN controller NAME: name or ID of the SDN controller """ - sdncontroller = {} - if newname: sdncontroller['name'] = newname - if type: sdncontroller['type'] = type - if ip_address: sdncontroller['ip'] = ip_address - if port: sdncontroller['port'] = int(port) - if switch_dpid: sdncontroller['dpid'] = switch_dpid -# sdncontroller['description'] = description - if sdn_controller_version is not None: - if sdn_controller_version=="": - sdncontroller['version'] = None - else: - sdncontroller['version'] = sdn_controller_version - if user is not None: - if user=="": - sdncontroller['user'] = None - else: - sdncontroller['user'] = user - if password is not None: - if password=="": - sdncontroller['password'] = None - else: - sdncontroller['password'] = user - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.update(name, sdncontroller, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='sdnc-delete', short_help='deletes an SDN controller') + logger.debug("") + sdncontroller = {x: kwargs[x] for x in kwargs if kwargs[x] and + x not in ("wait", "ip_address", "port", "switch_dpid", "new_name")} + if kwargs.get("newname"): + sdncontroller["name"] = kwargs["newname"] + if kwargs.get("port"): + print("option '--port' is deprecated, use '--url' instead") + sdncontroller["port"] = int(kwargs["port"]) + if kwargs.get("ip_address"): + print("option '--ip_address' is deprecated, use '--url' instead") + sdncontroller["ip"] = kwargs["ip_address"] + if kwargs.get("switch_dpid"): + print("option '--switch_dpid' is deprecated, use '--config={switch_id: id|DPID}' instead") + sdncontroller["dpid"] = kwargs["switch_dpid"] + if kwargs.get("sdn_controller_version"): + print("option '--sdn_controller_version' is deprecated, use '---config={version: SDN_CONTROLLER_VERSION}'" + " instead") + + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.sdnc.update(kwargs["name"], sdncontroller, wait=kwargs["wait"]) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='sdnc-delete', short_help='deletes an SDN controller') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') -@click.option('--wait', - required=False, - default=False, - is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') +@click.option('--wait', required=False, default=False, is_flag=True, + help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def sdnc_delete(ctx, name, force, wait): """deletes an SDN controller NAME: name or ID of the SDN controller to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.delete(name, force, wait=wait) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.sdnc.delete(name, force, wait=wait) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='sdnc-list', short_help='list all SDN controllers') +@cli_osm.command(name='sdnc-list', short_help='list all SDN controllers') @click.option('--filter', default=None, - help='restricts the list to the SDN controllers matching the filter') + help="restricts the list to the SDN controllers matching the filter with format: 'k[.k..]=v[&k[.k]=v2]'") @click.pass_context def sdnc_list(ctx, filter): """list all SDN controllers""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.sdnc.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.sdnc.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['sdnc name', 'id']) for sdnc in resp: table.add_row([sdnc['name'], sdnc['_id']]) @@ -2288,7 +2928,7 @@ def sdnc_list(ctx, filter): print(table) -@cli.command(name='sdnc-show', short_help='shows the details of an SDN controller') +@cli_osm.command(name='sdnc-show', short_help='shows the details of an SDN controller') @click.argument('name') @click.pass_context def sdnc_show(ctx, name): @@ -2296,12 +2936,13 @@ def sdnc_show(ctx, name): NAME: name or ID of the SDN controller """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.sdnc.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.sdnc.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in list(resp.items()): @@ -2314,7 +2955,7 @@ def sdnc_show(ctx, name): # K8s cluster operations ########################### -@cli.command(name='k8scluster-add') +@cli_osm.command(name='k8scluster-add', short_help='adds a K8s cluster to OSM') @click.argument('name') @click.option('--creds', prompt=True, @@ -2329,7 +2970,7 @@ def sdnc_show(ctx, name): prompt=True, help='list of VIM networks, in JSON inline format, where the cluster is accessible via L3 routing, e.g. "{(k8s_net1:vim_network1) [,(k8s_net2:vim_network2) ...]}"') @click.option('--description', - default='', + default=None, help='human readable description') @click.option('--namespace', default='kube-system', @@ -2342,8 +2983,7 @@ def sdnc_show(ctx, name): # help='If set, K8s cluster is assumed to be ready for its use with OSM') #@click.option('--wait', # is_flag=True, -# help='do not return the control immediately, but keep it \ -# until the operation is completed, or timeout') +# help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def k8scluster_add(ctx, name, @@ -2358,25 +2998,26 @@ def k8scluster_add(ctx, NAME: name of the K8s cluster """ - try: - check_client_version(ctx.obj, ctx.command.name) - cluster = {} - cluster['name'] = name - with open(creds, 'r') as cf: - cluster['credentials'] = yaml.safe_load(cf.read()) - cluster['k8s_version'] = version - cluster['vim_account'] = vim - cluster['nets'] = yaml.safe_load(k8s_nets) + # try: + check_client_version(ctx.obj, ctx.command.name) + cluster = {} + cluster['name'] = name + with open(creds, 'r') as cf: + cluster['credentials'] = yaml.safe_load(cf.read()) + cluster['k8s_version'] = version + cluster['vim_account'] = vim + cluster['nets'] = yaml.safe_load(k8s_nets) + if description: cluster['description'] = description - if namespace: cluster['namespace'] = namespace - if cni: cluster['cni'] = yaml.safe_load(cni) - ctx.obj.k8scluster.create(name, cluster) - except ClientException as e: - print(str(e)) - exit(1) + if namespace: cluster['namespace'] = namespace + if cni: cluster['cni'] = yaml.safe_load(cni) + ctx.obj.k8scluster.create(name, cluster) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='k8scluster-update', short_help='updates a K8s cluster') +@cli_osm.command(name='k8scluster-update', short_help='updates a K8s cluster') @click.argument('name') @click.option('--newname', help='New name for the K8s cluster') @click.option('--creds', help='credentials file, i.e. a valid `.kube/config` file') @@ -2401,47 +3042,46 @@ def k8scluster_update(ctx, NAME: name or ID of the K8s cluster """ - try: - check_client_version(ctx.obj, ctx.command.name) - cluster = {} - if newname: cluster['name'] = newname - if creds: - with open(creds, 'r') as cf: - cluster['credentials'] = yaml.safe_load(cf.read()) - if version: cluster['k8s_version'] = version - if vim: cluster['vim_account'] = vim - if k8s_nets: cluster['nets'] = yaml.safe_load(k8s_nets) - if description: cluster['description'] = description - if namespace: cluster['namespace'] = namespace - if cni: cluster['cni'] = yaml.safe_load(cni) - ctx.obj.k8scluster.update(name, cluster) - except ClientException as e: - print(str(e)) - exit(1) - - -@cli.command(name='k8scluster-delete') + # try: + check_client_version(ctx.obj, ctx.command.name) + cluster = {} + if newname: cluster['name'] = newname + if creds: + with open(creds, 'r') as cf: + cluster['credentials'] = yaml.safe_load(cf.read()) + if version: cluster['k8s_version'] = version + if vim: cluster['vim_account'] = vim + if k8s_nets: cluster['nets'] = yaml.safe_load(k8s_nets) + if description: cluster['description'] = description + if namespace: cluster['namespace'] = namespace + if cni: cluster['cni'] = yaml.safe_load(cni) + ctx.obj.k8scluster.update(name, cluster) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +@cli_osm.command(name='k8scluster-delete', short_help='deletes a K8s cluster') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion from the DB (not recommended)') #@click.option('--wait', # is_flag=True, -# help='do not return the control immediately, but keep it \ -# until the operation is completed, or timeout') +# help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def k8scluster_delete(ctx, name, force): """deletes a K8s cluster NAME: name or ID of the K8s cluster to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.k8scluster.delete(name, force=force) - except ClientException as e: - print(str(e)) - exit(1) + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.k8scluster.delete(name, force=force) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='k8scluster-list') +@cli_osm.command(name='k8scluster-list') @click.option('--filter', default=None, help='restricts the list to the K8s clusters matching the filter') @click.option('--literal', is_flag=True, @@ -2449,25 +3089,25 @@ def k8scluster_delete(ctx, name, force): @click.pass_context def k8scluster_list(ctx, filter, literal): """list all K8s clusters""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.k8scluster.list(filter) - if literal: - print(yaml.safe_dump(resp)) - return - table = PrettyTable(['Name', 'Id', 'Version', 'VIM', 'K8s-nets', 'Description']) - for cluster in resp: - table.add_row([cluster['name'], cluster['_id'], cluster['k8s_version'], cluster['vim_account'], - json.dumps(cluster['nets']), trunc_text(cluster.get('description',''),40) - ]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.k8scluster.list(filter) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + table = PrettyTable(['Name', 'Id', 'Version', 'VIM', 'K8s-nets', 'Operational State', 'Description']) + for cluster in resp: + table.add_row([cluster['name'], cluster['_id'], cluster['k8s_version'], cluster['vim_account'], + json.dumps(cluster['nets']), cluster["_admin"]["operationalState"], + trunc_text(cluster.get('description') or '', 40)]) + table.align = 'l' + print(table) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='k8scluster-show') +@cli_osm.command(name='k8scluster-show', short_help='shows the details of a K8s cluster') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -2477,19 +3117,19 @@ def k8scluster_show(ctx, name, literal): NAME: name or ID of the K8s cluster """ - try: - resp = ctx.obj.k8scluster.get(name) - if literal: - print(yaml.safe_dump(resp)) - return - table = PrettyTable(['key', 'attribute']) - for k, v in list(resp.items()): - table.add_row([k, wrap_text(text=json.dumps(v, indent=2),width=100)]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + # try: + resp = ctx.obj.k8scluster.get(name) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + table = PrettyTable(['key', 'attribute']) + for k, v in list(resp.items()): + table.add_row([k, wrap_text(text=json.dumps(v, indent=2),width=100)]) + table.align = 'l' + print(table) + # except ClientException as e: + # print(str(e)) + # exit(1) @@ -2497,101 +3137,117 @@ def k8scluster_show(ctx, name, literal): # Repo operations ########################### -@cli.command(name='repo-add') +@cli_osm.command(name='repo-add', short_help='adds a repo to OSM') @click.argument('name') @click.argument('uri') @click.option('--type', - type=click.Choice(['helm-chart', 'juju-bundle']), - prompt=True, - help='type of repo (helm-chart for Helm Charts, juju-bundle for Juju Bundles)') + type=click.Choice(['helm-chart', 'juju-bundle', 'osm']), + default='osm', + help='type of repo (helm-chart for Helm Charts, juju-bundle for Juju Bundles, osm for OSM Repositories)') @click.option('--description', - default='', + default=None, help='human readable description') +@click.option('--user', + default=None, + help='OSM repository: The username of the OSM repository') +@click.option('--password', + default=None, + help='OSM repository: The password of the OSM repository') #@click.option('--wait', # is_flag=True, -# help='do not return the control immediately, but keep it \ -# until the operation is completed, or timeout') +# help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context -def repo_add(ctx, - name, - uri, - type, - description): +def repo_add(ctx, **kwargs): """adds a repo to OSM NAME: name of the repo URI: URI of the repo """ - try: - check_client_version(ctx.obj, ctx.command.name) - repo = {} - repo['name'] = name - repo['url'] = uri - repo['type'] = type - repo['description'] = description - ctx.obj.repo.create(name, repo) - except ClientException as e: - print(str(e)) - exit(1) + # try: + kwargs = {k: v for k, v in kwargs.items() if v is not None} + repo = kwargs + repo["url"] = repo.pop("uri") + if repo["type"] in ['helm-chart', 'juju-bundle']: + ctx.obj.repo.create(repo['name'], repo) + else: + ctx.obj.osmrepo.create(repo['name'], repo) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='repo-update') +@cli_osm.command(name='repo-update', short_help='updates a repo in OSM') @click.argument('name') @click.option('--newname', help='New name for the repo') @click.option('--uri', help='URI of the repo') -@click.option('--type', type=click.Choice(['helm-chart', 'juju-bundle']), - help='type of repo (helm-chart for Helm Charts, juju-bundle for Juju Bundles)') @click.option('--description', help='human readable description') #@click.option('--wait', # is_flag=True, -# help='do not return the control immediately, but keep it \ -# until the operation is completed, or timeout') +# help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def repo_update(ctx, name, newname, uri, - type, description): """updates a repo in OSM NAME: name of the repo """ + # try: + check_client_version(ctx.obj, ctx.command.name) + repo = {} + if newname: + repo['name'] = newname + if uri: + repo['uri'] = uri + if description: repo['description'] = description try: - check_client_version(ctx.obj, ctx.command.name) - repo = {} - if newname: repo['name'] = newname - if uri: repo['uri'] = uri - if type: repo['type'] = type - if description: repo['description'] = description ctx.obj.repo.update(name, repo) - except ClientException as e: - print(str(e)) - exit(1) + except NotFound: + ctx.obj.osmrepo.update(name, repo) + + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='repo-delete') +@cli_osm.command(name='repo-index', short_help='Index a repository from a folder with artifacts') +@click.option('--origin', default='.', help='origin path where the artifacts are located') +@click.option('--destination', default='.', help='destination path where the index is deployed') +@click.pass_context +def repo_index(ctx, origin, destination): + """Index a repository + + NAME: name or ID of the repo to be deleted + """ + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.osmrepo.repo_index(origin, destination) + + +@cli_osm.command(name='repo-delete', short_help='deletes a repo') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion from the DB (not recommended)') #@click.option('--wait', # is_flag=True, -# help='do not return the control immediately, but keep it \ -# until the operation is completed, or timeout') +# help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def repo_delete(ctx, name, force): """deletes a repo NAME: name or ID of the repo to be deleted """ + logger.debug("") try: - check_client_version(ctx.obj, ctx.command.name) ctx.obj.repo.delete(name, force=force) - except ClientException as e: - print(str(e)) - exit(1) + except NotFound: + ctx.obj.osmrepo.delete(name, force=force) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='repo-list') +@cli_osm.command(name='repo-list') @click.option('--filter', default=None, help='restricts the list to the repos matching the filter') @click.option('--literal', is_flag=True, @@ -2599,24 +3255,27 @@ def repo_delete(ctx, name, force): @click.pass_context def repo_list(ctx, filter, literal): """list all repos""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.repo.list(filter) - if literal: - print(yaml.safe_dump(resp)) - return - table = PrettyTable(['Name', 'Id', 'Type', 'URI', 'Description']) - for repo in resp: - #cluster['k8s-nets'] = json.dumps(yaml.safe_load(cluster['k8s-nets'])) - table.add_row([repo['name'], repo['_id'], repo['type'], repo['url'], trunc_text(repo.get('description',''),40)]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + # try: + # K8s Repositories + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.repo.list(filter) + resp += ctx.obj.osmrepo.list(filter) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + table = PrettyTable(['Name', 'Id', 'Type', 'URI', 'Description']) + for repo in resp: + #cluster['k8s-nets'] = json.dumps(yaml.safe_load(cluster['k8s-nets'])) + table.add_row([repo['name'], repo['_id'], repo['type'], repo['url'], trunc_text(repo.get('description') or '',40)]) + table.align = 'l' + print(table) + + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='repo-show') +@cli_osm.command(name='repo-show', short_help='shows the details of a repo') @click.argument('name') @click.option('--literal', is_flag=True, help='print literally, no pretty table') @@ -2628,17 +3287,23 @@ def repo_show(ctx, name, literal): """ try: resp = ctx.obj.repo.get(name) - if literal: - print(yaml.safe_dump(resp)) - return - table = PrettyTable(['key', 'attribute']) + except NotFound: + resp = ctx.obj.osmrepo.get(name) + + if literal: + if resp: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + table = PrettyTable(['key', 'attribute']) + if resp: for k, v in list(resp.items()): table.add_row([k, json.dumps(v, indent=2)]) - table.align = 'l' - print(table) - except ClientException as e: - print(str(e)) - exit(1) + + table.align = 'l' + print(table) + # except ClientException as e: + # print(str(e)) + # exit(1) @@ -2646,28 +3311,56 @@ def repo_show(ctx, name, literal): # Project mgmt operations #################### -@cli.command(name='project-create', short_help='creates a new project') +@cli_osm.command(name='project-create', short_help='creates a new project') @click.argument('name') #@click.option('--description', # default='no description', # help='human readable description') +@click.option('--domain-name', 'domain_name', + default=None, + help='assign to a domain') +@click.option('--quotas', 'quotas', multiple=True, default=None, + help="provide quotas. Can be used several times: 'quota1=number[,quota2=number,...]'. Quotas can be one " + "of vnfds, nsds, nsts, pdus, nsrs, nsis, vim_accounts, wim_accounts, sdns, k8sclusters, k8srepos") @click.pass_context -def project_create(ctx, name): +def project_create(ctx, name, domain_name, quotas): """Creates a new project NAME: name of the project + DOMAIN_NAME: optional domain name for the project when keystone authentication is used + QUOTAS: set quotas for the project """ - project = {} - project['name'] = name + logger.debug("") + project = {'name': name} + if domain_name: + project['domain_name'] = domain_name + quotas_dict = _process_project_quotas(quotas) + if quotas_dict: + project['quotas'] = quotas_dict + + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.project.create(name, project) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +def _process_project_quotas(quota_list): + quotas_dict = {} + if not quota_list: + return quotas_dict try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.project.create(name, project) - except ClientException as e: - print(str(e)) - exit(1) + for quota in quota_list: + for single_quota in quota.split(","): + k, v = single_quota.split("=") + quotas_dict[k] = None if v in ('None', 'null', '') else int(v) + except (ValueError, TypeError): + raise ClientException("invalid format for 'quotas'. Use 'k1=v1,v1=v2'. v must be a integer or null") + return quotas_dict -@cli.command(name='project-delete', short_help='deletes a project') +@cli_osm.command(name='project-delete', short_help='deletes a project') @click.argument('name') #@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -2676,26 +3369,28 @@ def project_delete(ctx, name): NAME: name or ID of the project to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.project.delete(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.project.delete(name) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='project-list', short_help='list all projects') +@cli_osm.command(name='project-list', short_help='list all projects') @click.option('--filter', default=None, help='restricts the list to the projects matching the filter') @click.pass_context def project_list(ctx, filter): """list all projects""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.project.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.project.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['name', 'id']) for proj in resp: table.add_row([proj['name'], proj['_id']]) @@ -2703,7 +3398,7 @@ def project_list(ctx, filter): print(table) -@cli.command(name='project-show', short_help='shows the details of a project') +@cli_osm.command(name='project-show', short_help='shows the details of a project') @click.argument('name') @click.pass_context def project_show(ctx, name): @@ -2711,12 +3406,13 @@ def project_show(ctx, name): NAME: name or ID of the project """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.project.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.project.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in resp.items(): @@ -2725,38 +3421,44 @@ def project_show(ctx, name): print(table) -@cli.command(name='project-update', short_help='updates a project (only the name can be updated)') +@cli_osm.command(name='project-update', short_help='updates a project (only the name can be updated)') @click.argument('project') -@click.option('--name', - prompt=True, +@click.option('--name', default=None, help='new name for the project') - +@click.option('--quotas', 'quotas', multiple=True, default=None, + help="change quotas. Can be used several times: 'quota1=number|empty[,quota2=...]' " + "(use empty to reset quota to default") @click.pass_context -def project_update(ctx, project, name): +def project_update(ctx, project, name, quotas): """ Update a project name :param ctx: :param project: id or name of the project to modify :param name: new name for the project + :param quotas: change quotas of the project :return: """ - + logger.debug("") project_changes = {} - project_changes['name'] = name + if name: + project_changes['name'] = name + quotas_dict = _process_project_quotas(quotas) + if quotas_dict: + project_changes['quotas'] = quotas_dict - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.project.update(project, project_changes) - except ClientException as e: - print(str(e)) + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.project.update(project, project_changes) + # except ClientException as e: + # print(str(e)) #################### # User mgmt operations #################### -@cli.command(name='user-create', short_help='creates a new user') +@cli_osm.command(name='user-create', short_help='creates a new user') @click.argument('username') @click.option('--password', prompt=True, @@ -2770,9 +3472,12 @@ def project_update(ctx, project, name): help='list of project ids that the user belongs to') @click.option('--project-role-mappings', 'project_role_mappings', default=None, multiple=True, - help='creating user project/role(s) mapping') + help="assign role(s) in a project. Can be used several times: 'project,role1[,role2,...]'") +@click.option('--domain-name', 'domain_name', + default=None, + help='assign to a domain') @click.pass_context -def user_create(ctx, username, password, projects, project_role_mappings): +def user_create(ctx, username, password, projects, project_role_mappings, domain_name): """Creates a new user \b @@ -2780,22 +3485,26 @@ def user_create(ctx, username, password, projects, project_role_mappings): PASSWORD: password of the user PROJECTS: projects assigned to user (internal only) PROJECT_ROLE_MAPPING: roles in projects assigned to user (keystone) + DOMAIN_NAME: optional domain name for the user when keystone authentication is used """ + logger.debug("") user = {} user['username'] = username user['password'] = password user['projects'] = projects user['project_role_mappings'] = project_role_mappings - - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.user.create(username, user) - except ClientException as e: - print(str(e)) - exit(1) + if domain_name: + user['domain_name'] = domain_name + + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.user.create(username, user) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='user-update', short_help='updates user information') +@cli_osm.command(name='user-update', short_help='updates user information') @click.argument('username') @click.option('--password', # prompt=True, @@ -2807,16 +3516,16 @@ def user_create(ctx, username, password, projects, project_role_mappings): help='change username') @click.option('--set-project', 'set_project', default=None, multiple=True, - help='create/replace the project,role(s) mapping for this project: \'project,role1,role2,...\'') + help="create/replace the roles for this project: 'project,role1[,role2,...]'") @click.option('--remove-project', 'remove_project', default=None, multiple=True, - help='removes project from user: \'project\'') + help="removes project from user: 'project'") @click.option('--add-project-role', 'add_project_role', default=None, multiple=True, - help='adds project,role(s) mapping: \'project,role1,role2,...\'') + help="assign role(s) in a project. Can be used several times: 'project,role1[,role2,...]'") @click.option('--remove-project-role', 'remove_project_role', default=None, multiple=True, - help='removes project,role(s) mapping: \'project,role1,role2,...\'') + help="remove role(s) in a project. Can be used several times: 'project,role1[,role2,...]'") @click.pass_context def user_update(ctx, username, password, set_username, set_project, remove_project, add_project_role, remove_project_role): @@ -2831,6 +3540,7 @@ def user_update(ctx, username, password, set_username, set_project, remove_proje ADD_PROJECT_ROLE: adding mappings for project/role(s) REMOVE_PROJECT_ROLE: removing mappings for project/role(s) """ + logger.debug("") user = {} user['password'] = password user['username'] = set_username @@ -2839,15 +3549,15 @@ def user_update(ctx, username, password, set_username, set_project, remove_proje user['add-project-role'] = add_project_role user['remove-project-role'] = remove_project_role - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.user.update(username, user) - except ClientException as e: - print(str(e)) - exit(1) + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.user.update(username, user) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='user-delete', short_help='deletes a user') +@cli_osm.command(name='user-delete', short_help='deletes a user') @click.argument('name') #@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -2857,26 +3567,27 @@ def user_delete(ctx, name): \b NAME: name or ID of the user to be deleted """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.user.delete(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.user.delete(name) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='user-list', short_help='list all users') +@cli_osm.command(name='user-list', short_help='list all users') @click.option('--filter', default=None, help='restricts the list to the users matching the filter') @click.pass_context def user_list(ctx, filter): """list all users""" - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.user.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.user.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['name', 'id']) for user in resp: table.add_row([user['username'], user['_id']]) @@ -2884,7 +3595,7 @@ def user_list(ctx, filter): print(table) -@cli.command(name='user-show', short_help='shows the details of a user') +@cli_osm.command(name='user-show', short_help='shows the details of a user') @click.argument('name') @click.pass_context def user_show(ctx, name): @@ -2892,14 +3603,15 @@ def user_show(ctx, name): NAME: name or ID of the user """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.user.get(name) - if 'password' in resp: - resp['password']='********' - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.user.get(name) + if 'password' in resp: + resp['password']='********' + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in resp.items(): @@ -2912,7 +3624,7 @@ def user_show(ctx, name): # Fault Management operations #################### -@cli.command(name='ns-alarm-create') +@cli_osm.command(name='ns-alarm-create') @click.argument('name') @click.option('--ns', prompt=True, help='NS instance id or name') @click.option('--vnf', prompt=True, @@ -2935,27 +3647,28 @@ def ns_alarm_create(ctx, name, ns, vnf, vdu, metric, severity, """creates a new alarm for a NS instance""" # TODO: Check how to validate threshold_value. # Should it be an integer (1-100), percentage, or decimal (0.01-1.00)? - try: - ns_instance = ctx.obj.ns.get(ns) - alarm = {} - alarm['alarm_name'] = name - alarm['ns_id'] = ns_instance['_id'] - alarm['correlation_id'] = ns_instance['_id'] - alarm['vnf_member_index'] = vnf - alarm['vdu_name'] = vdu - alarm['metric_name'] = metric - alarm['severity'] = severity - alarm['threshold_value'] = int(threshold_value) - alarm['operation'] = threshold_operator - alarm['statistic'] = statistic - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.ns.create_alarm(alarm) - except ClientException as e: - print(str(e)) - exit(1) - - -#@cli.command(name='ns-alarm-delete') + logger.debug("") + # try: + ns_instance = ctx.obj.ns.get(ns) + alarm = {} + alarm['alarm_name'] = name + alarm['ns_id'] = ns_instance['_id'] + alarm['correlation_id'] = ns_instance['_id'] + alarm['vnf_member_index'] = vnf + alarm['vdu_name'] = vdu + alarm['metric_name'] = metric + alarm['severity'] = severity + alarm['threshold_value'] = int(threshold_value) + alarm['operation'] = threshold_operator + alarm['statistic'] = statistic + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.ns.create_alarm(alarm) + # except ClientException as e: + # print(str(e)) + # exit(1) + + +#@cli_osm.command(name='ns-alarm-delete') #@click.argument('name') #@click.pass_context #def ns_alarm_delete(ctx, name): @@ -2975,7 +3688,7 @@ def ns_alarm_create(ctx, name, ns, vnf, vdu, metric, severity, # Performance Management operations #################### -@cli.command(name='ns-metric-export', short_help='exports a metric to the internal OSM bus, which can be read by other apps') +@cli_osm.command(name='ns-metric-export', short_help='exports a metric to the internal OSM bus, which can be read by other apps') @click.option('--ns', prompt=True, help='NS instance id or name') @click.option('--vnf', prompt=True, help='VNF name (VNF member index as declared in the NSD)') @@ -2991,64 +3704,69 @@ def ns_metric_export(ctx, ns, vnf, vdu, metric, interval): """exports a metric to the internal OSM bus, which can be read by other apps""" # TODO: Check how to validate interval. # Should it be an integer (seconds), or should a suffix (s,m,h,d,w) also be permitted? - try: - ns_instance = ctx.obj.ns.get(ns) - metric_data = {} - metric_data['ns_id'] = ns_instance['_id'] - metric_data['correlation_id'] = ns_instance['_id'] - metric_data['vnf_member_index'] = vnf - metric_data['vdu_name'] = vdu - metric_data['metric_name'] = metric - metric_data['collection_unit'] = 'WEEK' - metric_data['collection_period'] = 1 - check_client_version(ctx.obj, ctx.command.name) - if not interval: - print('{}'.format(ctx.obj.ns.export_metric(metric_data))) - else: - i = 1 - while True: - print('{} {}'.format(ctx.obj.ns.export_metric(metric_data),i)) - time.sleep(int(interval)) - i+=1 - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + ns_instance = ctx.obj.ns.get(ns) + metric_data = {} + metric_data['ns_id'] = ns_instance['_id'] + metric_data['correlation_id'] = ns_instance['_id'] + metric_data['vnf_member_index'] = vnf + metric_data['vdu_name'] = vdu + metric_data['metric_name'] = metric + metric_data['collection_unit'] = 'WEEK' + metric_data['collection_period'] = 1 + check_client_version(ctx.obj, ctx.command.name) + if not interval: + print('{}'.format(ctx.obj.ns.export_metric(metric_data))) + else: + i = 1 + while True: + print('{} {}'.format(ctx.obj.ns.export_metric(metric_data),i)) + time.sleep(int(interval)) + i+=1 + # except ClientException as e: + # print(str(e)) + # exit(1) #################### # Other operations #################### -@cli.command(name='version') +@cli_osm.command(name='version', short_help='shows client and server versions') @click.pass_context def get_version(ctx): - try: - check_client_version(ctx.obj, "version") - print ("Server version: {}".format(ctx.obj.get_version())) - print ("Client version: {}".format(pkg_resources.get_distribution("osmclient").version)) - except ClientException as e: - print(str(e)) - exit(1) - -@cli.command(name='upload-package', short_help='uploads a VNF package or NS package') + """shows client and server versions""" + # try: + check_client_version(ctx.obj, "version") + print ("Server version: {}".format(ctx.obj.get_version())) + print ("Client version: {}".format(pkg_resources.get_distribution("osmclient").version)) + # except ClientException as e: + # print(str(e)) + # exit(1) + +@cli_osm.command(name='upload-package', short_help='uploads a VNF package or NS package') @click.argument('filename') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='the charm will not be compiled, it is assumed to already exist') @click.pass_context -def upload_package(ctx, filename): - """uploads a VNF package or NS package +def upload_package(ctx, filename, skip_charm_build): + """uploads a vnf package or ns package - FILENAME: VNF or NS package file (tar.gz) + filename: vnf or ns package folder, or vnf or ns package file (tar.gz) """ - try: - ctx.obj.package.upload(filename) - fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ - if fullclassname != 'osmclient.sol005.client.Client': - ctx.obj.package.wait_for_upload(filename) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + ctx.obj.package.upload(filename, skip_charm_build=skip_charm_build) + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname != 'osmclient.sol005.client.Client': + ctx.obj.package.wait_for_upload(filename) + # except ClientException as e: + # print(str(e)) + # exit(1) -#@cli.command(name='ns-scaling-show') +#@cli_osm.command(name='ns-scaling-show') #@click.argument('ns_name') #@click.pass_context #def show_ns_scaling(ctx, ns_name): @@ -3090,7 +3808,7 @@ def upload_package(ctx, filename): # print(table) -#@cli.command(name='ns-scale') +#@cli_osm.command(name='ns-scale') #@click.argument('ns_name') #@click.option('--ns_scale_group', prompt=True) #@click.option('--index', prompt=True) @@ -3114,7 +3832,7 @@ def upload_package(ctx, filename): # exit(1) -#@cli.command(name='config-agent-list') +#@cli_osm.command(name='config-agent-list') #@click.pass_context #def config_agent_list(ctx): # """list config agents""" @@ -3133,7 +3851,7 @@ def upload_package(ctx, filename): # print(table) -#@cli.command(name='config-agent-delete') +#@cli_osm.command(name='config-agent-delete') #@click.argument('name') #@click.pass_context #def config_agent_delete(ctx, name): @@ -3149,7 +3867,7 @@ def upload_package(ctx, filename): # exit(1) -#@cli.command(name='config-agent-add') +#@cli_osm.command(name='config-agent-add') #@click.option('--name', # prompt=True) #@click.option('--account_type', @@ -3173,7 +3891,7 @@ def upload_package(ctx, filename): # exit(1) -#@cli.command(name='ro-dump') +#@cli_osm.command(name='ro-dump') #@click.pass_context #def ro_dump(ctx): # """shows RO agent information""" @@ -3186,7 +3904,7 @@ def upload_package(ctx, filename): # print(table) -#@cli.command(name='vcs-list') +#@cli_osm.command(name='vcs-list') #@click.pass_context #def vcs_list(ctx): # check_client_version(ctx.obj, ctx.command.name, 'v1') @@ -3198,21 +3916,21 @@ def upload_package(ctx, filename): # print(table) -@cli.command(name='ns-action', short_help='executes an action/primitive over a NS instance') +@cli_osm.command(name='ns-action', short_help='executes an action/primitive over a NS instance') @click.argument('ns_name') @click.option('--vnf_name', default=None, help='member-vnf-index if the target is a vnf instead of a ns)') @click.option('--kdu_name', default=None, help='kdu-name if the target is a kdu)') @click.option('--vdu_id', default=None, help='vdu-id if the target is a vdu') -@click.option('--vdu_count', default=None, help='number of vdu instance of this vdu_id') +@click.option('--vdu_count', default=None, type=int, help='number of vdu instance of this vdu_id') @click.option('--action_name', prompt=True, help='action name') @click.option('--params', default=None, help='action params in YAML/JSON inline string') @click.option('--params_file', default=None, help='YAML/JSON file with action params') +@click.option('--timeout', required=False, default=None, type=int, help='timeout in seconds') @click.option('--wait', required=False, default=False, is_flag=True, - help='do not return the control immediately, but keep it \ - until the operation is completed, or timeout') + help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def ns_action(ctx, ns_name, @@ -3223,50 +3941,59 @@ def ns_action(ctx, action_name, params, params_file, + timeout, wait): """executes an action/primitive over a NS instance NS_NAME: name or ID of the NS instance """ - try: - check_client_version(ctx.obj, ctx.command.name) - op_data = {} - if vnf_name: - op_data['member_vnf_index'] = vnf_name - if kdu_name: - op_data['kdu_name'] = kdu_name - if vdu_id: - op_data['vdu_id'] = vdu_id - if vdu_count: - op_data['vdu_count_index'] = vdu_count - op_data['primitive'] = action_name - if params_file: - with open(params_file, 'r') as pf: - params = pf.read() - if params: - op_data['primitive_params'] = yaml.safe_load(params) - else: - op_data['primitive_params'] = {} - print(ctx.obj.ns.exec_op(ns_name, op_name='action', op_data=op_data, wait=wait)) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + op_data = {} + if vnf_name: + op_data['member_vnf_index'] = vnf_name + if kdu_name: + op_data['kdu_name'] = kdu_name + if vdu_id: + op_data['vdu_id'] = vdu_id + if vdu_count is not None: + op_data['vdu_count_index'] = vdu_count + if timeout: + op_data['timeout_ns_action'] = timeout + op_data['primitive'] = action_name + if params_file: + with open(params_file, 'r') as pf: + params = pf.read() + if params: + op_data['primitive_params'] = yaml.safe_load(params) + else: + op_data['primitive_params'] = {} + print(ctx.obj.ns.exec_op(ns_name, op_name='action', op_data=op_data, wait=wait)) - except ClientException as e: - print(str(e)) - exit(1) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='vnf-scale', short_help='executes a VNF scale (adding/removing VDUs)') +@cli_osm.command(name='vnf-scale', short_help='executes a VNF scale (adding/removing VDUs)') @click.argument('ns_name') @click.argument('vnf_name') @click.option('--scaling-group', prompt=True, help="scaling-group-descriptor name to use") @click.option('--scale-in', default=False, is_flag=True, help="performs a scale in operation") @click.option('--scale-out', default=False, is_flag=True, help="performs a scale out operation (by default)") +@click.option('--timeout', required=False, default=None, type=int, help='timeout in seconds') +@click.option('--wait', required=False, default=False, is_flag=True, + help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def vnf_scale(ctx, ns_name, vnf_name, scaling_group, scale_in, - scale_out): + scale_out, + timeout, + wait): """ Executes a VNF scale (adding/removing VDUs) @@ -3274,21 +4001,22 @@ def vnf_scale(ctx, NS_NAME: name or ID of the NS instance. VNF_NAME: member-vnf-index in the NS to be scaled. """ - try: - check_client_version(ctx.obj, ctx.command.name) - if not scale_in and not scale_out: - scale_out = True - ctx.obj.ns.scale_vnf(ns_name, vnf_name, scaling_group, scale_in, scale_out) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + if not scale_in and not scale_out: + scale_out = True + ctx.obj.ns.scale_vnf(ns_name, vnf_name, scaling_group, scale_in, scale_out, wait, timeout) + # except ClientException as e: + # print(str(e)) + # exit(1) ############################## # Role Management Operations # ############################## -@cli.command(name='role-create', short_help='creates a new role') +@cli_osm.command(name='role-create', short_help='creates a new role') @click.argument('name') @click.option('--permissions', default=None, @@ -3302,15 +4030,16 @@ def role_create(ctx, name, permissions): NAME: Name or ID of the role. DEFINITION: Definition of grant/denial of access to resources. """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.role.create(name, permissions) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.role.create(name, permissions) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='role-update', short_help='updates a role') +@cli_osm.command(name='role-update', short_help='updates a role') @click.argument('name') @click.option('--set-name', default=None, @@ -3335,15 +4064,16 @@ def role_update(ctx, name, set_name, add, remove): ADD: Grant/denial of access to resource to add. REMOVE: Grant/denial of access to resource to remove. """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.role.update(name, set_name, None, add, remove) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.role.update(name, set_name, None, add, remove) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='role-delete', short_help='deletes a role') +@cli_osm.command(name='role-delete', short_help='deletes a role') @click.argument('name') # @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context @@ -3354,15 +4084,16 @@ def role_delete(ctx, name): \b NAME: Name or ID of the role. """ - try: - check_client_version(ctx.obj, ctx.command.name) - ctx.obj.role.delete(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.role.delete(name) + # except ClientException as e: + # print(str(e)) + # exit(1) -@cli.command(name='role-list', short_help='list all roles') +@cli_osm.command(name='role-list', short_help='list all roles') @click.option('--filter', default=None, help='restricts the list to the projects matching the filter') @click.pass_context @@ -3370,12 +4101,13 @@ def role_list(ctx, filter): """ List all roles. """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.role.list(filter) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.role.list(filter) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['name', 'id']) for role in resp: table.add_row([role['name'], role['_id']]) @@ -3383,7 +4115,7 @@ def role_list(ctx, filter): print(table) -@cli.command(name='role-show', short_help='show specific role') +@cli_osm.command(name='role-show', short_help='show specific role') @click.argument('name') @click.pass_context def role_show(ctx, name): @@ -3393,12 +4125,13 @@ def role_show(ctx, name): \b NAME: Name or ID of the role. """ - try: - check_client_version(ctx.obj, ctx.command.name) - resp = ctx.obj.role.get(name) - except ClientException as e: - print(str(e)) - exit(1) + logger.debug("") + # try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.role.get(name) + # except ClientException as e: + # print(str(e)) + # exit(1) table = PrettyTable(['key', 'attribute']) for k, v in resp.items(): @@ -3407,7 +4140,7 @@ def role_show(ctx, name): print(table) -@cli.command(name='package-create', +@cli_osm.command(name='package-create', short_help='Create a package descriptor') @click.argument('package-type') @click.argument('package-name') @@ -3473,90 +4206,113 @@ def package_create(ctx, PACKAGE_NAME: Name of the package to create the folder with the content. """ - try: - check_client_version(ctx.obj, ctx.command.name) - print("Creating the {} structure: {}/{}".format(package_type.upper(), base_directory, package_name)) - resp = ctx.obj.package_tool.create(package_type, - base_directory, - package_name, - override=override, - image=image, - vdus=vdus, - vcpu=vcpu, - memory=memory, - storage=storage, - interfaces=interfaces, - vendor=vendor, - detailed=detailed, - netslice_subnets=netslice_subnets, - netslice_vlds=netslice_vlds) - print(resp) - except ClientException as inst: - print("ERROR: {}".format(inst)) - exit(1) - -@cli.command(name='package-validate', + # try: + check_client_version(ctx.obj, ctx.command.name) + print("Creating the {} structure: {}/{}".format(package_type.upper(), base_directory, package_name)) + resp = ctx.obj.package_tool.create(package_type, + base_directory, + package_name, + override=override, + image=image, + vdus=vdus, + vcpu=vcpu, + memory=memory, + storage=storage, + interfaces=interfaces, + vendor=vendor, + detailed=detailed, + netslice_subnets=netslice_subnets, + netslice_vlds=netslice_vlds) + print(resp) + # except ClientException as inst: + # print("ERROR: {}".format(inst)) + # exit(1) + +@cli_osm.command(name='package-validate', short_help='Validate a package descriptor') @click.argument('base-directory', default=".", required=False) +@click.option('--recursive/--no-recursive', + default=True, + help='The activated recursive option will validate the yaml files' + ' within the indicated directory and in its subdirectories') @click.pass_context def package_validate(ctx, - base_directory): + base_directory, + recursive): """ Validate descriptors given a base directory. \b BASE_DIRECTORY: Stub folder for NS, VNF or NST package. """ - try: - check_client_version(ctx.obj, ctx.command.name) - results = ctx.obj.package_tool.validate(base_directory) - table = PrettyTable() - table.field_names = ["TYPE", "PATH", "VALID", "ERROR"] - # Print the dictionary generated by the validation function - for result in results: - table.add_row([result["type"], result["path"], result["valid"], result["error"]]) - table.sortby = "VALID" - table.align["PATH"] = "l" - table.align["TYPE"] = "l" - table.align["ERROR"] = "l" - print(table) - except ClientException as inst: - print("ERROR: {}".format(inst)) - exit(1) + # try: + check_client_version(ctx.obj, ctx.command.name) + results = ctx.obj.package_tool.validate(base_directory, recursive) + table = PrettyTable() + table.field_names = ["TYPE", "PATH", "VALID", "ERROR"] + # Print the dictionary generated by the validation function + for result in results: + table.add_row([result["type"], result["path"], result["valid"], result["error"]]) + table.sortby = "VALID" + table.align["PATH"] = "l" + table.align["TYPE"] = "l" + table.align["ERROR"] = "l" + print(table) + # except ClientException as inst: + # print("ERROR: {}".format(inst)) + # exit(1) -@cli.command(name='package-build', +@cli_osm.command(name='package-build', short_help='Build the tar.gz of the package') @click.argument('package-folder') @click.option('--skip-validation', default=False, is_flag=True, help='skip package validation') +@click.option('--skip-charm-build', default=False, is_flag=True, + help='the charm will not be compiled, it is assumed to already exist') @click.pass_context def package_build(ctx, package_folder, - skip_validation): + skip_validation, + skip_charm_build): """ Build the package NS, VNF given the package_folder. \b PACKAGE_FOLDER: Folder of the NS, VNF or NST to be packaged """ + # try: + check_client_version(ctx.obj, ctx.command.name) + results = ctx.obj.package_tool.build(package_folder, + skip_validation=skip_validation, + skip_charm_build=skip_charm_build) + print(results) + # except ClientException as inst: + # print("ERROR: {}".format(inst)) + # exit(1) + + +def cli(): try: - check_client_version(ctx.obj, ctx.command.name) - results = ctx.obj.package_tool.build(package_folder, skip_validation) - print(results) - except ClientException as inst: - print("ERROR: {}".format(inst)) - exit(1) + cli_osm() + exit(0) + except pycurl.error as exc: + print(exc) + print('Maybe "--hostname" option or OSM_HOSTNAME environment variable needs to be specified') + except ClientException as exc: + print("ERROR: {}".format(exc)) + except (FileNotFoundError, PermissionError) as exc: + print("Cannot open file: {}".format(exc)) + except yaml.YAMLError as exc: + print("Invalid YAML format: {}".format(exc)) + exit(1) + # TODO capture other controlled exceptions here + # TODO remove the ClientException captures from all places, unless they do something different if __name__ == '__main__': - try: - cli() - except pycurl.error as e: - print(e) - print('Maybe "--hostname" option or OSM_HOSTNAME' + - 'environment variable needs to be specified') - exit(1) + cli() + diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 2de6a4c..00c50cb 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -36,9 +36,10 @@ from osmclient.sol005 import role from osmclient.sol005 import pdud from osmclient.sol005 import k8scluster from osmclient.sol005 import repo -from osmclient.common.exceptions import ClientException +from osmclient.sol005 import osmrepo from osmclient.common import package_tool import json +import logging class Client(object): @@ -55,10 +56,12 @@ class Client(object): self._user = user self._password = password self._project = project + self._project_domain_name = kwargs.get("project_domain_name") + self._user_domain_name = kwargs.get("user_domain_name") + self._logger = logging.getLogger('osmclient') self._auth_endpoint = '/admin/v1/tokens' self._headers = {} self._token = None - if len(host.split(':')) > 1: # backwards compatible, port provided as part of host self._host = host.split(':')[0] @@ -68,7 +71,7 @@ class Client(object): self._so_port = so_port self._http_client = http.Http( - 'https://{}:{}/osm'.format(self._host,self._so_port)) + 'https://{}:{}/osm'.format(self._host,self._so_port), **kwargs) self._headers['Accept'] = 'application/json' self._headers['Content-Type'] = 'application/yaml' http_header = ['{}: {}'.format(key, val) @@ -91,6 +94,7 @@ class Client(object): self.pdu = pdud.Pdu(self._http_client, client=self) self.k8scluster = k8scluster.K8scluster(self._http_client, client=self) self.repo = repo.Repo(self._http_client, client=self) + self.osmrepo = osmrepo.OSMRepo(self._http_client, client=self) self.package_tool = package_tool.PackageTool(client=self) ''' self.vca = vca.Vca(http_client, client=self, **kwargs) @@ -98,15 +102,21 @@ class Client(object): ''' def get_token(self): + self._logger.debug("") if self._token is None: postfields_dict = {'username': self._user, 'password': self._password, 'project_id': self._project} + if self._project_domain_name: + postfields_dict["project_domain_name"] = self._project_domain_name + if self._user_domain_name: + postfields_dict["user_domain_name"] = self._user_domain_name http_code, resp = self._http_client.post_cmd(endpoint=self._auth_endpoint, - postfields_dict=postfields_dict) - if http_code not in (200, 201, 202, 204): - message ='Authentication error: not possible to get auth token\nresp:\n{}'.format(resp) - raise ClientException(message) + postfields_dict=postfields_dict, + skip_query_admin=True) +# if http_code not in (200, 201, 202, 204): +# message ='Authentication error: not possible to get auth token\nresp:\n{}'.format(resp) +# raise ClientException(message) token = json.loads(resp) if resp else None self._token = token['id'] @@ -118,6 +128,22 @@ class Client(object): self._http_client.set_http_header(http_header) def get_version(self): - resp = self._http_client.get_cmd(endpoint="/version") - return "{} {}".format(resp.get("version"), resp.get("date")) + _, resp = self._http_client.get2_cmd(endpoint="/version", skip_query_admin=True) + #print(http_code, resp) + try: + resp = json.loads(resp) + version = resp.get("version") + date = resp.get("date") + except ValueError: + version = resp.split()[2] + date = resp.split()[4] + return "{} {}".format(version, date) + def set_default_params(self, **kwargs): + host = kwargs.pop('host', None) + if host != None: + self._host=host + port = kwargs.pop('port', None) + if port != None: + self._so_port=port + self._http_client.set_query_admin(**kwargs) diff --git a/osmclient/sol005/http.py b/osmclient/sol005/http.py index f19a098..9a7bc6e 100644 --- a/osmclient/sol005/http.py +++ b/osmclient/sol005/http.py @@ -14,22 +14,58 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from io import BytesIO -import pycurl import json +import logging + from osmclient.common import http +from osmclient.common.exceptions import OsmHttpException, NotFound +import pycurl + class Http(http.Http): + CONNECT_TIMEOUT = 15 - def __init__(self, url, user='admin', password='admin'): + def __init__(self, url, user='admin', password='admin', **kwargs): self._url = url self._user = user self._password = password self._http_header = None + self._logger = logging.getLogger('osmclient') + self._default_query_admin = None + self._all_projects = None + self._public = None + if 'all_projects' in kwargs: + self._all_projects = kwargs['all_projects'] + if 'public' in kwargs: + self._public = kwargs['public'] + self._default_query_admin = self._complete_default_query_admin() + + def _complete_default_query_admin(self): + query_string_list = [] + if self._all_projects: + query_string_list.append("ADMIN") + if self._public is not None: + query_string_list.append("PUBLIC={}".format(self._public)) + return "&".join(query_string_list) + + def _complete_endpoint(self, endpoint): + if self._default_query_admin: + if '?' in endpoint: + endpoint = '&'.join([endpoint, self._default_query_admin]) + else: + endpoint = '?'.join([endpoint, self._default_query_admin]) + return endpoint - def _get_curl_cmd(self, endpoint): + def _get_curl_cmd(self, endpoint, skip_query_admin=False): + self._logger.debug("") curl_cmd = pycurl.Curl() - #print(self._url + endpoint) + if self._logger.getEffectiveLevel() == logging.DEBUG: + curl_cmd.setopt(pycurl.VERBOSE, True) + if not skip_query_admin: + endpoint = self._complete_endpoint(endpoint) + curl_cmd.setopt(pycurl.CONNECTTIMEOUT, self.CONNECT_TIMEOUT) curl_cmd.setopt(pycurl.URL, self._url + endpoint) curl_cmd.setopt(pycurl.SSL_VERIFYPEER, 0) curl_cmd.setopt(pycurl.SSL_VERIFYHOST, 0) @@ -37,26 +73,33 @@ class Http(http.Http): curl_cmd.setopt(pycurl.HTTPHEADER, self._http_header) return curl_cmd - def delete_cmd(self, endpoint): + def delete_cmd(self, endpoint, skip_query_admin=False): + self._logger.debug("") data = BytesIO() - curl_cmd = self._get_curl_cmd(endpoint) + curl_cmd = self._get_curl_cmd(endpoint, skip_query_admin) curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE") curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + self._logger.info("Request METHOD: {} URL: {}".format("DELETE", self._url + endpoint)) curl_cmd.perform() http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) - #print('HTTP_CODE: {}'.format(http_code)) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() + self.check_http_response(http_code, data) # TODO 202 accepted should be returned somehow if data.getvalue(): - return http_code, data.getvalue().decode() + data_text = data.getvalue().decode() + self._logger.verbose("Response DATA: {}".format(data_text)) + return http_code, data_text else: return http_code, None def send_cmd(self, endpoint='', postfields_dict=None, formfile=None, filename=None, - put_method=False, patch_method=False): + put_method=False, patch_method=False, + skip_query_admin=False): + self._logger.debug("") data = BytesIO() - curl_cmd = self._get_curl_cmd(endpoint) + curl_cmd = self._get_curl_cmd(endpoint, skip_query_admin) if put_method: curl_cmd.setopt(pycurl.CUSTOMREQUEST, "PUT") elif patch_method: @@ -66,6 +109,13 @@ class Http(http.Http): if postfields_dict is not None: jsondata = json.dumps(postfields_dict) + if 'password' in postfields_dict: + postfields_dict_copy = copy.deepcopy(postfields_dict) + postfields_dict_copy['password'] = '******' + jsondata_log = json.dumps(postfields_dict_copy) + else: + jsondata_log = jsondata + self._logger.verbose("Request POSTFIELDS: {}".format(jsondata_log)) curl_cmd.setopt(pycurl.POSTFIELDS, jsondata) elif formfile is not None: curl_cmd.setopt( @@ -75,50 +125,92 @@ class Http(http.Http): formfile[1])))]) elif filename is not None: with open(filename, 'rb') as stream: - postdata=stream.read() + postdata = stream.read() + self._logger.verbose("Request POSTFIELDS: Binary content") curl_cmd.setopt(pycurl.POSTFIELDS, postdata) + if put_method: + self._logger.info("Request METHOD: {} URL: {}".format("PUT", self._url + endpoint)) + elif patch_method: + self._logger.info("Request METHOD: {} URL: {}".format("PATCH", self._url + endpoint)) + else: + self._logger.info("Request METHOD: {} URL: {}".format("POST", self._url + endpoint)) curl_cmd.perform() http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() + self.check_http_response(http_code, data) if data.getvalue(): - return http_code, data.getvalue().decode() + data_text = data.getvalue().decode() + self._logger.verbose("Response DATA: {}".format(data_text)) + return http_code, data_text else: return http_code, None def post_cmd(self, endpoint='', postfields_dict=None, - formfile=None, filename=None): + formfile=None, filename=None, + skip_query_admin=False): + self._logger.debug("") return self.send_cmd(endpoint=endpoint, postfields_dict=postfields_dict, - formfile=formfile, - filename=filename, - put_method=False, patch_method=False) + formfile=formfile, filename=filename, + put_method=False, patch_method=False, + skip_query_admin=skip_query_admin) def put_cmd(self, endpoint='', postfields_dict=None, - formfile=None, filename=None): + formfile=None, filename=None, + skip_query_admin=False): + self._logger.debug("") return self.send_cmd(endpoint=endpoint, postfields_dict=postfields_dict, - formfile=formfile, - filename=filename, - put_method=True, patch_method=False) + formfile=formfile, filename=filename, + put_method=True, patch_method=False, + skip_query_admin=skip_query_admin) def patch_cmd(self, endpoint='', postfields_dict=None, - formfile=None, filename=None): + formfile=None, filename=None, + skip_query_admin=False): + self._logger.debug("") return self.send_cmd(endpoint=endpoint, postfields_dict=postfields_dict, - formfile=formfile, - filename=filename, - put_method=False, patch_method=True) + formfile=formfile, filename=filename, + put_method=False, patch_method=True, + skip_query_admin=skip_query_admin) - def get2_cmd(self, endpoint): + def get2_cmd(self, endpoint, skip_query_admin=False): + self._logger.debug("") data = BytesIO() - curl_cmd = self._get_curl_cmd(endpoint) + curl_cmd = self._get_curl_cmd(endpoint, skip_query_admin) curl_cmd.setopt(pycurl.HTTPGET, 1) curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + self._logger.info("Request METHOD: {} URL: {}".format("GET", self._url + endpoint)) curl_cmd.perform() http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + self._logger.info("Response HTTPCODE: {}".format(http_code)) curl_cmd.close() + self.check_http_response(http_code, data) if data.getvalue(): - return http_code, data.getvalue().decode() + data_text = data.getvalue().decode() + self._logger.verbose("Response DATA: {}".format(data_text)) + return http_code, data_text return http_code, None + def check_http_response(self, http_code, data): + if http_code >= 300: + resp = "" + if data.getvalue(): + data_text = data.getvalue().decode() + self._logger.verbose("Response {} DATA: {}".format(http_code, data_text)) + resp = ": " + data_text + else: + self._logger.verbose("Response {}".format(http_code)) + if http_code == 404: + raise NotFound("Error {}{}".format(http_code, resp)) + raise OsmHttpException("Error {}{}".format(http_code, resp)) + + def set_query_admin(self, **kwargs): + if 'all_projects' in kwargs: + self._all_projects = kwargs['all_projects'] + if 'public' in kwargs: + self._public = kwargs['public'] + self._default_query_admin = self._complete_default_query_admin() diff --git a/osmclient/sol005/k8scluster.py b/osmclient/sol005/k8scluster.py index 5520787..4ac2e48 100644 --- a/osmclient/sol005/k8scluster.py +++ b/osmclient/sol005/k8scluster.py @@ -17,8 +17,8 @@ OSM K8s cluster API handling """ from osmclient.common import utils -from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException import json class K8scluster(object): @@ -45,21 +45,20 @@ class K8scluster(object): postfields_dict=k8s_cluster) #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to add K8s cluster {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format(resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to add K8s cluster {} - {}".format(name, msg)) def update(self, name, k8s_cluster): self._client.get_token() @@ -68,16 +67,16 @@ class K8scluster(object): postfields_dict=k8s_cluster) # print 'HTTP CODE: {}'.format(http_code) # print 'RESP: {}'.format(resp) - if http_code in (200, 201, 202, 204): - pass - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update K8s cluster {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + # pass + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update K8s cluster {} - {}".format(name, msg)) def get_id(self, name): """Returns a K8s cluster id from a K8s cluster name @@ -104,12 +103,12 @@ class K8scluster(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete K8s cluster {} - {}".format(name, msg)) def list(self, filter=None): @@ -119,9 +118,9 @@ class K8scluster(object): filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): @@ -131,10 +130,13 @@ class K8scluster(object): cluster_id = name if not utils.validate_uuid4(name): cluster_id = self.get_id(name) - resp = self._http.get_cmd('{}/{}'.format(self._apiBase,cluster_id)) - if not resp or '_id' not in resp: - raise ClientException('failed to get K8s cluster info: '.format(resp)) - else: + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase,cluster_id)) + if resp: + resp = json.loads(resp) + if not resp or '_id' not in resp: + raise ClientException('failed to get K8s cluster info: {}'.format(resp)) return resp - raise NotFound("K8s cluster {} not found".format(name)) + except NotFound: + raise NotFound("K8s cluster {} not found".format(name)) diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py index e9858f4..b551868 100644 --- a/osmclient/sol005/ns.py +++ b/osmclient/sol005/ns.py @@ -24,6 +24,7 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml import json +import logging class Ns(object): @@ -31,6 +32,7 @@ class Ns(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/nslcm' self._apiVersion = '/v1' self._apiResource = '/ns_instances_content' @@ -38,14 +40,17 @@ class Ns(object): self._apiVersion, self._apiResource) # NS '--wait' option - def _wait(self, id, deleteFlag=False): + def _wait(self, id, wait_time, deleteFlag=False): + self._logger.debug("") # Endpoint to get operation status apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/ns_lcm_op_occs') # Wait for status for NS instance creation/update/deletion + if isinstance(wait_time, bool): + wait_time = WaitForStatus.TIMEOUT_NS_OPERATION WaitForStatus.wait_for_status( 'NS', str(id), - WaitForStatus.TIMEOUT_NS_OPERATION, + wait_time, apiUrlStatus, self._http.get2_cmd, deleteFlag=deleteFlag) @@ -53,18 +58,20 @@ class Ns(object): def list(self, filter=None): """Returns a list of NS """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns an NS based on name or id """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for ns in self.list(): @@ -74,9 +81,10 @@ class Ns(object): for ns in self.list(): if name == ns['name']: return ns - raise NotFound("ns {} not found".format(name)) + raise NotFound("ns '{}' not found".format(name)) def get_individual(self, name): + self._logger.debug("") self._client.get_token() ns_id = name if not utils.validate_uuid4(name): @@ -84,43 +92,69 @@ class Ns(object): if name == ns['name']: ns_id = ns['_id'] break - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, ns_id)) - #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, ns_id)) - #print(yaml.safe_dump(resp)) - if resp: - return resp - raise NotFound("ns {} not found".format(name)) + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, ns_id)) + #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, ns_id)) + #print(yaml.safe_dump(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("ns '{}' not found".format(name)) + raise NotFound("ns '{}' not found".format(name)) - def delete(self, name, force=False, wait=False): + def delete(self, name, force=False, config=None, wait=False): + """ + Deletes a Network Service (NS) + :param name: name of network service + :param force: set force. Direct deletion without cleaning at VIM + :param config: parameters of deletion, as: + autoremove: Bool (default True) + timeout_ns_terminate: int + skip_terminate_primitives: Bool (default False) to not exec the terminate primitives + :param wait: Make synchronous. Wait until deletion is completed: + False to not wait (by default), True to wait a standard time, or int (time to wait) + :return: None. Exception if fail + """ + self._logger.debug("") ns = self.get(name) + querystring_list = [] querystring = '' + if config: + ns_config = yaml.safe_load(config) + querystring_list += ["{}={}".format(k, v) for k, v in ns_config.items()] if force: - querystring = '?FORCE=True' + querystring_list.append('FORCE=True') + if querystring_list: + querystring = "?" + "&".join(querystring_list) http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, ns['_id'], querystring)) + # TODO change to use a POST self._http.post_cmd('{}/{}/terminate{}'.format(_apiBase, ns['_id'], querystring), + # postfields_dict=ns_config) + # seting autoremove as True by default # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) if http_code == 202: if wait and resp: resp = json.loads(resp) # For the 'delete' operation, '_id' is used - self._wait(resp.get('_id'), deleteFlag=True) + self._wait(resp.get('_id'), wait, deleteFlag=True) else: print('Deletion in progress') elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete ns {} - {}".format(name, msg)) def create(self, nsd_name, nsr_name, account, config=None, ssh_keys=None, description='default description', admin_status='ENABLED', wait=False): + self._logger.debug("") self._client.get_token() nsd = self._client.nsd.get(nsd_name) @@ -128,9 +162,9 @@ class Ns(object): wim_account_id = {} def get_vim_account_id(vim_account): + self._logger.debug("") if vim_account_id.get(vim_account): return vim_account_id[vim_account] - vim = self._client.vim.get(vim_account) if vim is None: raise NotFound("cannot find vim account '{}'".format(vim_account)) @@ -138,11 +172,12 @@ class Ns(object): return vim['_id'] def get_wim_account_id(wim_account): + self._logger.debug("") + # wim_account can be False (boolean) to indicate not use wim account if not isinstance(wim_account, str): return wim_account if wim_account_id.get(wim_account): return wim_account_id[wim_account] - wim = self._client.wim.get(wim_account) if wim is None: raise NotFound("cannot find wim account '{}'".format(wim_account)) @@ -168,40 +203,44 @@ class Ns(object): if "vim-network-name" in ns_config: ns_config["vld"] = ns_config.pop("vim-network-name") if "vld" in ns_config: + if not isinstance(ns_config["vld"], list): + raise ClientException("Error at --config 'vld' must be a list of dictionaries") for vld in ns_config["vld"]: + if not isinstance(vld, dict): + raise ClientException("Error at --config 'vld' must be a list of dictionaries") if vld.get("vim-network-name"): if isinstance(vld["vim-network-name"], dict): vim_network_name_dict = {} - for vim_account, vim_net in list(vld["vim-network-name"].items()): + for vim_account, vim_net in vld["vim-network-name"].items(): vim_network_name_dict[get_vim_account_id(vim_account)] = vim_net vld["vim-network-name"] = vim_network_name_dict if "wim_account" in vld and vld["wim_account"] is not None: vld["wimAccountId"] = get_wim_account_id(vld.pop("wim_account")) - ns["vld"] = ns_config["vld"] if "vnf" in ns_config: for vnf in ns_config["vnf"]: if vnf.get("vim_account"): vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) - ns["vnf"] = ns_config["vnf"] if "additionalParamsForNs" in ns_config: - ns["additionalParamsForNs"] = ns_config.pop("additionalParamsForNs") - if not isinstance(ns["additionalParamsForNs"], dict): - raise ValueError("Error at --config 'additionalParamsForNs' must be a dictionary") + if not isinstance(ns_config["additionalParamsForNs"], dict): + raise ClientException("Error at --config 'additionalParamsForNs' must be a dictionary") if "additionalParamsForVnf" in ns_config: - ns["additionalParamsForVnf"] = ns_config.pop("additionalParamsForVnf") - if not isinstance(ns["additionalParamsForVnf"], list): - raise ValueError("Error at --config 'additionalParamsForVnf' must be a list") - for additional_param_vnf in ns["additionalParamsForVnf"]: + if not isinstance(ns_config["additionalParamsForVnf"], list): + raise ClientException("Error at --config 'additionalParamsForVnf' must be a list") + for additional_param_vnf in ns_config["additionalParamsForVnf"]: if not isinstance(additional_param_vnf, dict): - raise ValueError("Error at --config 'additionalParamsForVnf' items must be dictionaries") + raise ClientException("Error at --config 'additionalParamsForVnf' items must be dictionaries") if not additional_param_vnf.get("member-vnf-index"): - raise ValueError("Error at --config 'additionalParamsForVnf' items must contain " + raise ClientException("Error at --config 'additionalParamsForVnf' items must contain " "'member-vnf-index'") if "wim_account" in ns_config: wim_account = ns_config.pop("wim_account") if wim_account is not None: ns['wimAccountId'] = get_wim_account_id(wim_account) + # rest of parameters without any transformation or checking + # "timeout_ns_deploy" + # "placement-engine" + ns.update(ns_config) # print(yaml.safe_dump(ns)) try: @@ -214,27 +253,28 @@ class Ns(object): for (key,val) in list(headers.items())] self._http.set_http_header(http_header) http_code, resp = self._http.post_cmd(endpoint=self._apiBase, - postfields_dict=ns) + postfields_dict=ns) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {} '.format( + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {} '.format( resp)) - if wait: - # Wait for status for NS instance creation - self._wait(resp.get('nslcmop_id')) - return resp['id'] - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException(msg) + if wait: + # Wait for status for NS instance creation + self._wait(resp.get('nslcmop_id'), wait) + print(resp['id']) + return resp['id'] + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to create ns: {} nsd: {}\nerror:\n{}".format( nsr_name, @@ -245,6 +285,7 @@ class Ns(object): def list_op(self, name, filter=None): """Returns the list of operations of a NS """ + self._logger.debug("") ns = self.get(name) try: self._apiResource = '/ns_lcm_op_occs' @@ -252,8 +293,8 @@ class Ns(object): self._apiVersion, self._apiResource) filter_string = '' if filter: - filter_string = '&{}'.format(filter) - http_code, resp = self._http.get2_cmd('{}?nsInstanceId={}'.format( + filter_string = '&{}'.format(filter) + http_code, resp = self._http.get2_cmd('{}?nsInstanceId={}{}'.format( self._apiBase, ns['_id'], filter_string) ) #print('HTTP CODE: {}'.format(http_code)) @@ -265,13 +306,13 @@ class Ns(object): else: raise ClientException('unexpected response from server') else: - msg = "" - if resp: - try: - resp = json.loads(resp) - msg = resp['detail'] - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # resp = json.loads(resp) + # msg = resp['detail'] + # except ValueError: + # msg = resp raise ClientException(msg) except ClientException as exc: message="failed to get operation list of NS {}:\nerror:\n{}".format( @@ -282,6 +323,7 @@ class Ns(object): def get_op(self, operationId): """Returns the status of an operation """ + self._logger.debug("") self._client.get_token() try: self._apiResource = '/ns_lcm_op_occs' @@ -297,13 +339,13 @@ class Ns(object): else: raise ClientException('unexpected response from server') else: - msg = "" - if resp: - try: - resp = json.loads(resp) - msg = resp['detail'] - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # resp = json.loads(resp) + # msg = resp['detail'] + # except ValueError: + # msg = resp raise ClientException(msg) except ClientException as exc: message="failed to get status of operation {}:\nerror:\n{}".format( @@ -314,6 +356,8 @@ class Ns(object): def exec_op(self, name, op_name, op_data=None, wait=False, ): """Executes an operation on a NS """ + self._logger.debug("") + ns = self.get(name) try: ns = self.get(name) self._apiResource = '/ns_instances' @@ -325,47 +369,52 @@ class Ns(object): http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=op_data) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - if wait: - # Wait for status for NS instance action - # For the 'action' operation, 'id' is used - self._wait(resp.get('id')) - return resp['id'] - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException(msg) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + if wait: + # Wait for status for NS instance action + # For the 'action' operation, 'id' is used + self._wait(resp.get('id'), wait) + return resp['id'] + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to exec operation {}:\nerror:\n{}".format( name, str(exc)) raise ClientException(message) - def scale_vnf(self, ns_name, vnf_name, scaling_group, scale_in, scale_out, wait=False): + def scale_vnf(self, ns_name, vnf_name, scaling_group, scale_in, scale_out, wait=False, timeout=None): """Scales a VNF by adding/removing VDUs """ + self._logger.debug("") self._client.get_token() try: op_data={} op_data["scaleType"] = "SCALE_VNF" op_data["scaleVnfData"] = {} - if scale_in: + if scale_in and not scale_out: op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_IN" - else: + elif not scale_in and scale_out: op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_OUT" + else: + raise ClientException("you must set either 'scale_in' or 'scale_out'") op_data["scaleVnfData"]["scaleByStepData"] = { "member-vnf-index": vnf_name, "scaling-group-descriptor": scaling_group, } + if timeout: + op_data["timeout_ns_scale"] = timeout op_id = self.exec_op(ns_name, op_name='scale', op_data=op_data, wait=wait) print(str(op_id)) except ClientException as exc: @@ -374,6 +423,7 @@ class Ns(object): raise ClientException(message) def create_alarm(self, alarm): + self._logger.debug("") self._client.get_token() data = {} data["create_alarm_request"] = {} @@ -383,18 +433,18 @@ class Ns(object): postfields_dict=data) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - #resp = json.loads(resp) - print('Alarm created') - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException('error: code: {}, resp: {}'.format( - http_code, msg)) + # if http_code in (200, 201, 202, 204): + # resp = json.loads(resp) + print('Alarm created') + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException('error: code: {}, resp: {}'.format( + # http_code, msg)) except ClientException as exc: message="failed to create alarm: alarm {}\n{}".format( alarm, @@ -402,6 +452,7 @@ class Ns(object): raise ClientException(message) def delete_alarm(self, name): + self._logger.debug("") self._client.get_token() data = {} data["delete_alarm_request"] = {} @@ -412,18 +463,18 @@ class Ns(object): postfields_dict=data) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - #resp = json.loads(resp) - print('Alarm deleted') - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException('error: code: {}, resp: {}'.format( - http_code, msg)) + # if http_code in (200, 201, 202, 204): + # resp = json.loads(resp) + print('Alarm deleted') + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException('error: code: {}, resp: {}'.format( + # http_code, msg)) except ClientException as exc: message="failed to delete alarm: alarm {}\n{}".format( name, @@ -431,6 +482,7 @@ class Ns(object): raise ClientException(message) def export_metric(self, metric): + self._logger.debug("") self._client.get_token() data = {} data["read_metric_data_request"] = metric @@ -439,18 +491,18 @@ class Ns(object): postfields_dict=data) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - #resp = json.loads(resp) - return 'Metric exported' - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException('error: code: {}, resp: {}'.format( - http_code, msg)) + # if http_code in (200, 201, 202, 204): + # resp = json.loads(resp) + return 'Metric exported' + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException('error: code: {}, resp: {}'.format( + # http_code, msg)) except ClientException as exc: message="failed to export metric: metric {}\n{}".format( metric, @@ -458,6 +510,7 @@ class Ns(object): raise ClientException(message) def get_field(self, ns_name, field): + self._logger.debug("") nsr = self.get(ns_name) print(yaml.safe_dump(nsr)) if nsr is None: @@ -467,3 +520,4 @@ class Ns(object): return nsr[field] raise NotFound("failed to find {} in ns {}".format(field, ns_name)) + diff --git a/osmclient/sol005/nsd.py b/osmclient/sol005/nsd.py index 9adc8f7..9aca930 100644 --- a/osmclient/sol005/nsd.py +++ b/osmclient/sol005/nsd.py @@ -24,6 +24,8 @@ from osmclient.common import utils import json import magic from os.path import basename +import logging +import os.path #from os import stat @@ -32,6 +34,7 @@ class Nsd(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/nsd' self._apiVersion = '/v1' self._apiResource = '/ns_descriptors' @@ -40,17 +43,19 @@ class Nsd(object): #self._apiBase='/nsds' def list(self, filter=None): + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase, filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase, filter_string)) #print(yaml.safe_dump(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for nsd in self.list(): @@ -63,46 +68,56 @@ class Nsd(object): raise NotFound("nsd {} not found".format(name)) def get_individual(self, name): - # Called to get_token not required, because will be implicitly called by get. - nsd = self.get(name) - # It is redundant, since the previous one already gets the whole nsdinfo - # The only difference is that a different primitive is exercised - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nsd['_id'])) - #print(yaml.safe_dump(resp)) - if resp: - return resp - raise NotFound("nsd {} not found".format(name)) + self._logger.debug("") + # Call to get_token not required, because will be implicitly called by get. + try: + nsd = self.get(name) + # It is redundant, since the previous one already gets the whole nsdinfo + # The only difference is that a different primitive is exercised + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, nsd['_id'])) + #print(yaml.safe_dump(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("nsd '{}' not found".format(name)) + raise NotFound("nsd '{}' not found".format(name)) def get_thing(self, name, thing, filename): + self._logger.debug("") + # Call to get_token not required, because will be implicitly called by get. nsd = self.get(name) headers = self._client._headers headers['Accept'] = 'application/binary' http_code, resp = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, nsd['_id'], thing)) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - #store in a file - return resp + #if http_code in (200, 201, 202, 204): + if resp: + #store in a file + return json.loads(resp) else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) def get_descriptor(self, name, filename): + self._logger.debug("") self.get_thing(name, 'nsd', filename) def get_package(self, name, filename): + self._logger.debug("") self.get_thing(name, 'package_content', filename) def get_artifact(self, name, artifact, filename): + self._logger.debug("") self.get_thing(name, 'artifacts/{}'.format(artifact), filename) def delete(self, name, force=False): + self._logger.debug("") nsd = self.get(name) querystring = '' if force: @@ -116,72 +131,80 @@ class Nsd(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete nsd {} - {}".format(name, msg)) - def create(self, filename, overwrite=None, update_endpoint=None): - self._client.get_token() - mime_type = magic.from_file(filename, mime=True) - if mime_type is None: - raise ClientException( - "failed to guess MIME type for file '{}'".format(filename)) - headers= self._client._headers - headers['Content-Filename'] = basename(filename) - if mime_type in ['application/yaml', 'text/plain', 'application/json']: - headers['Content-Type'] = 'text/plain' - elif mime_type in ['application/gzip', 'application/x-gzip']: - headers['Content-Type'] = 'application/gzip' - #headers['Content-Type'] = 'application/binary' - # Next three lines are to be removed in next version - #headers['Content-Filename'] = basename(filename) - #file_size = stat(filename).st_size - #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + def create(self, filename, overwrite=None, update_endpoint=None, skip_charm_build=False): + self._logger.debug("") + if os.path.isdir(filename): + filename = filename.rstrip('/') + filename = self._client.package_tool.build(filename, skip_validation=False, skip_charm_build=skip_charm_build) + self.create(filename, overwrite=overwrite, update_endpoint=update_endpoint) else: - raise ClientException( + self._client.get_token() + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers= self._client._headers + headers['Content-Filename'] = basename(filename) + if mime_type in ['application/yaml', 'text/plain', 'application/json']: + headers['Content-Type'] = 'text/plain' + elif mime_type in ['application/gzip', 'application/x-gzip']: + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + else: + raise ClientException( "Unexpected MIME type for file {}: MIME type {}".format( filename, mime_type) ) - headers["Content-File-MD5"] = utils.md5(filename) - http_header = ['{}: {}'.format(key,val) - for (key,val) in list(headers.items())] - self._http.set_http_header(http_header) - if update_endpoint: - http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) - else: - ow_string = '' - if overwrite: - ow_string = '?{}'.format(overwrite) - self._apiResource = '/ns_descriptors_content' - self._apiBase = '{}{}{}'.format(self._apiName, - self._apiVersion, self._apiResource) - endpoint = '{}{}'.format(self._apiBase,ow_string) - http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) - #print('HTTP CODE: {}'.format(http_code)) - #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - elif http_code == 204: - print('Updated') - else: - msg = "Error {}".format(http_code) - if resp: - try: - msg = "{} - {}".format(msg, json.loads(resp)) - except ValueError: - msg = "{} - {}".format(msg, resp) - raise ClientException("failed to create/update nsd - {}".format(msg)) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + if update_endpoint: + http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/ns_descriptors_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print('HTTP CODE: {}'.format(http_code)) + #print('RESP: {}'.format(resp)) + if http_code in (200, 201, 202): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format(resp)) + print(resp['id']) + elif http_code == 204: + print('Updated') + # else: + # msg = "Error {}".format(http_code) + # if resp: + # try: + # msg = "{} - {}".format(msg, json.loads(resp)) + # except ValueError: + # msg = "{} - {}".format(msg, resp) + # raise ClientException("failed to create/update nsd - {}".format(msg)) def update(self, name, filename): + self._logger.debug("") nsd = self.get(name) endpoint = '{}/{}/nsd_content'.format(self._apiBase, nsd['_id']) self.create(filename=filename, update_endpoint=endpoint) diff --git a/osmclient/sol005/nsi.py b/osmclient/sol005/nsi.py index 0fcfe1d..20065b7 100644 --- a/osmclient/sol005/nsi.py +++ b/osmclient/sol005/nsi.py @@ -24,6 +24,7 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml import json +import logging class Nsi(object): @@ -31,6 +32,7 @@ class Nsi(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/nsilcm' self._apiVersion = '/v1' self._apiResource = '/netslice_instances_content' @@ -38,15 +40,18 @@ class Nsi(object): self._apiVersion, self._apiResource) # NSI '--wait' option - def _wait(self, id, deleteFlag=False): + def _wait(self, id, wait_time, deleteFlag=False): + self._logger.debug("") self._client.get_token() # Endpoint to get operation status apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/nsi_lcm_op_occs') # Wait for status for NSI instance creation/update/deletion + if isinstance(wait_time, bool): + wait_time = WaitForStatus.TIMEOUT_NSI_OPERATION WaitForStatus.wait_for_status( 'NSI', str(id), - WaitForStatus.TIMEOUT_NSI_OPERATION, + wait_time, apiUrlStatus, self._http.get2_cmd, deleteFlag=deleteFlag) @@ -54,18 +59,20 @@ class Nsi(object): def list(self, filter=None): """Returns a list of NSI """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns an NSI based on name or id """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for nsi in self.list(): @@ -78,6 +85,7 @@ class Nsi(object): raise NotFound("nsi {} not found".format(name)) def get_individual(self, name): + self._logger.debug("") nsi_id = name self._client.get_token() if not utils.validate_uuid4(name): @@ -85,14 +93,18 @@ class Nsi(object): if name == nsi['name']: nsi_id = nsi['_id'] break - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nsi_id)) - #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, nsi_id)) - #print(yaml.safe_dump(resp)) - if resp: - return resp + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, nsi_id)) + #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, nsi_id)) + #print(yaml.safe_dump(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("nsi '{}' not found".format(name)) raise NotFound("nsi {} not found".format(name)) def delete(self, name, force=False, wait=False): + self._logger.debug("") nsi = self.get(name) querystring = '' if force: @@ -106,30 +118,32 @@ class Nsi(object): resp = json.loads(resp) # Wait for status for NSI instance deletion # For the 'delete' operation, '_id' is used - self._wait(resp.get('_id'), deleteFlag=True) + self._wait(resp.get('_id'), wait, deleteFlag=True) else: print('Deletion in progress') elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete nsi {} - {}".format(name, msg)) def create(self, nst_name, nsi_name, account, config=None, ssh_keys=None, description='default description', admin_status='ENABLED', wait=False): + self._logger.debug("") self._client.get_token() nst = self._client.nst.get(nst_name) vim_account_id = {} def get_vim_account_id(vim_account): + self._logger.debug("") if vim_account_id.get(vim_account): return vim_account_id[vim_account] @@ -181,7 +195,7 @@ class Nsi(object): vim_network_name_dict[get_vim_account_id(vim_account)] = vim_net vld["vim-network-name"] = vim_network_name_dict if "vnf" in nssubnet: - for vnf in nsi_config["vnf"]: + for vnf in nssubnet["vnf"]: if vnf.get("vim_account"): vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) nsi["netslice-subnet"] = nsi_config["netslice-subnet"] @@ -203,6 +217,8 @@ class Nsi(object): not additional_param_subnet.get("additionalParamsForVnf"): raise ValueError("Error at --config 'additionalParamsForSubnet' items must contain " "'additionalParamsForNs' and/or 'additionalParamsForVnf'") + if "timeout_nsi_deploy" in nsi_config: + nsi["timeout_nsi_deploy"] = nsi_config.pop("timeout_nsi_deploy") # print(yaml.safe_dump(nsi)) try: @@ -215,27 +231,27 @@ class Nsi(object): for (key,val) in list(headers.items())] self._http.set_http_header(http_header) http_code, resp = self._http.post_cmd(endpoint=self._apiBase, - postfields_dict=nsi) + postfields_dict=nsi) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {} '.format( - resp)) - if wait: - # Wait for status for NSI instance creation - self._wait(resp.get('nsilcmop_id')) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException(msg) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {} '.format( + resp)) + if wait: + # Wait for status for NSI instance creation + self._wait(resp.get('nsilcmop_id'), wait) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to create nsi: {} nst: {}\nerror:\n{}".format( nsi_name, @@ -246,6 +262,7 @@ class Nsi(object): def list_op(self, name, filter=None): """Returns the list of operations of a NSI """ + self._logger.debug("") nsi = self.get(name) try: self._apiResource = '/nsi_lcm_op_occs' @@ -254,26 +271,26 @@ class Nsi(object): filter_string = '' if filter: filter_string = '&{}'.format(filter) - http_code, resp = self._http.get2_cmd('{}?netsliceInstanceId={}'.format( + http_code, resp = self._http.get2_cmd('{}?netsliceInstanceId={}{}'.format( self._apiBase, nsi['_id'], filter_string) ) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code == 200: - if resp: - resp = json.loads(resp) - return resp - else: - raise ClientException('unexpected response from server') + #if http_code == 200: + if resp: + resp = json.loads(resp) + return resp else: - msg = "" - if resp: - try: - resp = json.loads(resp) - msg = resp['detail'] - except ValueError: - msg = resp - raise ClientException(msg) + raise ClientException('unexpected response from server') + #else: + # msg = "" + # if resp: + # try: + # resp = json.loads(resp) + # msg = resp['detail'] + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to get operation list of NSI {}:\nerror:\n{}".format( name, @@ -283,6 +300,7 @@ class Nsi(object): def get_op(self, operationId): """Returns the status of an operation """ + self._logger.debug("") self._client.get_token() try: self._apiResource = '/nsi_lcm_op_occs' @@ -291,21 +309,21 @@ class Nsi(object): http_code, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, operationId)) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code == 200: - if resp: - resp = json.loads(resp) - return resp - else: - raise ClientException('unexpected response from server') + #if http_code == 200: + if resp: + resp = json.loads(resp) + return resp else: - msg = "" - if resp: - try: - resp = json.loads(resp) - msg = resp['detail'] - except ValueError: - msg = resp - raise ClientException(msg) + raise ClientException('unexpected response from server') + #else: + # msg = "" + # if resp: + # try: + # resp = json.loads(resp) + # msg = resp['detail'] + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to get status of operation {}:\nerror:\n{}".format( operationId, @@ -315,6 +333,7 @@ class Nsi(object): def exec_op(self, name, op_name, op_data=None): """Executes an operation on a NSI """ + self._logger.debug("") nsi = self.get(name) try: self._apiResource = '/netslice_instances' @@ -326,21 +345,21 @@ class Nsi(object): http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=op_data) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException(msg) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException(msg) except ClientException as exc: message="failed to exec operation {}:\nerror:\n{}".format( name, diff --git a/osmclient/sol005/nst.py b/osmclient/sol005/nst.py index f0da0f5..214a79d 100644 --- a/osmclient/sol005/nst.py +++ b/osmclient/sol005/nst.py @@ -23,6 +23,8 @@ from osmclient.common.exceptions import ClientException from osmclient.common import utils import json import magic +import logging +import os.path #from os import stat #from os.path import basename @@ -31,6 +33,7 @@ class Nst(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/nst' self._apiVersion = '/v1' self._apiResource = '/netslice_templates' @@ -38,17 +41,19 @@ class Nst(object): self._apiVersion, self._apiResource) def list(self, filter=None): + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase, filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase, filter_string)) #print(yaml.safe_dump(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for nst in self.list(): @@ -61,45 +66,57 @@ class Nst(object): raise NotFound("nst {} not found".format(name)) def get_individual(self, name): + self._logger.debug("") nst = self.get(name) # It is redundant, since the previous one already gets the whole nstinfo # The only difference is that a different primitive is exercised - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nst['_id'])) - #print(yaml.safe_dump(resp)) - if resp: - return resp - raise NotFound("nst {} not found".format(name)) + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, nst['_id'])) + #print(yaml.safe_dump(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("nst '{}' not found".format(name)) + raise NotFound("nst '{}' not found".format(name)) def get_thing(self, name, thing, filename): + self._logger.debug("") nst = self.get(name) headers = self._client._headers headers['Accept'] = 'application/binary' - http_code, resp = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, nst['_id'], thing)) + try: + http_code, resp = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, nst['_id'], thing)) + except NotFound: + raise NotFound("nst '{} 'not found".format(name)) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - #store in a file - return resp - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + #store in a file + return json.loads(resp) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) def get_descriptor(self, name, filename): + self._logger.debug("") self.get_thing(name, 'nst', filename) def get_package(self, name, filename): + self._logger.debug("") self.get_thing(name, 'nst_content', filename) def get_artifact(self, name, artifact, filename): + self._logger.debug("") self.get_thing(name, 'artifacts/{}'.format(artifact), filename) def delete(self, name, force=False): + self._logger.debug("") nst = self.get(name) querystring = '' if force: @@ -113,69 +130,88 @@ class Nst(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - resp = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # resp = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete nst {} - {}".format(name, msg)) def create(self, filename, overwrite=None, update_endpoint=None): - self._client.get_token() - mime_type = magic.from_file(filename, mime=True) - if mime_type is None: - raise ClientException( - "failed to guess MIME type for file '{}'".format(filename)) - headers= self._client._headers - if mime_type in ['application/yaml', 'text/plain']: - headers['Content-Type'] = 'application/yaml' - elif mime_type in ['application/gzip', 'application/x-gzip']: - headers['Content-Type'] = 'application/gzip' - #headers['Content-Type'] = 'application/binary' - # Next three lines are to be removed in next version - #headers['Content-Filename'] = basename(filename) - #file_size = stat(filename).st_size - #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + self._logger.debug("") + if os.path.isdir(filename): + charm_folder = filename.rstrip('/') + for files in os.listdir(charm_folder): + if "nst.yaml" in files: + results = self._client.package_tool.validate(charm_folder, recursive=False) + for result in results: + if result["valid"] != "OK": + raise ClientException('There was an error validating the file: {} ' + 'with error: {}'.format(result["path"], result["error"])) + result = self._client.package_tool.build(charm_folder) + if 'Created' in result: + filename = "{}.tar.gz".format(charm_folder) + else: + raise ClientException('Failed in {}tar.gz creation'.format(charm_folder)) + self.create(filename, overwrite, update_endpoint) else: - raise ClientException( - "Unexpected MIME type for file {}: MIME type {}".format( - filename, mime_type) - ) - headers["Content-File-MD5"] = utils.md5(filename) - http_header = ['{}: {}'.format(key,val) - for (key,val) in list(headers.items())] - self._http.set_http_header(http_header) - if update_endpoint: - http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) - else: - ow_string = '' - if overwrite: - ow_string = '?{}'.format(overwrite) - self._apiResource = '/netslice_templates_content' - self._apiBase = '{}{}{}'.format(self._apiName, - self._apiVersion, self._apiResource) - endpoint = '{}{}'.format(self._apiBase,ow_string) - http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) - #print('HTTP CODE: {}'.format(http_code)) - #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): + self._client.get_token() + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers= self._client._headers + if mime_type in ['application/yaml', 'text/plain']: + headers['Content-Type'] = 'application/yaml' + elif mime_type in ['application/gzip', 'application/x-gzip']: + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + else: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + if update_endpoint: + http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/netslice_templates_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print('HTTP CODE: {}'.format(http_code)) + #print('RESP: {}'.format(resp)) + # if http_code in (200, 201, 202, 204): if resp: resp = json.loads(resp) if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) + raise ClientException('unexpected response from server - {}'.format(resp)) print(resp['id']) - else: - msg = "Error {}".format(http_code) - if resp: - try: - msg = "{} - {}".format(msg, json.loads(resp)) - except ValueError: - msg = "{} - {}".format(msg, resp) - raise ClientException("failed to create/update nst - {}".format(msg)) + # else: + # msg = "Error {}".format(http_code) + # if resp: + # try: + # msg = "{} - {}".format(msg, json.loads(resp)) + # except ValueError: + # msg = "{} - {}".format(msg, resp) + # raise ClientException("failed to create/update nst - {}".format(msg)) def update(self, name, filename): + self._logger.debug("") nst = self.get(name) endpoint = '{}/{}/nst_content'.format(self._apiBase, nst['_id']) self.create(filename=filename, update_endpoint=endpoint) diff --git a/osmclient/sol005/osmrepo.py b/osmclient/sol005/osmrepo.py new file mode 100644 index 0000000..b4d49cf --- /dev/null +++ b/osmclient/sol005/osmrepo.py @@ -0,0 +1,379 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +""" +OSM Repo API handling +""" +from osmclient.common.exceptions import ClientException +from osmclient.sol005.repo import Repo +from osmclient.common.package_tool import PackageTool +import requests +import logging +import tempfile +from shutil import copyfile, rmtree +import yaml +import tarfile +import glob +from packaging import version as versioning +import time +from os import listdir, mkdir, getcwd, remove +from os.path import isfile, isdir, join, abspath +import hashlib +from osm_im.validation import Validation as validation_im +import ruamel.yaml + + +class OSMRepo(Repo): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/osmrepos' + self._logger = logging.getLogger('osmclient') + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + def pkg_list(self, pkgtype, filter=None, repo=None): + """ + Returns a repo based on name or id + """ + self._logger.debug("") + self._client.get_token() + # Get OSM registered repository list + repositories = self.list() + if repo: + repositories = [r for r in repositories if r["name"] == repo] + if not repositories: + raise ClientException('Not repository found') + + vnf_repos = [] + for repository in repositories: + try: + r = requests.get('{}/index.yaml'.format(repository.get('url'))) + + if r.status_code == 200: + repo_list = yaml.safe_load(r.text) + vnf_packages = repo_list.get('{}_packages'.format(pkgtype)) + for repo in vnf_packages: + versions = vnf_packages.get(repo) + latest = versions.get('latest') + del versions['latest'] + for version in versions: + latest_version = False + if version == latest: + latest_version = True + vnf_repos.append({'vendor': versions[version].get("vendor"), + 'name': versions[version].get("name"), + 'version': version, + 'description': versions[version].get("description"), + 'location': versions[version].get("path"), + 'repository': repository.get('name'), + 'repourl': repository.get('url'), + 'latest': latest_version + }) + else: + raise Exception('repository in url {} unreachable'.format(repository.get('url'))) + except Exception as e: + logging.error("Error cannot read from repository {} '{}': {}".format(repository['name'], repository['url'], e)) + continue + + vnf_repos_filtered = [] + if filter: + for vnf_repo in vnf_repos: + for k, v in vnf_repo.items(): + if v: + kf, vf = filter.split('=') + if k == kf and vf in v: + vnf_repos_filtered.append(vnf_repo) + break + vnf_repos = vnf_repos_filtered + return vnf_repos + + def get_pkg(self, pkgtype, name, repo, filter, version): + """ + Returns the filename of the PKG downloaded to disk + """ + self._logger.debug("") + self._client.get_token() + f = None + f_name = None + # Get OSM registered repository list + pkgs = self.pkg_list(pkgtype, filter, repo) + for pkg in pkgs: + if pkg.get('repository') == repo and pkg.get('name') == name: + if 'latest' in version: + if not pkg.get('latest'): + continue + else: + version = pkg.get('version') + if pkg.get('version') == version: + r = requests.get('{}{}'.format(pkg.get('repourl'), pkg.get('location')), stream=True) + if r.status_code != 200: + raise ClientException("Package not found") + + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(r.raw.read()) + f_name = f.name + if not f_name: + raise ClientException("{} {} not found at repo {}".format(pkgtype,name, repo)) + return f_name + + def pkg_get(self, pkgtype, name, repo, version, filter): + + pkg_name = self.get_pkg(pkgtype, name, repo, filter, version) + if not pkg_name: + raise ClientException('Package not found') + folder, descriptor = self.zip_extraction(pkg_name) + with open(descriptor) as pkg: + pkg_descriptor = yaml.safe_load(pkg) + rmtree(folder, ignore_errors=False) + if ((pkgtype == 'vnf' and (pkg_descriptor.get('vnfd') or pkg_descriptor.get('vnfd:vnfd_catalog'))) or + (pkgtype == 'ns' and (pkg_descriptor.get('nsd') or pkg_descriptor.get('nsd:nsd_catalog')))): + raise ClientException('Wrong Package type') + return pkg_descriptor + + def repo_index(self, origin=".", destination='.'): + """ + Repo Index main function + :param origin: origin directory for getting all the artifacts + :param destination: destination folder for create and index the valid artifacts + """ + if destination == '.': + if origin == destination: + destination = 'repository' + + destination = abspath(destination) + origin = abspath(origin) + + if origin[0] != '/': + origin = join(getcwd(), origin) + if destination[0] != '/': + destination = join(getcwd(), destination) + + self.init_directory(destination) + artifacts = [f for f in listdir(origin) if isfile(join(origin, f))] + directories = [f for f in listdir(origin) if isdir(join(origin, f))] + for artifact in artifacts: + self.register_artifact_in_repository(join(origin, artifact), destination, source='file') + for artifact in directories: + self.register_artifact_in_repository(join(origin, artifact), destination, source='directory') + print("\nFinal Results: ") + print("VNF Packages Indexed: " + str(len(glob.glob(destination + "/vnf/*/*/metadata.yaml")))) + print("NS Packages Indexed: " + str(len(glob.glob(destination + "/ns/*/*/metadata.yaml")))) + + def md5(self, fname): + """ + Checksum generator + :param fname: file path + :return: checksum string + """ + hash_md5 = hashlib.md5() + with open(fname, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + def fields_building(self, descriptor_json, file, package_type): + """ + From an artifact descriptor, obtain the fields required for indexing + :param descriptor_json: artifact description + :param file: artifact package + :param package_type: type of artifact (vnf or ns) + :return: fields + """ + fields = {} + base_path = '/{}/'.format(package_type) + aux_dict = {} + if package_type == "vnf": + if descriptor_json.get('vnfd-catalog', False): + aux_dict = descriptor_json.get('vnfd-catalog', {}).get('vnfd', [{}])[0] + else: + aux_dict = descriptor_json.get('vnfd:vnfd-catalog', {}).get('vnfd', [{}])[0] + + images = [] + for vdu in aux_dict.get('vdu', ()): + images.append(vdu.get('image')) + fields['images'] = images + if package_type == "ns": + if descriptor_json.get('nsd-catalog', False): + aux_dict = descriptor_json.get('nsd-catalog', {}).get('nsd', [{}])[0] + else: + aux_dict = descriptor_json.get('nsd:nsd-catalog', {}).get('nsd', [{}])[0] + + vnfs = [] + + for vnf in aux_dict.get('constituent-vnfd', ()): + vnfs.append(vnf.get('vnfd-id-ref')) + self._logger.debug('Used VNFS in the NSD: ' + str(vnfs)) + fields['vnfd-id-ref'] = vnfs + + fields['name'] = aux_dict.get('name') + fields['id'] = aux_dict.get('id') + fields['description'] = aux_dict.get('description') + fields['vendor'] = aux_dict.get('vendor') + fields['version'] = aux_dict.get('version', '1.0') + fields['path'] = "{}{}/{}/{}-{}.tar.gz".format(base_path, fields['id'], fields['version'], fields.get('id'), \ + fields.get('version')) + return fields + + def zip_extraction(self, file_name): + """ + Validation of artifact. + :param file: file path + :return: status details, status, fields, package_type + """ + self._logger.debug("Decompressing package file") + temp_file = '/tmp/{}'.format(file_name.split('/')[-1]) + if file_name != temp_file: + copyfile(file_name, temp_file) + with tarfile.open(temp_file, "r:gz") as tar: + folder = tar.getnames()[0].split('/')[0] + tar.extractall() + + remove(temp_file) + descriptor_file = glob.glob('{}/*.y*ml'.format(folder))[0] + return folder, descriptor_file + + def validate_artifact(self, path, source): + """ + Validation of artifact. + :param path: file path + :return: status details, status, fields, package_type + """ + package_type = '' + folder = '' + try: + if source == 'directory': + descriptor_file = glob.glob('{}/*.y*ml'.format(path))[0] + else: + folder, descriptor_file = self.zip_extraction(path) + + self._logger.debug("Opening descriptor file: {}".format(descriptor_file)) + + with open(descriptor_file, 'r') as f: + descriptor_data = f.read() + validation = validation_im() + desc_type, descriptor_data = validation.yaml_validation(descriptor_data) + validation_im.pyangbind_validation(self, desc_type, descriptor_data) + if 'vnf' in list(descriptor_data.keys())[0]: + package_type = 'vnf' + else: + # raise ClientException("Not VNF package") + package_type = 'ns' + + self._logger.debug("Descriptor: {}".format(descriptor_data)) + fields = self.fields_building(descriptor_data, path, package_type) + self._logger.debug("Descriptor sucessfully validated") + return {"detail": "{}D successfully validated".format(package_type.upper()), + "code": "OK"}, True, fields, package_type + except Exception as e: + # Delete the folder we just created + return {"detail": str(e)}, False, {}, package_type + finally: + if folder: + rmtree(folder, ignore_errors=True) + + def register_artifact_in_repository(self, path, destination, source): + """ + Registration of one artifact in a repository + file: VNF or NS + destination: path for index creation + """ + pt = PackageTool() + compresed = False + try: + fields = {} + _, valid, fields, package_type = self.validate_artifact(path, source) + if not valid: + raise Exception('{} {} Not well configured.'.format(package_type.upper(), str(path))) + else: + if source == 'directory': + path = pt.build(path) + compresed = True + fields['checksum'] = self.md5(path) + self.indexation(destination, path, package_type, fields) + + except Exception as e: + self._logger.debug("Error registering artifact in Repository: {}".format(e)) + + finally: + if source == 'directory' and compresed: + remove(path) + + def indexation(self, destination, path, package_type, fields): + """ + Process for index packages + :param destination: index repository path + :param path: path of the package + :param package_type: package type (vnf, ns) + :param fields: dict with the required values + """ + data_ind = {'name': fields.get('name'), 'description': fields.get('description'), + 'vendor': fields.get('vendor'), 'path': fields.get('path')} + + final_path = join(destination, package_type, fields.get('id'), fields.get('version')) + if isdir(join(destination, package_type, fields.get('id'))): + if isdir(final_path): + self._logger.warning('{} {} already exists'.format(package_type.upper(), str(path))) + else: + mkdir(final_path) + copyfile(path, + final_path + '/' + fields.get('id') + "-" + fields.get('version') + '.tar.gz') + yaml.dump(fields, open(final_path + '/' + 'metadata.yaml', 'w'), + Dumper=ruamel.yaml.RoundTripDumper) + index = yaml.load(open(destination + '/index.yaml')) + + index['{}_packages'.format(package_type)][fields.get('id')][fields.get('version')] = data_ind + if versioning.parse(index['{}_packages'.format(package_type)][fields.get('id')][ + 'latest']) < versioning.parse(fields.get('version')): + index['{}_packages'.format(package_type)][fields.get('id')]['latest'] = fields.get( + 'version') + yaml.dump(index, open(destination + '/index.yaml', 'w'), Dumper=ruamel.yaml.RoundTripDumper) + self._logger.info('{} {} added in the repository'.format(package_type.upper(), str(path))) + else: + mkdir(destination + '/{}/'.format(package_type) + fields.get('id')) + mkdir(final_path) + copyfile(path, + final_path + '/' + fields.get('id') + "-" + fields.get('version') + '.tar.gz') + yaml.dump(fields, open(join(final_path, 'metadata.yaml'), 'w'), Dumper=ruamel.yaml.RoundTripDumper) + index = yaml.load(open(destination + '/index.yaml')) + + index['{}_packages'.format(package_type)][fields.get('id')] = {fields.get('version'): data_ind} + index['{}_packages'.format(package_type)][fields.get('id')]['latest'] = fields.get('version') + yaml.dump(index, open(join(destination, 'index.yaml'), 'w'), Dumper=ruamel.yaml.RoundTripDumper) + self._logger.info('{} {} added in the repository'.format(package_type.upper(), str(path))) + + def current_datatime(self): + """ + Datetime Generator + :return: Datetime as string with the following structure "2020-04-29T08:41:07.681653Z" + """ + return time.strftime('%Y-%m-%dT%H:%M:%S.%sZ') + + def init_directory(self, destination): + """ + Initialize the index directory. Creation of index.yaml, and the directories for vnf and ns + :param destination: + :return: + """ + if not isdir(destination): + mkdir(destination) + if not isfile(join(destination, 'index.yaml')): + mkdir(join(destination, 'vnf')) + mkdir(join(destination, 'ns')) + index_data = {'apiVersion': 'v1', 'generated': self.current_datatime(), 'vnf_packages': {}, + 'ns_packages': {}} + with open(join(destination, 'index.yaml'), 'w') as outfile: + yaml.dump(index_data, outfile, default_flow_style=False) diff --git a/osmclient/sol005/package.py b/osmclient/sol005/package.py index c383862..622fb86 100644 --- a/osmclient/sol005/package.py +++ b/osmclient/sol005/package.py @@ -24,17 +24,22 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound from osmclient.common import utils import json +import logging +import os.path class Package(object): def __init__(self, http=None, client=None): self._client = client self._http = http + self._logger = logging.getLogger('osmclient') def get_key_val_from_pkg(self, descriptor_file): + self._logger.debug("") return utils.get_key_val_from_pkg(descriptor_file) def _wait_for_package(self, pkg_type): + self._logger.debug("") if 'vnfd' in pkg_type['type']: get_method = self._client.vnfd.get elif 'nsd' in pkg_type['type']: @@ -44,6 +49,7 @@ class Package(object): # helper method to check if pkg exists def check_exists(func): + self._logger.debug("") try: func() except NotFound: @@ -58,6 +64,7 @@ class Package(object): """wait(block) for an upload to succeed. The filename passed is assumed to be a descriptor tarball. """ + self._logger.debug("") self._client.get_token() pkg_type = utils.get_key_val_from_pkg(filename) @@ -68,44 +75,49 @@ class Package(object): raise ClientException("package {} failed to upload" .format(filename)) - def upload(self, filename): - self._client.get_token() - pkg_type = utils.get_key_val_from_pkg(filename) - if pkg_type is None: - raise ClientException("Cannot determine package type") - if pkg_type['type'] == 'nsd': - endpoint = '/nsd/v1/ns_descriptors_content' + def upload(self, filename, skip_charm_build=False): + self._logger.debug("") + if os.path.isdir(filename): + filename = filename.rstrip('/') + filename = self._client.package_tool.build(filename, skip_validation=False, skip_charm_build=skip_charm_build) + self.upload(filename) else: - endpoint = '/vnfpkgm/v1/vnf_packages_content' - #endpoint = '/nsds' if pkg_type['type'] == 'nsd' else '/vnfds' - #print('Endpoint: {}'.format(endpoint)) - headers = self._client._headers - headers['Content-Type'] = 'application/gzip' - #headers['Content-Type'] = 'application/binary' - # Next three lines are to be removed in next version - #headers['Content-Filename'] = basename(filename) - #file_size = stat(filename).st_size - #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) - headers["Content-File-MD5"] = utils.md5(filename) - http_header = ['{}: {}'.format(key,val) - for (key,val) in list(headers.items())] - self._http.set_http_header(http_header) - http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) - #print('HTTP CODE: {}'.format(http_code)) - #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): + self._client.get_token() + pkg_type = utils.get_key_val_from_pkg(filename) + if pkg_type is None: + raise ClientException("Cannot determine package type") + if pkg_type['type'] == 'nsd': + endpoint = '/nsd/v1/ns_descriptors_content' + else: + endpoint = '/vnfpkgm/v1/vnf_packages_content' + #endpoint = '/nsds' if pkg_type['type'] == 'nsd' else '/vnfds' + #print('Endpoint: {}'.format(endpoint)) + headers = self._client._headers + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print('HTTP CODE: {}'.format(http_code)) + #print('RESP: {}'.format(resp)) + #if http_code in (200, 201, 202, 204): if resp: resp = json.loads(resp) if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( - resp)) + resp)) print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to upload package - {}".format(msg)) - + # else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to upload package - {}".format(msg)) diff --git a/osmclient/sol005/pdud.py b/osmclient/sol005/pdud.py index 22ca8bc..aa4bf69 100644 --- a/osmclient/sol005/pdud.py +++ b/osmclient/sol005/pdud.py @@ -22,6 +22,7 @@ from osmclient.common.exceptions import NotFound from osmclient.common.exceptions import ClientException from osmclient.common import utils import json +import logging class Pdu(object): @@ -29,6 +30,7 @@ class Pdu(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/pdu' self._apiVersion = '/v1' self._apiResource = '/pdu_descriptors' @@ -36,16 +38,18 @@ class Pdu(object): self._apiVersion, self._apiResource) def list(self, filter=None): + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for pdud in self.list(): @@ -58,16 +62,21 @@ class Pdu(object): raise NotFound("pdud {} not found".format(name)) def get_individual(self, name): + self._logger.debug("") pdud = self.get(name) # It is redundant, since the previous one already gets the whole pdudInfo # The only difference is that a different primitive is exercised - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, pdud['_id'])) + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, pdud['_id'])) + except NotFound: + raise NotFound("pdu '{}' not found".format(name)) #print(yaml.safe_dump(resp)) if resp: - return resp - raise NotFound("pdu {} not found".format(name)) + return json.loads(resp) + raise NotFound("pdu '{}' not found".format(name)) def delete(self, name, force=False): + self._logger.debug("") pdud = self.get(name) querystring = '' if force: @@ -81,15 +90,16 @@ class Pdu(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete pdu {} - {}".format(name, msg)) def create(self, pdu, update_endpoint=None): + self._logger.debug("") self._client.get_token() headers= self._client._headers headers['Content-Type'] = 'application/yaml' @@ -104,23 +114,24 @@ class Pdu(object): http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=pdu) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server: '.format( - resp)) - print(resp['id']) - else: - msg = "Error {}".format(http_code) - if resp: - try: - msg = "{} - {}".format(msg, json.loads(resp)) - except ValueError: - msg = "{} - {}".format(msg, resp) - raise ClientException("failed to create/update pdu - {}".format(msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server: {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "Error {}".format(http_code) + # if resp: + # try: + # msg = "{} - {}".format(msg, json.loads(resp)) + # except ValueError: + # msg = "{} - {}".format(msg, resp) + # raise ClientException("failed to create/update pdu - {}".format(msg)) def update(self, name, filename): + self._logger.debug("") pdud = self.get(name) endpoint = '{}/{}'.format(self._apiBase, pdud['_id']) self.create(filename=filename, update_endpoint=endpoint) diff --git a/osmclient/sol005/project.py b/osmclient/sol005/project.py index 19a27c3..ed781fa 100644 --- a/osmclient/sol005/project.py +++ b/osmclient/sol005/project.py @@ -23,12 +23,14 @@ from osmclient.common import utils from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json +import logging class Project(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/projects' @@ -38,34 +40,38 @@ class Project(object): def create(self, name, project): """Creates a new OSM project """ + self._logger.debug("") self._client.get_token() http_code, resp = self._http.post_cmd(endpoint=self._apiBase, - postfields_dict=project) + postfields_dict=project, + skip_query_admin=True) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to create project {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to create project {} - {}".format(name, msg)) def update(self, project, project_changes): """Updates an OSM project identified by name """ + self._logger.debug("") self._client.get_token() proj = self.get(project) http_code, resp = self._http.patch_cmd(endpoint='{}/{}'.format(self._apiBase, proj['_id']), - postfields_dict=project_changes) + postfields_dict=project_changes, + skip_query_admin=True) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) if http_code in (200, 201, 202): @@ -77,25 +83,27 @@ class Project(object): print(resp['id']) elif http_code == 204: print("Updated") - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update project {} - {}".format(project, msg)) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update project {} - {}".format(project, msg)) def delete(self, name, force=False): """Deletes an OSM project identified by name """ + self._logger.debug("") self._client.get_token() project = self.get(name) querystring = '' if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, - project['_id'], querystring)) + project['_id'], querystring), + skip_query_admin=True) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) if http_code == 202: @@ -105,30 +113,33 @@ class Project(object): elif resp and 'result' in resp: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete project {} - {}".format(name, msg)) def list(self, filter=None): """Returns the list of OSM projects """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string), + skip_query_admin=True) #print('RESP: {}'.format(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns a specific OSM project based on name or id """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for proj in self.list(): @@ -140,4 +151,3 @@ class Project(object): return proj raise NotFound("Project {} not found".format(name)) - diff --git a/osmclient/sol005/repo.py b/osmclient/sol005/repo.py index a8d8f17..cc82402 100644 --- a/osmclient/sol005/repo.py +++ b/osmclient/sol005/repo.py @@ -37,21 +37,21 @@ class Repo(object): postfields_dict=repo) #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to add repo {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to add repo {} - {}".format(name, msg)) def update(self, name, repo): self._client.get_token() @@ -60,16 +60,16 @@ class Repo(object): postfields_dict=repo) # print 'HTTP CODE: {}'.format(http_code) # print 'RESP: {}'.format(resp) - if http_code in (200, 201, 202, 204): - pass - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update repo {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + # pass + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update repo {} - {}".format(name, msg)) def get_id(self, name): """Returns a repo id from a repo name @@ -97,12 +97,12 @@ class Repo(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete repo {} - {}".format(name, msg)) def list(self, filter=None): @@ -112,9 +112,9 @@ class Repo(object): filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): @@ -124,10 +124,13 @@ class Repo(object): repo_id = name if not utils.validate_uuid4(name): repo_id = self.get_id(name) - resp = self._http.get_cmd('{}/{}'.format(self._apiBase,repo_id)) - if not resp or '_id' not in resp: - raise ClientException('failed to get repo info: '.format(resp)) - else: + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase,repo_id)) + if resp: + resp = json.loads(resp) + if not resp or '_id' not in resp: + raise ClientException('failed to get repo info: {}'.format(resp)) return resp - raise NotFound("Repo {} not found".format(name)) + except NotFound: + raise NotFound("Repo {} not found".format(name)) diff --git a/osmclient/sol005/role.py b/osmclient/sol005/role.py index 01f3bc3..2544a7b 100644 --- a/osmclient/sol005/role.py +++ b/osmclient/sol005/role.py @@ -25,12 +25,14 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json import yaml +import logging class Role(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/roles' @@ -46,6 +48,7 @@ class Role(object): :raises ClientException: when receives an unexpected from the server. :raises ClientException: when fails creating a role. """ + self._logger.debug("") self._client.get_token() role = {"name": name} @@ -62,24 +65,25 @@ class Role(object): role["permissions"] = role_permissions http_code, resp = self._http.post_cmd(endpoint=self._apiBase, - postfields_dict=role) + postfields_dict=role, + skip_query_admin=True) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('Unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("Failed to create role {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('Unexpected response from server - {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("Failed to create role {} - {}".format(name, msg)) def update(self, name, new_name, permissions, add=None, remove=None): """ @@ -95,6 +99,7 @@ class Role(object): :raises ClientException: when receives an unexpected response from the server. :raises ClientException: when fails updating a role. """ + self._logger.debug("") self._client.get_token() if new_name is None and permissions is None and add is None and remove is None: raise ClientException('At least one option should be provided') @@ -144,7 +149,8 @@ class Role(object): del new_role_obj["permissions"] http_code, resp = self._http.patch_cmd(endpoint='{}/{}'.format(self._apiBase, role_obj['_id']), - postfields_dict=new_role_obj) + postfields_dict=new_role_obj, + skip_query_admin=True) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) if http_code in (200, 201, 202): @@ -152,18 +158,18 @@ class Role(object): resp = json.loads(resp) if not resp or 'id' not in resp: raise ClientException('Unexpected response from server - {}'.format( - resp)) + resp)) print(resp['id']) elif http_code == 204: print("Updated") - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("Failed to update role {} - {}".format(name, msg)) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("Failed to update role {} - {}".format(name, msg)) def delete(self, name, force=False): """ @@ -173,13 +179,15 @@ class Role(object): :param force: :raises ClientException: when fails to delete a role. """ + self._logger.debug("") self._client.get_token() role = self.get(name) querystring = '' if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, - role['_id'], querystring)) + role['_id'], querystring), + skip_query_admin=True) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) if http_code == 202: @@ -189,12 +197,12 @@ class Role(object): elif resp and 'result' in resp: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("Failed to delete role {} - {}".format(name, msg)) def list(self, filter=None): @@ -204,14 +212,15 @@ class Role(object): :param filter: :returns: """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase, filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase, filter_string),skip_query_admin=True) # print('RESP: {}'.format(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): @@ -222,6 +231,7 @@ class Role(object): :raises NotFound: when the role is not found. :returns: the specified role. """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for role in self.list(): @@ -232,3 +242,4 @@ class Role(object): if name == role['name']: return role raise NotFound("Role {} not found".format(name)) + diff --git a/osmclient/sol005/sdncontroller.py b/osmclient/sol005/sdncontroller.py index 4f3ced4..5b85ef4 100644 --- a/osmclient/sol005/sdncontroller.py +++ b/osmclient/sol005/sdncontroller.py @@ -23,12 +23,15 @@ from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json +import logging +import yaml class SdnController(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/sdns' @@ -36,21 +39,26 @@ class SdnController(object): self._apiVersion, self._apiResource) # SDNC '--wait' option - def _wait(self, id, deleteFlag=False): + def _wait(self, id, wait_time, deleteFlag=False): + self._logger.debug("") self._client.get_token() # Endpoint to get operation status apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/sdns') # Wait for status for SDN instance creation/update/deletion + if isinstance(wait_time, bool): + wait_time = WaitForStatus.TIMEOUT_SDNC_OPERATION WaitForStatus.wait_for_status( 'SDNC', str(id), - WaitForStatus.TIMEOUT_SDNC_OPERATION, + wait_time, apiUrlStatus, self._http.get2_cmd, deleteFlag=deleteFlag) def _get_id_for_wait(self, name): - # Returns id of name, or the id itself if given as argument + """Returns id of name, or the id itself if given as argument + """ + self._logger.debug("") for sdnc in self.list(): if name == sdnc['_id']: return sdnc['_id'] @@ -60,57 +68,64 @@ class SdnController(object): return '' def create(self, name, sdn_controller, wait=False): + self._logger.debug("") + if 'config' in sdn_controller and isinstance(sdn_controller["config"], str): + sdn_controller["config"] = yaml.safe_load(sdn_controller["config"]) self._client.get_token() http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=sdn_controller) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - if wait: - # Wait for status for SDNC instance creation - self._wait(resp.get('id')) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to create SDN controller {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + if wait: + # Wait for status for SDNC instance creation + self._wait(resp.get('id'), wait) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to create SDN controller {} - {}".format(name, msg)) def update(self, name, sdn_controller, wait=False): + self._logger.debug("") + if 'config' in sdn_controller and isinstance(sdn_controller["config"], str): + sdn_controller["config"] = yaml.safe_load(sdn_controller["config"]) self._client.get_token() sdnc = self.get(name) sdnc_id_for_wait = self._get_id_for_wait(name) http_code, resp = self._http.patch_cmd(endpoint='{}/{}'.format(self._apiBase,sdnc['_id']), - postfields_dict=sdn_controller) + postfields_dict=sdn_controller) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if wait: - # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. - # Use the previously obtained id instead. - wait_id = sdnc_id_for_wait - # Wait for status for VI instance update - self._wait(wait_id) - else: - pass - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update SDN controller {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if wait: + # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. + # Use the previously obtained id instead. + wait_id = sdnc_id_for_wait + # Wait for status for VI instance update + self._wait(wait_id, wait) + # else: + # pass + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update SDN controller {} - {}".format(name, msg)) def delete(self, name, force=False, wait=False): + self._logger.debug("") self._client.get_token() sdn_controller = self.get(name) sdnc_id_for_wait = self._get_id_for_wait(name) @@ -118,13 +133,13 @@ class SdnController(object): if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, - sdn_controller['_id'], querystring)) - #print('HTTP CODE: {}'.format(http_code)) - #print('RESP: {}'.format(resp)) + sdn_controller['_id'], querystring)) + # print('HTTP CODE: {}'.format(http_code)) + # print('RESP: {}'.format(resp)) if http_code == 202: if wait: # Wait for status for SDNC instance deletion - self._wait(sdnc_id_for_wait, deleteFlag=True) + self._wait(sdnc_id_for_wait, wait, deleteFlag=True) else: print('Deletion in progress') elif http_code == 204: @@ -132,30 +147,32 @@ class SdnController(object): elif resp and 'result' in resp: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete SDN controller {} - {}".format(name, msg)) def list(self, filter=None): """Returns a list of SDN controllers """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) #print('RESP: {}'.format(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns an SDN controller based on name or id """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for sdnc in self.list(): @@ -167,4 +184,3 @@ class SdnController(object): return sdnc raise NotFound("SDN controller {} not found".format(name)) - diff --git a/osmclient/sol005/user.py b/osmclient/sol005/user.py index 64d5c69..d28514e 100644 --- a/osmclient/sol005/user.py +++ b/osmclient/sol005/user.py @@ -19,16 +19,17 @@ OSM user mgmt API """ -from osmclient.common import utils from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json +import logging class User(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/users' @@ -38,6 +39,7 @@ class User(object): def create(self, name, user): """Creates a new OSM user """ + self._logger.debug("") self._client.get_token() if not user["projects"] or (len(user["projects"]) == 1 and not user["projects"][0]): del user["projects"] @@ -54,36 +56,37 @@ class User(object): for role in roles: mapping = {"project": project, "role": role} - if mapping not in project_role_mappings: + if mapping not in project_role_mappings: project_role_mappings.append(mapping) - user["project_role_mappings"] = project_role_mappings else: del user["project_role_mappings"] http_code, resp = self._http.post_cmd(endpoint=self._apiBase, - postfields_dict=user) + postfields_dict=user, + skip_query_admin=True) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to create user {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to create user {} - {}".format(name, msg)) def update(self, name, user): """Updates an existing OSM user identified by name """ + self._logger.debug("") self._client.get_token() # print(user) myuser = self.get(name) @@ -141,7 +144,7 @@ class User(object): raise ClientException("At least something should be changed.") http_code, resp = self._http.patch_cmd(endpoint='{}/{}'.format(self._apiBase, myuser['_id']), - postfields_dict=update_user) + postfields_dict=update_user, skip_query_admin=True) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) if http_code in (200, 201, 202): @@ -153,25 +156,26 @@ class User(object): print(resp['id']) elif http_code == 204: print('Updated') - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update user {} - {}".format(name, msg)) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update user {} - {}".format(name, msg)) def delete(self, name, force=False): """Deletes an existing OSM user identified by name """ + self._logger.debug("") self._client.get_token() user = self.get(name) querystring = '' if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, - user['_id'], querystring)) + user['_id'], querystring), skip_query_admin=True) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) if http_code == 202: @@ -181,39 +185,41 @@ class User(object): elif resp and 'result' in resp: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete user {} - {}".format(name, msg)) def list(self, filter=None): """Returns the list of OSM users """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string), skip_query_admin=True) #print('RESP: {}'.format(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns an OSM user based on name or id """ + self._logger.debug("") self._client.get_token() - if utils.validate_uuid4(name): - for user in self.list(): - if name == user['_id']: - return user - else: - for user in self.list(): - if name == user['username']: - return user + # keystone with external LDAP contains large ids, not uuid format + # utils.validate_uuid4(name) cannot be used + user_list = self.list() + for user in user_list: + if name == user['_id']: + return user + for user in user_list: + if name == user['username']: + return user raise NotFound("User {} not found".format(name)) - diff --git a/osmclient/sol005/vim.py b/osmclient/sol005/vim.py index 1af7cc7..fa17dfc 100644 --- a/osmclient/sol005/vim.py +++ b/osmclient/sol005/vim.py @@ -24,12 +24,14 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml import json +import logging class Vim(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/vim_accounts' @@ -37,21 +39,27 @@ class Vim(object): self._apiVersion, self._apiResource) # VIM '--wait' option - def _wait(self, id, deleteFlag=False): + def _wait(self, id, wait_time, deleteFlag=False): + self._logger.debug("") self._client.get_token() # Endpoint to get operation status apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/vim_accounts') # Wait for status for VIM instance creation/deletion + if isinstance(wait_time, bool): + wait_time = WaitForStatus.TIMEOUT_VIM_OPERATION WaitForStatus.wait_for_status( 'VIM', str(id), - WaitForStatus.TIMEOUT_VIM_OPERATION, + wait_time, apiUrlStatus, self._http.get2_cmd, deleteFlag=deleteFlag) def _get_id_for_wait(self, name): - # Returns id of name, or the id itself if given as argument + """ Returns id of name, or the id itself if given as argument + """ + self._logger.debug("") + self._client.get_token() for vim in self.list(): if name == vim['uuid']: return vim['uuid'] @@ -61,6 +69,7 @@ class Vim(object): return '' def create(self, name, vim_access, sdn_controller=None, sdn_port_mapping=None, wait=False): + self._logger.debug("") self._client.get_token() if 'vim-type' not in vim_access: #'openstack' not in vim_access['vim-type']): @@ -87,26 +96,27 @@ class Vim(object): postfields_dict=vim_account) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - if wait: - # Wait for status for VIM instance creation - self._wait(resp.get('id')) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to create vim {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + if wait: + # Wait for status for VIM instance creation + self._wait(resp.get('id'), wait) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to create vim {} - {}".format(name, msg)) def update(self, vim_name, vim_account, sdn_controller, sdn_port_mapping, wait=False): + self._logger.debug("") self._client.get_token() vim = self.get(vim_name) vim_id_for_wait = self._get_id_for_wait(vim_name) @@ -118,37 +128,42 @@ class Vim(object): vim_config = None else: vim_config = yaml.safe_load(vim_account['config']) - if sdn_controller: - sdnc = self._client.sdnc.get(sdn_controller) - vim_config['sdn-controller'] = sdnc['_id'] - if sdn_port_mapping: - with open(sdn_port_mapping, 'r') as f: - vim_config['sdn-port-mapping'] = yaml.safe_load(f.read()) + if sdn_controller == "": + vim_config['sdn-controller'] = None + vim_config['sdn-port-mapping'] = None + else: + if sdn_controller: + sdnc = self._client.sdnc.get(sdn_controller) + vim_config['sdn-controller'] = sdnc['_id'] + if sdn_port_mapping: + with open(sdn_port_mapping, 'r') as f: + vim_config['sdn-port-mapping'] = yaml.safe_load(f.read()) vim_account['config'] = vim_config #vim_account['config'] = json.dumps(vim_config) http_code, resp = self._http.patch_cmd(endpoint='{}/{}'.format(self._apiBase,vim['_id']), postfields_dict=vim_account) # print('HTTP CODE: {}'.format(http_code)) # print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if wait: - # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. - # Use the previously obtained id instead. - wait_id = vim_id_for_wait - # Wait for status for VI instance update - self._wait(wait_id) - else: - pass - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update vim {} - {}".format(vim_name, msg)) + #if http_code in (200, 201, 202, 204): + if wait: + # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. + # Use the previously obtained id instead. + wait_id = vim_id_for_wait + # Wait for status for VI instance update + self._wait(wait_id, wait) + # else: + # pass + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update vim {} - {}".format(vim_name, msg)) def update_vim_account_dict(self, vim_account, vim_access): + self._logger.debug("") vim_account['vim_type'] = vim_access['vim-type'] vim_account['description'] = vim_access['description'] vim_account['vim_url'] = vim_access['vim-url'] @@ -160,12 +175,14 @@ class Vim(object): def get_id(self, name): """Returns a VIM id from a VIM name """ + self._logger.debug("") for vim in self.list(): if name == vim['name']: return vim['uuid'] raise NotFound("vim {} not found".format(name)) def delete(self, vim_name, force=False, wait=False): + self._logger.debug("") self._client.get_token() vim_id = vim_name if not utils.validate_uuid4(vim_name): @@ -186,48 +203,51 @@ class Vim(object): resp = json.loads(resp) wait_id = resp.get('id') # Wait for status for VIM account deletion - self._wait(wait_id, deleteFlag=True) + self._wait(wait_id, wait, deleteFlag=True) else: print('Deletion in progress') elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete vim {} - {}".format(vim_name, msg)) def list(self, filter=None): """Returns a list of VIM accounts """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if not resp: return list() - vim_accounts = [] - for datacenter in resp: - vim_accounts.append({"name": datacenter['name'], "uuid": datacenter['_id'] - if '_id' in datacenter else None}) + vim_accounts = json.loads(resp) + for datacenter in vim_accounts: + datacenter["uuid"] = datacenter.get('_id') # backward compatibility? return vim_accounts def get(self, name): """Returns a VIM account based on name or id """ + self._logger.debug("") self._client.get_token() vim_id = name if not utils.validate_uuid4(name): vim_id = self.get_id(name) - resp = self._http.get_cmd('{}/{}'.format(self._apiBase,vim_id)) - if not resp or '_id' not in resp: - raise ClientException('failed to get vim info: '.format( - resp)) - else: + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase,vim_id)) + if resp: + resp = json.loads(resp) + if not resp or '_id' not in resp: + raise ClientException('failed to get vim info: {}'.format(resp)) return resp - raise NotFound("vim {} not found".format(name)) + except NotFound: + raise NotFound("vim '{}' not found".format(name)) diff --git a/osmclient/sol005/vnf.py b/osmclient/sol005/vnf.py index b7ac856..edae358 100644 --- a/osmclient/sol005/vnf.py +++ b/osmclient/sol005/vnf.py @@ -20,13 +20,15 @@ OSM vnf API handling from osmclient.common import utils from osmclient.common.exceptions import NotFound - +import logging +import json class Vnf(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/nslcm' self._apiVersion = '/v1' self._apiResource = '/vnfrs' @@ -36,6 +38,7 @@ class Vnf(object): def list(self, ns=None, filter=None): """Returns a list of VNF instances """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: @@ -46,15 +49,16 @@ class Vnf(object): filter_string += ',nsr-id-ref={}'.format(ns_instance['_id']) else: filter_string = '?nsr-id-ref={}'.format(ns_instance['_id']) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) #print('RESP: {}'.format(resp)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): """Returns a VNF instance based on name or id """ + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for vnf in self.list(): @@ -62,11 +66,12 @@ class Vnf(object): return vnf else: for vnf in self.list(): - if name == vnf['name']: + if name == vnf.get('name'): return vnf raise NotFound("vnf {} not found".format(name)) def get_individual(self, name): + self._logger.debug("") self._client.get_token() vnf_id = name if not utils.validate_uuid4(name): @@ -74,9 +79,11 @@ class Vnf(object): if name == vnf['name']: vnf_id = vnf['_id'] break - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, vnf_id)) - #print('RESP: {}'.format(resp)) - if resp: - return resp - raise NotFound("vnf {} not found".format(name)) - + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, vnf_id)) + #print('RESP: {}'.format(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("vnf '{}' not found".format(name)) + raise NotFound("vnf '{}' not found".format(name)) diff --git a/osmclient/sol005/vnfd.py b/osmclient/sol005/vnfd.py index ffa0182..ac44495 100644 --- a/osmclient/sol005/vnfd.py +++ b/osmclient/sol005/vnfd.py @@ -22,9 +22,14 @@ from osmclient.common.exceptions import NotFound from osmclient.common.exceptions import ClientException from osmclient.common import utils import json +import yaml import magic from os.path import basename -#from os import stat +import logging +import os.path +from urllib.parse import quote +import tarfile +from osm_im.validation import Validation as validation_im class Vnfd(object): @@ -32,6 +37,7 @@ class Vnfd(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/vnfpkgm' self._apiVersion = '/v1' self._apiResource = '/vnf_packages' @@ -40,16 +46,18 @@ class Vnfd(object): #self._apiBase='/vnfds' def list(self, filter=None): + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if resp: - return resp + return json.loads(resp) return list() def get(self, name): + self._logger.debug("") self._client.get_token() if utils.validate_uuid4(name): for vnfd in self.list(): @@ -62,45 +70,54 @@ class Vnfd(object): raise NotFound("vnfd {} not found".format(name)) def get_individual(self, name): + self._logger.debug("") vnfd = self.get(name) # It is redundant, since the previous one already gets the whole vnfpkginfo # The only difference is that a different primitive is exercised - resp = self._http.get_cmd('{}/{}'.format(self._apiBase, vnfd['_id'])) - #print(yaml.safe_dump(resp)) - if resp: - return resp - raise NotFound("vnfd {} not found".format(name)) + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, vnfd['_id'])) + #print(yaml.safe_dump(resp)) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound("vnfd '{}' not found".format(name)) + raise NotFound("vnfd '{}' not found".format(name)) def get_thing(self, name, thing, filename): + self._logger.debug("") vnfd = self.get(name) headers = self._client._headers headers['Accept'] = 'application/binary' http_code, resp = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, vnfd['_id'], thing)) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - #store in a file - return resp - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + #store in a file + return json.loads(resp) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) def get_descriptor(self, name, filename): + self._logger.debug("") self.get_thing(name, 'vnfd', filename) def get_package(self, name, filename): + self._logger.debug("") self.get_thing(name, 'package_content', filename) def get_artifact(self, name, artifact, filename): + self._logger.debug("") self.get_thing(name, 'artifacts/{}'.format(artifact), filename) def delete(self, name, force=False): + self._logger.debug("") self._client.get_token() vnfd = self.get(name) querystring = '' @@ -115,72 +132,143 @@ class Vnfd(object): elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete vnfd {} - {}".format(name, msg)) - def create(self, filename, overwrite=None, update_endpoint=None): - self._client.get_token() - mime_type = magic.from_file(filename, mime=True) - if mime_type is None: - raise ClientException( - "failed to guess MIME type for file '{}'".format(filename)) - headers= self._client._headers - headers['Content-Filename'] = basename(filename) - if mime_type in ['application/yaml', 'text/plain', 'application/json']: - headers['Content-Type'] = 'text/plain' - elif mime_type in ['application/gzip', 'application/x-gzip']: - headers['Content-Type'] = 'application/gzip' - #headers['Content-Type'] = 'application/binary' - # Next three lines are to be removed in next version - #headers['Content-Filename'] = basename(filename) - #file_size = stat(filename).st_size - #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + def create(self, filename, overwrite=None, update_endpoint=None, skip_charm_build=False, + override_epa=False, override_nonepa=False, override_paravirt=False): + self._logger.debug("") + if os.path.isdir(filename): + filename = filename.rstrip('/') + filename = self._client.package_tool.build(filename, skip_validation=False, skip_charm_build=skip_charm_build) + print('Uploading package {}'.format(filename)) + self.create(filename, overwrite=overwrite, update_endpoint=update_endpoint, + override_epa=override_epa, override_nonepa=override_nonepa, + override_paravirt=override_paravirt) else: - raise ClientException( - "Unexpected MIME type for file {}: MIME type {}".format( - filename, mime_type) - ) - headers["Content-File-MD5"] = utils.md5(filename) - http_header = ['{}: {}'.format(key,val) - for (key,val) in list(headers.items())] - self._http.set_http_header(http_header) - if update_endpoint: - http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) - else: - ow_string = '' - if overwrite: - ow_string = '?{}'.format(overwrite) - self._apiResource = '/vnf_packages_content' - self._apiBase = '{}{}{}'.format(self._apiName, - self._apiVersion, self._apiResource) - endpoint = '{}{}'.format(self._apiBase,ow_string) - http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) - #print('HTTP CODE: {}'.format(http_code)) - #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server: '.format( - resp)) - print(resp['id']) - elif http_code == 204: - print('Updated') - else: - msg = "Error {}".format(http_code) - if resp: - try: - msg = "{} - {}".format(msg, json.loads(resp)) - except ValueError: - msg = "{} - {}".format(msg, resp) - raise ClientException("failed to create/update vnfd - {}".format(msg)) + self._client.get_token() + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers = self._client._headers + headers['Content-Filename'] = basename(filename) + if mime_type in ['application/yaml', 'text/plain', 'application/json']: + headers['Content-Type'] = 'text/plain' + elif mime_type in ['application/gzip', 'application/x-gzip']: + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + else: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + special_ow_string = '' + if override_epa or override_nonepa or override_paravirt: + # If override for EPA, non-EPA or paravirt is required, get the descriptor data + descriptor_data = None + if mime_type in ['application/yaml', 'text/plain', 'application/json']: + with open(filename) as df: + descriptor_data = df.read() + elif mime_type in ['application/gzip', 'application/x-gzip']: + tar_object = tarfile.open(filename, "r:gz") + descriptor_list = [] + for member in tar_object: + if member.isreg(): + if '/' not in os.path.dirname(member.name) and member.name.endswith('.yaml'): + descriptor_list.append(member.name) + if len(descriptor_list) > 1: + raise ClientException('Found more than one potential descriptor in the tar.gz file') + elif len(descriptor_list) == 0: + raise ClientException('No descriptor was found in the tar.gz file') + with tar_object.extractfile(descriptor_list[0]) as df: + descriptor_data = df.read() + tar_object.close() + if not descriptor_data: + raise ClientException('Descriptor could not be read') + desc_type, vnfd = validation_im.yaml_validation(self, descriptor_data) + validation_im.pyangbind_validation(self, desc_type, vnfd) + vnfd = yaml.safe_load(descriptor_data) + vdu_list = [] + for k in vnfd: + # Get only the first descriptor in case there are many in the yaml file + # k can be vnfd:vnfd-catalog or vnfd-catalog. This check is skipped + first_vnfd = vnfd[k]['vnfd'][0] + vdu_list = first_vnfd.get('vdu',[]) + break; + for vdu_number, vdu in enumerate(vdu_list): + if override_epa: + guest_epa = {} + guest_epa["mempage-size"] = "LARGE" + guest_epa["cpu-pinning-policy"] = "DEDICATED" + guest_epa["cpu-thread-pinning-policy"] = "PREFER" + guest_epa["numa-node-policy"] = {} + guest_epa["numa-node-policy"]["node-cnt"] = 1 + guest_epa["numa-node-policy"]["mem-policy"] = "STRICT" + #guest_epa["numa-node-policy"]["node"] = [] + #guest_epa["numa-node-policy"]["node"].append({"id": "0", "paired-threads": {"num-paired-threads": 1} }) + special_ow_string = "{}vdu.{}.guest-epa={};".format(special_ow_string,vdu_number,quote(yaml.safe_dump(guest_epa))) + headers['Query-String-Format'] = 'yaml' + if override_nonepa: + special_ow_string = "{}vdu.{}.guest-epa=;".format(special_ow_string,vdu_number) + if override_paravirt: + for iface_number in range(len(vdu['interface'])): + special_ow_string = "{}vdu.{}.interface.{}.virtual-interface.type=PARAVIRT;".format( + special_ow_string,vdu_number,iface_number) + special_ow_string = special_ow_string.rstrip(";") + + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + if update_endpoint: + http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if special_ow_string: + if overwrite: + overwrite = "{};{}".format(overwrite,special_ow_string) + else: + overwrite = special_ow_string + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/vnf_packages_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print('HTTP CODE: {}'.format(http_code)) + #print('RESP: {}'.format(resp)) + if http_code in (200, 201, 202): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server: {}'.format(resp)) + print(resp['id']) + elif http_code == 204: + print('Updated') + # else: + # msg = "Error {}".format(http_code) + # if resp: + # try: + # msg = "{} - {}".format(msg, json.loads(resp)) + # except ValueError: + # msg = "{} - {}".format(msg, resp) + # raise ClientException("failed to create/update vnfd - {}".format(msg)) def update(self, name, filename): + self._logger.debug("") self._client.get_token() vnfd = self.get(name) endpoint = '{}/{}/package_content'.format(self._apiBase, vnfd['_id']) diff --git a/osmclient/sol005/wim.py b/osmclient/sol005/wim.py index 943ceb4..5da3941 100644 --- a/osmclient/sol005/wim.py +++ b/osmclient/sol005/wim.py @@ -24,12 +24,14 @@ from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml import json +import logging class Wim(object): def __init__(self, http=None, client=None): self._http = http self._client = client + self._logger = logging.getLogger('osmclient') self._apiName = '/admin' self._apiVersion = '/v1' self._apiResource = '/wim_accounts' @@ -37,21 +39,26 @@ class Wim(object): self._apiVersion, self._apiResource) # WIM '--wait' option - def _wait(self, id, deleteFlag=False): + def _wait(self, id, wait_time, deleteFlag=False): + self._logger.debug("") self._client.get_token() # Endpoint to get operation status apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/wim_accounts') # Wait for status for WIM instance creation/deletion + if isinstance(wait_time, bool): + wait_time = WaitForStatus.TIMEOUT_WIM_OPERATION WaitForStatus.wait_for_status( 'WIM', str(id), - WaitForStatus.TIMEOUT_WIM_OPERATION, + wait_time, apiUrlStatus, self._http.get2_cmd, deleteFlag=deleteFlag) def _get_id_for_wait(self, name): - # Returns id of name, or the id itself if given as argument + """Returns id of name, or the id itself if given as argument + """ + self._logger.debug("") for wim in self.list(): if name == wim['uuid']: return wim['uuid'] @@ -61,6 +68,7 @@ class Wim(object): return '' def create(self, name, wim_input, wim_port_mapping=None, wait=False): + self._logger.debug("") self._client.get_token() if 'wim_type' not in wim_input: raise Exception("wim type not provided") @@ -82,26 +90,27 @@ class Wim(object): postfields_dict=wim_account) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if resp: - resp = json.loads(resp) - if not resp or 'id' not in resp: - raise ClientException('unexpected response from server - {}'.format( - resp)) - if wait: - # Wait for status for WIM instance creation - self._wait(resp.get('id')) - print(resp['id']) - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to create wim {} - {}".format(name, msg)) + #if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + if wait: + # Wait for status for WIM instance creation + self._wait(resp.get('id'), wait) + print(resp['id']) + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to create wim {} - {}".format(name, msg)) def update(self, wim_name, wim_account, wim_port_mapping=None, wait=False): + self._logger.debug("") self._client.get_token() wim = self.get(wim_name) wim_id_for_wait = self._get_id_for_wait(wim_name) @@ -122,26 +131,27 @@ class Wim(object): postfields_dict=wim_account) #print('HTTP CODE: {}'.format(http_code)) #print('RESP: {}'.format(resp)) - if http_code in (200, 201, 202, 204): - if wait: - # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. - # Use the previously obtained id instead. - wait_id = wim_id_for_wait - # Wait for status for WIM instance update - self._wait(wait_id) - else: - pass - else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp - raise ClientException("failed to update wim {} - {}".format(wim_name, msg)) + #if http_code in (200, 201, 202, 204): + if wait: + # In this case, 'resp' always returns None, so 'resp['id']' cannot be used. + # Use the previously obtained id instead. + wait_id = wim_id_for_wait + # Wait for status for WIM instance update + self._wait(wait_id, wait) + # else: + # pass + #else: + # msg = "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp + # raise ClientException("failed to update wim {} - {}".format(wim_name, msg)) def update_wim_account_dict(self, wim_account, wim_input): - print(wim_input) + self._logger.debug("") + self._logger.debug(str(wim_input)) wim_account['wim_type'] = wim_input['wim_type'] wim_account['description'] = wim_input['description'] wim_account['wim_url'] = wim_input['url'] @@ -152,12 +162,14 @@ class Wim(object): def get_id(self, name): """Returns a WIM id from a WIM name """ + self._logger.debug("") for wim in self.list(): if name == wim['name']: return wim['uuid'] raise NotFound("wim {} not found".format(name)) def delete(self, wim_name, force=False, wait=False): + self._logger.debug("") self._client.get_token() wim_id = wim_name wim_id_for_wait = self._get_id_for_wait(wim_name) @@ -180,32 +192,33 @@ class Wim(object): resp = json.loads(resp) wait_id = resp.get('id') # Wait for status for WIM account deletion - self._wait(wait_id, deleteFlag=True) + self._wait(wait_id, wait, deleteFlag=True) else: print('Deletion in progress') elif http_code == 204: print('Deleted') else: - msg = "" - if resp: - try: - msg = json.loads(resp) - except ValueError: - msg = resp + msg = resp or "" + # if resp: + # try: + # msg = json.loads(resp) + # except ValueError: + # msg = resp raise ClientException("failed to delete wim {} - {}".format(wim_name, msg)) def list(self, filter=None): """Returns a list of VIM accounts """ + self._logger.debug("") self._client.get_token() filter_string = '' if filter: filter_string = '?{}'.format(filter) - resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + _, resp = self._http.get2_cmd('{}{}'.format(self._apiBase,filter_string)) if not resp: return list() wim_accounts = [] - for datacenter in resp: + for datacenter in json.loads(resp): wim_accounts.append({"name": datacenter['name'], "uuid": datacenter['_id'] if '_id' in datacenter else None}) return wim_accounts @@ -213,14 +226,18 @@ class Wim(object): def get(self, name): """Returns a VIM account based on name or id """ + self._logger.debug("") self._client.get_token() wim_id = name if not utils.validate_uuid4(name): wim_id = self.get_id(name) - resp = self._http.get_cmd('{}/{}'.format(self._apiBase,wim_id)) - if not resp or '_id' not in resp: - raise ClientException('failed to get wim info: '.format( - resp)) - else: + try: + _, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase,wim_id)) + if resp: + resp = json.loads(resp) + if not resp or '_id' not in resp: + raise ClientException('failed to get wim info: {}'.format(resp)) return resp - raise NotFound("wim {} not found".format(name)) + except NotFound: + raise NotFound("wim '{}' not found".format(name)) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a2a4e5c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +# Copyright 2019 ETSI OSM +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +Click +prettytable +PyYAML +pycurl +python-magic +jinja2 +verboselogs +packaging +ruamel.yaml +requests +git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im diff --git a/setup.py b/setup.py index 689e926..f5b9e15 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,8 @@ setup( license='Apache 2', install_requires=[ 'Click', 'prettytable', 'pyyaml', 'pycurl', 'python-magic', - 'jinja2', 'osm-im' + 'jinja2', 'osm-im', 'verboselogs', 'packaging', 'ruamel.yaml', + 'requests' ], dependency_links=[ 'git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im', diff --git a/snap/local/README.md b/snap/local/README.md new file mode 100644 index 0000000..756034b --- /dev/null +++ b/snap/local/README.md @@ -0,0 +1,47 @@ +## +# Copyright 2020 ETSI +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +# Snap for OSM client + +The snapcraft.yaml located in this folder, allows to build a snap of the OSM client + + +# Build the Snap + +```bash +docker run -v ${PWD}:/build -w /build snapcore/snapcraft:stable /bin/bash -c "apt update && snapcraft" +``` + + +## Working on build steps + +As the build can take upwards of 4 minutes, it might be easier to enter the docker +container and perform iterative builds there. + +```bash +docker run -v ${PWD}:/build -w /build snapcore/snapcraft:stable /bin/bash -c /bin/bash +apt update +snapcraft +``` + + +## Install + +```bash +$ sudo snap install --devmode osmclient_v7.1.0+git4.a4af86f-dirty_amd64.snap +osmclient v7.1.0+git4.a4af86f-dirty installed +$ sudo snap alias osmclient.osm osm +``` diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index b7d0f05..805f1b6 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,20 +1,73 @@ -name: osmclient # you probably want to 'snapcraft register ' -version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' -summary: A python client for osm orchestration +--- +## +# Copyright 2020 ETSI +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +name: osmclient +base: core18 +version: git +summary: A python client for orchestrating OSM description: | - A python client for osm orchestration + A python client for orchestrating Open Source Mano (OSM). -grade: stable # must be 'stable' to release into candidate/stable channels -confinement: strict # use 'strict' once you have the right plugs and slots +grade: stable + +confinement: strict + +environment: + LC_ALL: C.UTF-8 + LANG: C.UTF-8 apps: - osmclient: - command: bin/osm + osm: + command: usr/bin/python3 $SNAP/usr/local/bin/osm + plugs: + - network + - home + - ssh-public-keys + environment: + PATH: $SNAP/usr/bin:$SNAP/bin/:$PATH + PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$PYTHONPATH + MAGIC: $SNAP/usr/share/file/magic.mgc parts: - osmclient: + client: + plugin: dump source: . - plugin: python - python-version: python2 - stage: - - -README.md + build-environment: + - PATH: "$PATH:$SNAPCRAFT_PART_INSTALL/usr/local/bin" + build-packages: + - gcc + - git + stage-packages: + - libmagic1 + - python3 + - python3-dev + - python3-pip + - python3-pycurl + - python3-setuptools + override-build: | + $SNAPCRAFT_PART_INSTALL/usr/bin/pip3 install wheel + $SNAPCRAFT_PART_INSTALL/usr/bin/pip3 install pyang + $SNAPCRAFT_PART_INSTALL/usr/bin/pip3 install pyangbind + PATH="$PATH:$SNAPCRAFT_PART_INSTALL/usr/local/bin" + BRANCH_OR_TAG=`git symbolic-ref -q --short HEAD || git describe --tags --exact-match 2>/dev/null || echo $BRANCH` + git clone https://osm.etsi.org/gerrit/osm/IM.git + cd IM/ + git checkout $BRANCH_OR_TAG + $SNAPCRAFT_PART_INSTALL/usr/bin/pip3 install . + cd ../ && rm -rf IM/ + $SNAPCRAFT_PART_INSTALL/usr/bin/pip3 install . + rm -rf .tox + snapcraftctl build diff --git a/test-requirements.txt b/test-requirements.txt index 301fb43..e944b57 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,4 +16,3 @@ nose mock -git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im \ No newline at end of file diff --git a/tox.ini b/tox.ini index 4f7d816..b02cab2 100644 --- a/tox.ini +++ b/tox.ini @@ -15,35 +15,39 @@ # under the License. # [tox] -envlist = py27,py35,flake8,pyflakes +envlist = py36,flakes toxworkdir={toxinidir}/.tox [testenv] -deps = -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +install_command = python3 -m pip install -U {opts} {packages} commands=nosetests -install_command = python -m pip install -r test-requirements.txt -U {opts} {packages} -[testenv:flake8] +[testenv:flakes] basepython = python3 deps = flake8 + pyflakes + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +install_command = python3 -m pip install -U {opts} {packages} commands = flake8 setup.py - -[testenv:pyflakes] -basepython = python3 -deps = pyflakes -commands = pyflakes osmclient [testenv:build] basepython = python deps = stdeb setuptools-version-command -commands = python setup.py --command-packages=stdeb.command bdist_deb + -r{toxinidir}/requirements.txt +install_command = python2 -m pip install -U {opts} {packages} +commands = python2 setup.py --command-packages=stdeb.command bdist_deb [testenv:build3] basepython = python3 deps = stdeb setuptools-version-command + -r{toxinidir}/requirements.txt +install_command = python3 -m pip install -U {opts} {packages} commands = python3 setup.py --command-packages=stdeb.command bdist_deb -- 2.25.1