Merge branch 'master' into v7.0 22/8922/1 v7.1.0rc1
authorbeierlm <mark.beierl@canonical.com>
Sat, 16 May 2020 00:41:16 +0000 (20:41 -0400)
committerbeierlm <mark.beierl@canonical.com>
Sat, 16 May 2020 00:43:30 +0000 (20:43 -0400)
Change-Id: I48a508a25fb86c3c2ce480b24053aded97a56f85
Signed-off-by: beierlm <mark.beierl@canonical.com>
21 files changed:
osmclient/common/package_tool.py
osmclient/common/utils.py
osmclient/common/wait.py
osmclient/scripts/osm.py
osmclient/sol005/client.py
osmclient/sol005/http.py
osmclient/sol005/k8scluster.py
osmclient/sol005/ns.py
osmclient/sol005/nsd.py
osmclient/sol005/nsi.py
osmclient/sol005/nst.py
osmclient/sol005/package.py
osmclient/sol005/pdud.py
osmclient/sol005/project.py
osmclient/sol005/repo.py
osmclient/sol005/role.py
osmclient/sol005/sdncontroller.py
osmclient/sol005/user.py
osmclient/sol005/vim.py
osmclient/sol005/vnfd.py
osmclient/sol005/wim.py

index 7cc10be..d60c1d0 100644 (file)
@@ -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,10 +149,11 @@ class PackageTool(object):
                 - package_folder: is the folder where we have the files to calculate the checksum
             :returns: None
         """
+        self._logger.debug("")
         files = [f for f in glob.glob(package_folder + "/**/*.*", recursive=True)]
-        checksum = open("{}/checksum.txt".format(package_folder), "w+")
+        checksum = open("{}/checksums.txt".format(package_folder), "w+")
         for file_item in files:
-            if "checksum.txt" in file_item:
+            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()
@@ -215,6 +227,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 +247,7 @@ class PackageTool(object):
 
             :return: Missing paths Dict
         """
+        self._logger.debug("")
         missing_paths = {}
         folders = []
         files = []
@@ -249,6 +263,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 +309,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 +335,120 @@ 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("")
+        try:
+            directory_name = self.create_temp_dir(package_folder, charm_list)
+            cwd = os.getcwd()
+            os.chdir(directory_name)
+            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)
+            #self.build("{}".format(os.path.basename(package_folder)))
+            os.chdir(cwd)
+        except Exception as exc:
+            shutil.rmtree(os.path.join(package_folder, "tmp"))
+            raise ClientException('failure during build of targz file (create temp dir, calculate checksum, tar.gz file): {}'.format(exc))
+        os.rename("{}/{}.tar.gz".format(directory_name, os.path.basename(package_folder)),
+                  "{}.tar.gz".format(os.path.basename(package_folder)))
+        os.rename("{}/{}/checksums.txt".format(directory_name, os.path.basename(package_folder)),
+                  "{}/checksums.txt".format(package_folder))
+        shutil.rmtree(os.path.join(package_folder, "tmp"))
+        print("Package created: {}.tar.gz".format(os.path.basename(package_folder)))
+        return "{}.tar.gz".format(package_folder)
+
+    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("{}/tmp".format(package_folder))
+        os.makedirs("{}/{}".format(directory_name, os.path.basename(package_folder)),exist_ok=True)
+        self._logger.debug("Makedirs DONE: {}/{}".format(directory_name, os.path.basename(package_folder)))
+        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, os.path.basename(package_folder)), 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
+
+    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
+
index 43287e9..ec0e0b0 100644 (file)
@@ -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
 
index e3152af..bb9a82a 100644 (file)
 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_<ENTITY>_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/<id>       ---> ''
-            # /nslcm/v1/ns_instances_content/<id> ---> '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)
index 280264b..46ceb91 100755 (executable)
@@ -97,6 +97,22 @@ def check_client_version(obj, what, version='sol005'):
                    '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',
@@ -118,14 +134,16 @@ def check_client_version(obj, what, version='sol005'):
 #              help='hostname of RO server.  ' +
 #                   'Also can set OSM_RO_PORT in environment')
 @click.pass_context
-def cli_osm(ctx, hostname, user, password, project, verbose):
+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 = {'verbose': verbose}
+    # 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:
@@ -135,12 +153,16 @@ def cli_osm(ctx, hostname, user, password, project, verbose):
 #    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')
 
@@ -206,9 +228,11 @@ def ns_list(ctx, filter, long):
     def summarize_deployment_status(status_dict):
         #Nets
         summary = ""
+        if not status_dict:
+            return summary
         n_nets = 0
         status_nets = {}
-        net_list = status_dict['nets']
+        net_list = status_dict.get('nets',[])
         for net in net_list:
             n_nets += 1
             if net['status'] not in status_nets:
@@ -256,6 +280,9 @@ def ns_list(ctx, filter, long):
         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:
@@ -268,7 +295,6 @@ def ns_list(ctx, filter, long):
                 status_ee[ee['elementType']][ee['status']] += 1
             else:
                 status_ee[ee['elementType']][ee['status']] = 1
-        summary = ""
         for elementType in ["KDU", "VDU", "PDU", "VNF", "NS"]:
             if elementType in status_ee:
                 message = ""
@@ -280,6 +306,7 @@ def ns_list(ctx, filter, long):
                 summary += "{}: {}".format(elementType, message)
         summary += "TOTAL Exec. Env.: {}".format(n_ee)
         return summary
+
     logger.debug("")
     if filter:
         check_client_version(ctx.obj, '--filter')
@@ -312,13 +339,14 @@ def ns_list(ctx, filter, long):
         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['nsState']
+            ns_state = nsr.get('nsState', nsr['_admin']['nsState'])
             if long:
-                deployment_status = summarize_deployment_status(nsr['deploymentStatus'])
-                config_status = summarize_config_status(nsr['configurationStatus'])
+                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:
@@ -335,10 +363,13 @@ def ns_list(ctx, filter, long):
                         break
                 #vim = '{} ({})'.format(vim_name, vim_id)
                 vim = vim_name
-            current_operation = "{} ({})".format(nsr['currentOperation'],nsr['currentOperationID'])
+            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":
-                error_details = "{}\nDetail: {}".format(nsr['errorDescription'],nsr['errorDetail'])
+            if ns_state == "BROKEN" or ns_state == "DEGRADED" or nsr.get('errorDescription'):
+                error_details = "{}\nDetail: {}".format(nsr['errorDescription'], nsr['errorDetail'])
         else:
             nsopdata = ctx.obj.ns.get_opdata(ns['id'])
             nsr = nsopdata['nsr:nsr']
@@ -348,9 +379,9 @@ def ns_list(ctx, filter, long):
             project = '-'
             deployment_status = nsr['operational-status'] if 'operational-status' in nsr else 'Not found'
             ns_state = deployment_status
-            config_status = nsr['config-status'] if 'config-status' in nsr else 'Not found'
+            config_status = nsr.get('config-status', 'Not found')
             current_operation = "Unknown"
-            error_details = nsr['detailed-status'] if 'detailed-status' in nsr else 'Not found'
+            error_details = nsr.get('detailed-status', 'Not found')
             if config_status == "config_not_needed":
                 config_status = "configured (no charms)"
 
@@ -379,7 +410,7 @@ def ns_list(ctx, filter, long):
     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')
@@ -387,15 +418,28 @@ def nsd_list(ctx, 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)
 
@@ -403,24 +447,26 @@ def nsd_list(ctx, filter):
 @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"""
     logger.debug("")
-    nsd_list(ctx, filter)
+    nsd_list(ctx, filter, long)
 
 
 @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"""
     logger.debug("")
-    nsd_list(ctx, filter)
+    nsd_list(ctx, filter, long)
 
 
-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')
@@ -444,13 +490,26 @@ 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', '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','-')
+                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'], 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'
@@ -461,41 +520,44 @@ def vnfd_list(ctx, nf_type, filter):
 @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)"""
     logger.debug("")
-    vnfd_list(ctx, nf_type, filter)
+    vnfd_list(ctx, nf_type, filter, long)
 
 
 @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)"""
     logger.debug("")
-    vnfd_list(ctx, nf_type, filter)
+    vnfd_list(ctx, nf_type, filter, long)
 
 
 @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)"""
     logger.debug("")
     # try:
     check_client_version(ctx.obj, ctx.command.name)
-    vnfd_list(ctx, nf_type, filter)
+    vnfd_list(ctx, nf_type, filter, long)
     # except ClientException as e:
     #     print(str(e))
     #     exit(1)
 
 
-def vnf_list(ctx, ns, filter):
+def vnf_list(ctx, ns, filter, long):
     # try:
     if ns or filter:
         if ns:
@@ -510,24 +572,23 @@ def vnf_list(ctx, ns, filter):
     #     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',
@@ -551,19 +612,21 @@ def vnf_list(ctx, ns, filter):
 @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"""
     logger.debug("")
-    vnf_list(ctx, ns, filter)
+    vnf_list(ctx, ns, filter, 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
@@ -657,13 +720,13 @@ def ns_op_list(ctx, name, long):
             action_name = op['operationParams']['primitive']
         detail = "-"
         if op['operationState']=='PROCESSING':
-            if op['lcmOperationType']=='instantiate':
+            if op['lcmOperationType'] in ('instantiate', 'terminate'):
                 if op['stage']:
                     detail = op['stage']
             else:
                 detail = "In queue. Current position: {}".format(op['queuePosition'])
-        elif op['operationState']=='FAILED' or op['operationState']=='FAILED_TEMP':
-            detail = op['errorMessage']
+        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:
@@ -677,7 +740,7 @@ def ns_op_list(ctx, name, long):
                            wrap_text(text=detail,width=50)])
         else:
             table.add_row([op['id'], op['lcmOperationType'], action_name,
-                           op['operationState'], date, wrap_text(text=detail,width=50)])
+                           op['operationState'], date, wrap_text(text=detail or "",width=50)])
     table.align = 'l'
     print(table)
 
@@ -867,7 +930,7 @@ def nsd_show(ctx, name, literal):
 
     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)
 
@@ -915,7 +978,7 @@ def vnfd_show(ctx, name, literal):
 
     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)
 
@@ -1021,7 +1084,7 @@ def vnf_show(ctx, name, literal, filter, kdu):
             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 \
-            "notes" in op_status["info"]["status"] and "seconds" in op_status["info"]["last_deployed"]:
+            "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"]))
@@ -1032,8 +1095,9 @@ def vnf_show(ctx, name, literal, filter, kdu):
                 print()
                 print("RESOURCES:")
                 print(op_status["info"]["status"]["resources"])
-                print("NOTES:")
-                print(op_status["info"]["status"]["notes"])
+                if "notes" in op_status["info"]["status"]:
+                    print("NOTES:")
+                    print(op_status["info"]["status"]["notes"])
             else:
                 print(op_info_status)
         except Exception:
@@ -1343,11 +1407,11 @@ def pdu_show(ctx, name, literal, filter):
 # CREATE operations
 ####################
 
-def nsd_create(ctx, filename, overwrite):
+def nsd_create(ctx, filename, overwrite, skip_charm_build):
     logger.debug("")
     # try:
     check_client_version(ctx.obj, ctx.command.name)
-    ctx.obj.nsd.create(filename, overwrite)
+    ctx.obj.nsd.create(filename, overwrite=overwrite, skip_charm_build=skip_charm_build)
     # except ClientException as e:
     #     print(str(e))
     #     exit(1)
@@ -1360,14 +1424,19 @@ def nsd_create(ctx, filename, overwrite):
 @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.pass_context
-def nsd_create1(ctx, filename, overwrite):
-    """creates a new NSD/NSpkg
+def nsd_create1(ctx, filename, overwrite, skip_charm_build):
+    """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.
     """
     logger.debug("")
-    nsd_create(ctx, filename, overwrite)
+    nsd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build)
 
 
 @cli_osm.command(name='nspkg-create', short_help='creates a new NSD/NSpkg')
@@ -1377,21 +1446,28 @@ def nsd_create1(ctx, filename, overwrite):
 @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.pass_context
-def nsd_create2(ctx, filename, overwrite):
-    """creates a new NSD/NSpkg
+def nsd_create2(ctx, filename, overwrite, skip_charm_build):
+    """onboards a new NSpkg
 
-    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.
     """
     logger.debug("")
-    nsd_create(ctx, filename, overwrite)
+    nsd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build)
 
 
-def vnfd_create(ctx, filename, overwrite):
+def vnfd_create(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt):
     logger.debug("")
     # try:
     check_client_version(ctx.obj, ctx.command.name)
-    ctx.obj.vnfd.create(filename, overwrite)
+    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)
@@ -1404,14 +1480,26 @@ def vnfd_create(ctx, filename, overwrite):
 @click.option('--override', 'overwrite', default=None,
               help='overrides fields in descriptor, format: '
                    '"key1.key2...=value[;key3...=value;...]"')
-@click.pass_context
-def vnfd_create1(ctx, filename, overwrite):
-    """creates a new VNFD/VNFpkg
+@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.pass_context
+def vnfd_create1(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt):
+    """onboards a new NFpkg (alias of nfpkg-create) (TO BE DEPRECATED)
 
-    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.
     """
     logger.debug("")
-    vnfd_create(ctx, filename, overwrite)
+    vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build,
+                override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt)
 
 
 @cli_osm.command(name='vnfpkg-create', short_help='creates a new VNFD/VNFpkg')
@@ -1421,14 +1509,26 @@ def vnfd_create1(ctx, filename, overwrite):
 @click.option('--override', 'overwrite', default=None,
               help='overrides fields in descriptor, format: '
                    '"key1.key2...=value[;key3...=value;...]"')
-@click.pass_context
-def vnfd_create2(ctx, filename, overwrite):
-    """creates a new VNFD/VNFpkg
+@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.pass_context
+def vnfd_create2(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt):
+    """onboards a new NFpkg (alias of nfpkg-create)
 
-    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.
     """
     logger.debug("")
-    vnfd_create(ctx, filename, overwrite)
+    vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build,
+                override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt)
 
 
 @cli_osm.command(name='nfpkg-create', short_help='creates a new NFpkg')
@@ -1438,14 +1538,26 @@ def vnfd_create2(ctx, filename, overwrite):
 @click.option('--override', 'overwrite', default=None,
               help='overrides fields in descriptor, format: '
                    '"key1.key2...=value[;key3...=value;...]"')
-@click.pass_context
-def nfpkg_create(ctx, filename, overwrite):
-    """creates a new NFpkg
+@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.pass_context
+def nfpkg_create(ctx, filename, overwrite, skip_charm_build, override_epa, override_nonepa, override_paravirt):
+    """onboards a new NFpkg (alias of nfpkg-create)
 
-    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.
     """
     logger.debug("")
-    vnfd_create(ctx, filename, overwrite)
+    vnfd_create(ctx, filename, overwrite=overwrite, skip_charm_build=skip_charm_build,
+                override_epa=override_epa, override_nonepa=override_nonepa, override_paravirt=override_paravirt)
 
 
 @cli_osm.command(name='ns-create', short_help='creates a new Network Service instance')
@@ -1525,7 +1637,7 @@ def nst_create(ctx, filename, overwrite):
 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)
@@ -1906,6 +2018,9 @@ def nfpkg_delete(ctx, name, force):
 @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,
@@ -1913,7 +2028,7 @@ def nfpkg_delete(ctx, name, force):
               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
@@ -1921,10 +2036,10 @@ def ns_delete(ctx, name, force, wait):
     logger.debug("")
     # try:
     if not force:
-        ctx.obj.ns.delete(name, wait=wait)
+        ctx.obj.ns.delete(name, config=config, wait=wait)
     else:
         check_client_version(ctx.obj, '--force')
-        ctx.obj.ns.delete(name, force, wait=wait)
+        ctx.obj.ns.delete(name, force, config=config, wait=wait)
     # except ClientException as e:
     #     print(str(e))
     #     exit(1)
@@ -2055,7 +2170,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")
@@ -2112,7 +2227,8 @@ 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,
@@ -2189,8 +2305,10 @@ def vim_delete(ctx, name, force, wait):
 #              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:
@@ -2202,9 +2320,34 @@ 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'])
     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']])
     table.align = 'l'
     print(table)
 
@@ -2256,7 +2399,7 @@ 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 "
@@ -2440,9 +2583,10 @@ def wim_show(ctx, name):
 @click.option('--port',  # hidden=True,
               help='Deprecated. Use --url')
 @click.option('--switch_dpid',  # hidden=True,
-              help='Deprecated. Use --config {dpid: DPID}')
+              help='Deprecated. Use --config {switch_id: DPID}')
 @click.option('--config',
-              help='Extra information for SDN in yaml format, as {dpid: (Openflow Datapath ID), version: version}')
+              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',
@@ -2462,16 +2606,16 @@ def sdnc_create(ctx, **kwargs):
     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")
+        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")
+        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={dpid: DPID}' instead")
+        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}'"
+        print("option '--sdn_controller_version' is deprecated, use '--config={version: SDN_CONTROLLER_VERSION}'"
               " instead")
     # try:
     check_client_version(ctx.obj, ctx.command.name)
@@ -2487,7 +2631,8 @@ def sdnc_create(ctx, **kwargs):
 @click.option('--type', help='SDN controller type')
 @click.option('--url', help='URL in format http[s]://HOST:IP/')
 @click.option('--config', help='Extra information for SDN in yaml format, as '
-                               '{dpid: (Openflow Datapath ID), version: version}')
+                               '{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('--ip_address', help='Deprecated. Use --url')  # hidden=True
@@ -2508,13 +2653,13 @@ def sdnc_update(ctx, **kwargs):
     if kwargs.get("newname"):
         sdncontroller["name"] = kwargs["newname"]
     if kwargs.get("port"):
-        print("option '--port' is deprecated, use '-url' instead")
+        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")
+        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={dpid: DPID}' instead")
+        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}'"
@@ -2610,7 +2755,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',
@@ -2647,7 +2792,8 @@ def k8scluster_add(ctx,
     cluster['k8s_version'] = version
     cluster['vim_account'] = vim
     cluster['nets'] = yaml.safe_load(k8s_nets)
-    cluster['description'] = description
+    if description:
+        cluster['description'] = description
     if namespace: cluster['namespace'] = namespace
     if cni: cluster['cni'] = yaml.safe_load(cni)
     ctx.obj.k8scluster.create(name, cluster)
@@ -2784,7 +2930,7 @@ def k8scluster_show(ctx, name, literal):
               prompt=True,
               help='type of repo (helm-chart for Helm Charts, juju-bundle for Juju Bundles)')
 @click.option('--description',
-              default='',
+              default=None,
               help='human readable description')
 #@click.option('--wait',
 #              is_flag=True,
@@ -2806,7 +2952,8 @@ def repo_add(ctx,
     repo['name'] = name
     repo['url'] = uri
     repo['type'] = type
-    repo['description'] = description
+    if description:
+        repo['description'] = description
     ctx.obj.repo.create(name, repo)
     # except ClientException as e:
     #     print(str(e))
@@ -2927,15 +3074,21 @@ def repo_show(ctx, name, literal):
 #@click.option('--description',
 #              default='no description',
 #              help='human readable description')
+@click.option('--domain-name', 'domain_name',
+              default=None,
+              help='assign to a domain')
 @click.pass_context
-def project_create(ctx, name):
+def project_create(ctx, name, domain_name):
     """Creates a new project
 
     NAME: name of the project
+    DOMAIN_NAME: optional domain name for the project when keystone authentication is used
     """
     logger.debug("")
     project = {}
     project['name'] = name
+    if domain_name:
+        project['domain_name'] = domain_name
     # try:
     check_client_version(ctx.obj, ctx.command.name)
     ctx.obj.project.create(name, project)
@@ -3050,9 +3203,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
@@ -3060,6 +3216,7 @@ 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 = {}
@@ -3067,7 +3224,9 @@ def user_create(ctx, username, password, projects, project_role_mappings):
     user['password'] = password
     user['projects'] = projects
     user['project_role_mappings'] = project_role_mappings
-    
+    if domain_name:
+        user['domain_name'] = domain_name
+
     # try:
     check_client_version(ctx.obj, ctx.command.name)
     ctx.obj.user.create(username, user)
@@ -3088,16 +3247,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):
@@ -3319,15 +3478,17 @@ def get_version(ctx):
 
 @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)
     """
     logger.debug("")
     # try:
-    ctx.obj.package.upload(filename)
+    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)
@@ -3495,6 +3656,7 @@ def upload_package(ctx, filename):
 @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,
@@ -3510,6 +3672,7 @@ def ns_action(ctx,
               action_name,
               params,
               params_file,
+              timeout,
               wait):
     """executes an action/primitive over a NS instance
 
@@ -3527,6 +3690,8 @@ def ns_action(ctx,
         op_data['vdu_id'] = vdu_id
     if vdu_count:
         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:
@@ -3548,13 +3713,18 @@ def ns_action(ctx,
 @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)
 
@@ -3567,7 +3737,7 @@ def vnf_scale(ctx,
     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)
+    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)
@@ -3794,9 +3964,14 @@ def package_create(ctx,
 @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.
 
@@ -3805,7 +3980,7 @@ def package_validate(ctx,
     """
     # try:
     check_client_version(ctx.obj, ctx.command.name)
-    results = ctx.obj.package_tool.validate(base_directory)
+    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
@@ -3827,10 +4002,13 @@ def package_validate(ctx,
               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.
 
@@ -3839,7 +4017,9 @@ def package_build(ctx,
     """
     # try:
     check_client_version(ctx.obj, ctx.command.name)
-    results = ctx.obj.package_tool.build(package_folder, skip_validation)
+    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))
index 3ceb1b3..5e28797 100644 (file)
@@ -55,11 +55,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]
@@ -69,7 +70,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)
@@ -104,8 +105,13 @@ class Client(object):
             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)
+                                                         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)
@@ -120,6 +126,22 @@ class Client(object):
                 self._http_client.set_http_header(http_header)
 
     def get_version(self):
-        _, resp = self._http_client.get2_cmd(endpoint="/version")
-        resp = json.loads(resp)
-        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)
index 3d21465..d130879 100644 (file)
@@ -25,18 +25,44 @@ from osmclient.common.exceptions import OsmHttpException, NotFound
 
 class Http(http.Http):
 
-    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 _get_curl_cmd(self, endpoint):
+    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, skip_query_admin=False):
         self._logger.debug("")
         curl_cmd = pycurl.Curl()
         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.URL, self._url + endpoint)
         curl_cmd.setopt(pycurl.SSL_VERIFYPEER, 0)
         curl_cmd.setopt(pycurl.SSL_VERIFYHOST, 0)
@@ -44,10 +70,10 @@ 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))
@@ -66,10 +92,11 @@ class Http(http.Http):
 
     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:
@@ -118,36 +145,39 @@ class Http(http.Http):
             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))
@@ -174,3 +204,10 @@ class Http(http.Http):
             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()
index 0cdb089..4ac2e48 100644 (file)
@@ -135,7 +135,7 @@ class K8scluster(object):
             if resp:
                 resp = json.loads(resp)
             if not resp or '_id' not in resp:
-                raise ClientException('failed to get K8s cluster info: '.format(resp))
+                raise ClientException('failed to get K8s cluster info: {}'.format(resp))
             return resp
         except NotFound:
             raise NotFound("K8s cluster {} not found".format(name))
index f0877fc..b551868 100644 (file)
@@ -40,15 +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)
@@ -100,21 +102,42 @@ class Ns(object):
             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:
@@ -180,42 +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)
-            if "timeout_ns_deploy" in ns_config:
-                ns["timeout_ns_deploy"] = ns_config.pop("timeout_ns_deploy")
+            # rest of parameters without any transformation or checking
+            # "timeout_ns_deploy"
+            # "placement-engine"
+            ns.update(ns_config)
 
         # print(yaml.safe_dump(ns))
         try:
@@ -239,7 +264,7 @@ class Ns(object):
                                       resp))
             if wait:
                 # Wait for status for NS instance creation
-                self._wait(resp.get('nslcmop_id'))
+                self._wait(resp.get('nslcmop_id'), wait)
             print(resp['id'])
             return resp['id']
             #else:
@@ -269,7 +294,7 @@ class Ns(object):
             filter_string = ''
             if filter:
                  filter_string = '&{}'.format(filter)
-            http_code, resp = self._http.get2_cmd('{}?nsInstanceId={}'.format(
+            http_code, resp = self._http.get2_cmd('{}?nsInstanceId={}{}'.format(
                                                        self._apiBase, ns['_id'],
                                                        filter_string) )
             #print('HTTP CODE: {}'.format(http_code))
@@ -353,7 +378,7 @@ class Ns(object):
             if wait:
                 # Wait for status for NS instance action
                 # For the 'action' operation, 'id' is used
-                self._wait(resp.get('id'))
+                self._wait(resp.get('id'), wait)
             return resp['id']
             #else:
             #    msg = ""
@@ -369,7 +394,7 @@ class Ns(object):
                     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("")
@@ -378,14 +403,18 @@ class Ns(object):
             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:
index 5979bc0..9aca930 100644 (file)
@@ -25,6 +25,7 @@ import json
 import magic
 from os.path import basename
 import logging
+import os.path
 #from os import stat
 
 
@@ -138,62 +139,69 @@ class Nsd(object):
             #         msg = resp
             raise ClientException("failed to delete nsd {} - {}".format(name, msg))
 
-    def create(self, filename, overwrite=None, update_endpoint=None):
+    def create(self, filename, overwrite=None, update_endpoint=None, skip_charm_build=False):
         self._logger.debug("")
-        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)
+        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):
-            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("")
index 4b522a8..be1deea 100644 (file)
@@ -40,16 +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)
@@ -116,7 +118,7 @@ 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:
@@ -240,7 +242,7 @@ class Nsi(object):
                                   resp))
             if wait:
                 # Wait for status for NSI instance creation
-                self._wait(resp.get('nsilcmop_id'))
+                self._wait(resp.get('nsilcmop_id'), wait)
             print(resp['id'])
             #else:
             #    msg = ""
@@ -269,7 +271,7 @@ 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))
index 3b15e96..214a79d 100644 (file)
@@ -24,6 +24,7 @@ from osmclient.common import utils
 import json
 import magic
 import logging
+import os.path
 #from os import stat
 #from os.path import basename
 
@@ -139,57 +140,75 @@ class Nst(object):
 
     def create(self, filename, overwrite=None, update_endpoint=None):
         self._logger.debug("")
-        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)
-        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)
+        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:
-            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))
-        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))
+            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))
+            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))
 
     def update(self, name, filename):
         self._logger.debug("")
index 1ca3864..622fb86 100644 (file)
@@ -25,6 +25,7 @@ from osmclient.common.exceptions import NotFound
 from osmclient.common import utils
 import json
 import logging
+import os.path
 
 
 class Package(object):
@@ -74,44 +75,49 @@ class Package(object):
             raise ClientException("package {} failed to upload"
                                   .format(filename))
 
-    def upload(self, filename):
+    def upload(self, filename, skip_charm_build=False):
         self._logger.debug("")
-        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'
+        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):
-        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 upload package - {}".format(msg))
+            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))
+            print(resp['id'])
+            #  else:
+            #     msg = ""
+            #     if resp:
+            #         try:
+            #              msg = json.loads(resp)
+            #         except ValueError:
+            #             msg = resp
+            #     raise ClientException("failed to upload package - {}".format(msg))
index 3abb78c..aa4bf69 100644 (file)
@@ -118,7 +118,7 @@ class Pdu(object):
         if resp:
             resp = json.loads(resp)
         if not resp or 'id' not in resp:
-            raise ClientException('unexpected response from server: '.format(
+            raise ClientException('unexpected response from server: {}'.format(
                                   resp))
         print(resp['id'])
         #else:
index dc2e9a5..ed781fa 100644 (file)
@@ -43,7 +43,8 @@ class Project(object):
         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):
@@ -69,7 +70,8 @@ class Project(object):
         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):
@@ -100,7 +102,8 @@ class Project(object):
         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:
@@ -126,7 +129,8 @@ class Project(object):
         filter_string = ''
         if filter:
             filter_string = '?{}'.format(filter)
-        _, resp = self._http.get2_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 json.loads(resp)
index 7a31397..cc82402 100644 (file)
@@ -129,7 +129,7 @@ class Repo(object):
             if resp:
                 resp = json.loads(resp)
             if not resp or '_id' not in resp:
-                raise ClientException('failed to get repo info: '.format(resp))
+                raise ClientException('failed to get repo info: {}'.format(resp))
             return resp
         except NotFound:
             raise NotFound("Repo {} not found".format(name))
index c80e50e..2544a7b 100644 (file)
@@ -65,7 +65,8 @@ 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):
@@ -148,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):
@@ -184,7 +186,8 @@ class Role(object):
         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:
@@ -214,7 +217,7 @@ class Role(object):
         filter_string = ''
         if filter:
             filter_string = '?{}'.format(filter)
-        _, resp = self._http.get2_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 json.loads(resp)
index 35ad9ee..5b85ef4 100644 (file)
@@ -24,6 +24,7 @@ from osmclient.common.exceptions import ClientException
 from osmclient.common.exceptions import NotFound
 import json
 import logging
+import yaml
 
 
 class SdnController(object):
@@ -38,16 +39,18 @@ 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)
@@ -66,6 +69,8 @@ class SdnController(object):
 
     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)
@@ -79,7 +84,7 @@ class SdnController(object):
                                   resp))
         if wait:
             # Wait for status for SDNC instance creation
-            self._wait(resp.get('id'))
+            self._wait(resp.get('id'), wait)
         print(resp['id'])
         #else:
         #    msg = ""
@@ -92,6 +97,8 @@ class SdnController(object):
 
     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)
@@ -105,7 +112,7 @@ class SdnController(object):
             # Use the previously obtained id instead.
             wait_id = sdnc_id_for_wait
             # Wait for status for VI instance update
-            self._wait(wait_id)
+            self._wait(wait_id, wait)
         # else:
         #     pass
         #else:
@@ -132,7 +139,7 @@ class SdnController(object):
         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:
index fac26cd..d28514e 100644 (file)
@@ -19,7 +19,6 @@
 OSM user mgmt API
 """
 
-from osmclient.common import utils
 from osmclient.common.exceptions import ClientException
 from osmclient.common.exceptions import NotFound
 import json
@@ -57,15 +56,15 @@ 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):
@@ -145,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):
@@ -176,7 +175,7 @@ class User(object):
         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:
@@ -202,7 +201,7 @@ class User(object):
         filter_string = ''
         if filter:
             filter_string = '?{}'.format(filter)
-        _, resp = self._http.get2_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 json.loads(resp)
@@ -213,13 +212,14 @@ class User(object):
         """
         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))
 
index 9cd882c..3441161 100644 (file)
@@ -39,16 +39,18 @@ 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)
@@ -102,7 +104,7 @@ class Vim(object):
                                   resp))
         if wait:
             # Wait for status for VIM instance creation
-            self._wait(resp.get('id'))
+            self._wait(resp.get('id'), wait)
         print(resp['id'])
         #else:
         #    msg = ""
@@ -126,12 +128,16 @@ 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']),
@@ -144,7 +150,7 @@ class Vim(object):
             # Use the previously obtained id instead.
             wait_id = vim_id_for_wait
             # Wait for status for VI instance update
-            self._wait(wait_id)
+            self._wait(wait_id, wait)
         # else:
         #     pass
         #else:
@@ -197,7 +203,7 @@ 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:
index 8bf3552..48fbdb7 100644 (file)
@@ -22,10 +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
 import logging
-#from os import stat
+import os.path
+from urllib.parse import quote
+import tarfile
+from osm_im.validation import Validation as validation_im
 
 
 class Vnfd(object):
@@ -136,62 +140,131 @@ class Vnfd(object):
             #         msg = resp
             raise ClientException("failed to delete vnfd {} - {}".format(name, msg))
 
-    def create(self, filename, overwrite=None, update_endpoint=None):
+    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("")
-        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)
-        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)
+        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:
-            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
+                    vdu_list = vnfd[k]['vnfd'][0]['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("")
index f9d2431..5da3941 100644 (file)
@@ -39,16 +39,18 @@ 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)
@@ -96,7 +98,7 @@ class Wim(object):
                                   resp))
         if wait:
             # Wait for status for WIM instance creation
-            self._wait(resp.get('id'))
+            self._wait(resp.get('id'), wait)
         print(resp['id'])
         #else:
         #    msg = ""
@@ -135,7 +137,7 @@ class Wim(object):
             # Use the previously obtained id instead.
             wait_id = wim_id_for_wait
             # Wait for status for WIM instance update
-            self._wait(wait_id)
+            self._wait(wait_id, wait)
         # else:
         #     pass
         #else:
@@ -190,7 +192,7 @@ 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:
@@ -234,7 +236,7 @@ class Wim(object):
             if resp:
                 resp =  json.loads(resp)
             if not resp or '_id' not in resp:
-                raise ClientException('failed to get wim info: '.format(resp))
+                raise ClientException('failed to get wim info: {}'.format(resp))
             return resp
         except NotFound:
             raise NotFound("wim '{}' not found".format(name))