X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=osmclient%2Fscripts%2Fosm.py;h=de121b126b66d6993f88c44e95d2fc9d1ea18cfd;hb=e1735437d0506be22598161ae42f3b448177e90f;hp=df638a0e0359d6f4e46cce1c633152e3e361d98d;hpb=c4d4027967ad2ddf39e464f9711b77516002a6b8;p=osm%2Fosmclient.git diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index df638a0..de121b1 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -21,6 +21,7 @@ OSM shell/cli import click from osmclient import client from osmclient.common.exceptions import ClientException, NotFound +from osmclient.common.utils import validate_uuid4 from prettytable import PrettyTable import yaml import json @@ -94,6 +95,26 @@ def get_vim_name(vim_list, vim_id): return vim_name +def create_config(config_file, json_string): + """ + Combines a YAML or JSON file with a JSON string into a Python3 structure + It loads the YAML or JSON file 'cfile' into a first dictionary. + It loads the JSON string into a second dictionary. + Then it updates the first dictionary with the info in the second dictionary. + If the field is present in both cfile and cdict, the field in cdict prevails. + If both cfile and cdict are None, it returns an empty dict (i.e. {}) + """ + config = {} + if config_file: + with open(config_file, "r") as cf: + config = yaml.safe_load(cf.read()) + if json_string: + cdict = yaml.safe_load(json_string) + for k, v in cdict.items(): + config[k] = v + return config + + @click.group( context_settings=dict(help_option_names=["-h", "--help"], max_content_width=160) ) @@ -103,11 +124,6 @@ def get_vim_name(vim_list, vim_id): envvar="OSM_HOSTNAME", help="hostname of server. " + "Also can set OSM_HOSTNAME in environment", ) -# @click.option('--sol005/--no-sol005', -# default=True, -# envvar='OSM_SOL005', -# help='Use ETSI NFV SOL005 API (default) or the previous SO API. ' + -# 'Also can set OSM_SOL005 in environment') @click.option( "--user", default=None, @@ -154,26 +170,6 @@ def get_vim_name(vim_list, vim_id): 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', -# help='hostname of server. ' + -# 'Also can set OSM_SO_PORT in environment') -# @click.option('--so-project', -# default=None, -# envvar='OSM_SO_PROJECT', -# help='Project Name in SO. ' + -# 'Also can set OSM_SO_PROJECT in environment') -# @click.option('--ro-hostname', -# default=None, -# envvar='OSM_RO_HOSTNAME', -# help='hostname of RO server. ' + -# 'Also can set OSM_RO_HOSTNAME in environment') -# @click.option('--ro-port', -# default=None, -# envvar='OSM_RO_PORT', -# help='hostname of RO server. ' + -# 'Also can set OSM_RO_PORT in environment') @click.pass_context def cli_osm(ctx, **kwargs): global logger @@ -188,25 +184,7 @@ def cli_osm(ctx, **kwargs): exit(1) # 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: - # kwargs['so_project']=so_project - # if ro_hostname is not None: - # kwargs['ro_host']=ro_hostname - # 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 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") @@ -1036,7 +1014,7 @@ def ns_op_list(ctx, name, long): action_name = op["operationParams"]["primitive"] detail = "-" if op["operationState"] == "PROCESSING": - if op["queuePosition"] is not None and op["queuePosition"] > 0: + if op.get("queuePosition") is not None and op.get("queuePosition") > 0: detail = "In queue. Current position: {}".format(op["queuePosition"]) elif op["lcmOperationType"] in ("instantiate", "terminate"): if op["stage"]: @@ -2330,6 +2308,7 @@ def nfpkg_create( help="do not return the control immediately, but keep it " "until the operation is completed, or timeout", ) +@click.option("--timeout", default=None, help="ns deployment timeout") @click.pass_context def ns_create( ctx, @@ -2341,6 +2320,7 @@ def ns_create( config, config_file, wait, + timeout, ): """creates a new NS instance""" logger.debug("") @@ -2360,6 +2340,7 @@ def ns_create( ssh_keys=ssh_keys, account=vim_account, wait=wait, + timeout=timeout, ) # except ClientException as e: # print(str(e)) @@ -2583,7 +2564,9 @@ def pdu_create( check_client_version(ctx.obj, ctx.command.name) - pdu = create_pdu_dictionary(name, pdu_type, interface, description, vim_account, descriptor_file) + pdu = create_pdu_dictionary( + name, pdu_type, interface, description, vim_account, descriptor_file + ) ctx.obj.pdu.create(pdu) @@ -2631,11 +2614,15 @@ def pdu_update( if not newname: newname = name - pdu = create_pdu_dictionary(newname, pdu_type, interface, description, vim_account, descriptor_file, update) + pdu = create_pdu_dictionary( + newname, pdu_type, interface, description, vim_account, descriptor_file, update + ) ctx.obj.pdu.update(name, pdu) -def create_pdu_dictionary(name, pdu_type, interface, description, vim_account, descriptor_file, update=False): +def create_pdu_dictionary( + name, pdu_type, interface, description, vim_account, descriptor_file, update=False +): logger.debug("") pdu = {} @@ -2678,6 +2665,7 @@ def create_pdu_dictionary(name, pdu_type, interface, description, vim_account, d pdu["interfaces"] = ifaces_list return pdu + #################### # UPDATE operations #################### @@ -3094,19 +3082,34 @@ def pdu_delete(ctx, name, force): ################# +def _check_ca_cert(vim_config: dict) -> None: + """ + Checks if the VIM has a CA certificate. + In that case, reads the content and add it to the config + : param vim_config: configuration provided with the VIM creation + : return: None + """ + + if vim_config.get("ca_cert"): + with open(vim_config["ca_cert"], "r") as cert_f: + vim_config["ca_cert_content"] = str(cert_f.read()) + del vim_config["ca_cert"] + + @cli_osm.command(name="vim-create", short_help="creates a new VIM account") -@click.option("--name", prompt=True, help="Name to create datacenter") -@click.option("--user", prompt=True, help="VIM username") +@click.option("--name", required=True, help="Name to create datacenter") +@click.option("--user", default=None, help="VIM username") +@click.option("--password", default=None, help="VIM password") +@click.option("--auth_url", default=None, help="VIM url") @click.option( - "--password", - prompt=True, - hide_input=True, - confirmation_prompt=True, - help="VIM password", + "--tenant", "--project", "tenant", default=None, help="VIM tenant/project name" ) -@click.option("--auth_url", prompt=True, help="VIM url") -@click.option("--tenant", prompt=True, help="VIM tenant name") @click.option("--config", default=None, help="VIM specific config parameters") +@click.option( + "--config_file", + default=None, + help="VIM specific config parameters in YAML or JSON file", +) @click.option("--account_type", default="openstack", help="VIM type") @click.option("--description", default=None, help="human readable description") @click.option( @@ -3128,6 +3131,14 @@ def pdu_delete(ctx, name, force): "until the operation is completed, or timeout", ) @click.option("--vca", default=None, help="VCA to be used in this VIM account") +@click.option( + "--creds", default=None, help="credentials file (only applycable for GCP VIM type)" +) +@click.option( + "--prometheus_config_file", + default=None, + help="Prometheus configuration to get VIM data", +) @click.pass_context def vim_create( ctx, @@ -3137,12 +3148,15 @@ def vim_create( auth_url, tenant, config, + config_file, account_type, description, sdn_controller, sdn_port_mapping, wait, vca, + creds, + prometheus_config_file, ): """creates a new VIM account""" logger.debug("") @@ -3152,19 +3166,27 @@ def vim_create( if sdn_port_mapping: check_client_version(ctx.obj, "--sdn_port_mapping") vim = {} + if prometheus_config_file: + with open(prometheus_config_file) as prometheus_file: + prometheus_config_dict = json.load(prometheus_file) + vim["prometheus-config"] = prometheus_config_dict + vim["vim-username"] = user vim["vim-password"] = password vim["vim-url"] = auth_url vim["vim-tenant-name"] = tenant vim["vim-type"] = account_type vim["description"] = description - vim["config"] = config if vca: vim["vca"] = vca - if sdn_controller or sdn_port_mapping: - ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait) - else: - ctx.obj.vim.create(name, vim, wait=wait) + vim_config = create_config(config_file, config) + _check_ca_cert(vim_config) + if creds: + with open(creds, "r") as cf: + vim_config["credentials"] = yaml.safe_load(cf.read()) + ctx.obj.vim.create( + name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait + ) # except ClientException as e: # print(str(e)) # exit(1) @@ -3178,6 +3200,11 @@ def vim_create( @click.option("--auth_url", help="VIM url") @click.option("--tenant", help="VIM tenant name") @click.option("--config", help="VIM specific config parameters") +@click.option( + "--config_file", + default=None, + help="VIM specific config parameters in YAML or JSON file", +) @click.option("--account_type", help="VIM type") @click.option("--description", help="human readable description") @click.option( @@ -3199,6 +3226,14 @@ def vim_create( help="do not return the control immediately, but keep it " "until the operation is completed, or timeout", ) +@click.option( + "--creds", default=None, help="credentials file (only applycable for GCP VIM type)" +) +@click.option( + "--prometheus_config_file", + default=None, + help="Prometheus configuration to get VIM data", +) @click.pass_context def vim_update( ctx, @@ -3209,11 +3244,14 @@ def vim_update( auth_url, tenant, config, + config_file, account_type, description, sdn_controller, sdn_port_mapping, wait, + creds, + prometheus_config_file, ): """updates a VIM account @@ -3237,9 +3275,21 @@ def vim_update( vim["vim_type"] = account_type if description: vim["description"] = description - if config: - vim["config"] = config - ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping, wait=wait) + vim_config = None + if config or config_file: + vim_config = create_config(config_file, config) + _check_ca_cert(vim_config) + if creds: + with open(creds, "r") as cf: + vim_config["credentials"] = yaml.safe_load(cf.read()) + if prometheus_config_file: + with open(prometheus_config_file) as prometheus_file: + prometheus_config_dict = json.load(prometheus_file) + vim["prometheus-config"] = prometheus_config_dict + logger.info(f"VIM: {vim}, VIM config: {vim_config}") + ctx.obj.vim.update( + name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait + ) # except ClientException as e: # print(str(e)) # exit(1) @@ -3316,6 +3366,8 @@ def vim_list(ctx, filter, long): if long: if "vim_password" in vim: vim["vim_password"] = "********" + if "config" in vim and "credentials" in vim["config"]: + vim["config"]["credentials"] = "********" logger.debug("VIM details: {}".format(yaml.safe_dump(vim))) vim_state = vim["_admin"].get("operationalState", "-") error_details = "N/A" @@ -3348,8 +3400,9 @@ def vim_list(ctx, filter, long): multiple=True, help="restricts the information to the fields in the filter", ) +@click.option("--literal", is_flag=True, help="print literally, no pretty table") @click.pass_context -def vim_show(ctx, name, filter): +def vim_show(ctx, name, filter, literal): """shows the details of a VIM account NAME: name or ID of the VIM account @@ -3359,10 +3412,15 @@ def vim_show(ctx, name, filter): resp = ctx.obj.vim.get(name) if "vim_password" in resp: resp["vim_password"] = "********" + if "config" in resp and "credentials" in resp["config"]: + resp["config"]["credentials"] = "********" # except ClientException as e: # print(str(e)) # exit(1) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return table = PrettyTable(["key", "attribute"]) for k, v in list(resp.items()): if not filter or k in filter: @@ -3572,7 +3630,7 @@ def wim_show(ctx, name): check_client_version(ctx.obj, ctx.command.name) resp = ctx.obj.wim.get(name) if "password" in resp: - resp["wim_password"] = "********" + resp["password"] = "********" # except ClientException as e: # print(str(e)) # exit(1) @@ -3820,6 +3878,18 @@ def sdnc_show(ctx, name): 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( + "--init-helm2/--skip-helm2", required=False, default=True, help="Initialize helm v2" +) +@click.option( + "--init-helm3/--skip-helm3", required=False, default=True, help="Initialize helm v3" +) +@click.option( + "--init-jujubundle/--skip-jujubundle", + required=False, + default=True, + help="Initialize juju-bundle", +) @click.option("--description", default=None, help="human readable description") @click.option( "--namespace", @@ -3847,7 +3917,19 @@ def sdnc_show(ctx, name): # help='do not return the control immediately, but keep it until the operation is completed, or timeout') @click.pass_context def k8scluster_add( - ctx, name, creds, version, vim, k8s_nets, description, namespace, wait, cni + ctx, + name, + creds, + version, + vim, + k8s_nets, + init_helm2, + init_helm3, + init_jujubundle, + description, + namespace, + wait, + cni, ): """adds a K8s cluster to OSM @@ -3862,6 +3944,12 @@ def k8scluster_add( cluster["k8s_version"] = version cluster["vim_account"] = vim cluster["nets"] = yaml.safe_load(k8s_nets) + if not (init_helm2 and init_jujubundle and init_helm3): + cluster["deployment_methods"] = { + "helm-chart": init_helm2, + "juju-bundle": init_jujubundle, + "helm-chart-v3": init_helm3, + } if description: cluster["description"] = description if namespace: @@ -3991,6 +4079,7 @@ def k8scluster_list(ctx, filter, literal, long): "Version", "VIM", "K8s-nets", + "Deployment methods", "Operational State", "Op. state (details)", "Description", @@ -4028,6 +4117,7 @@ def k8scluster_list(ctx, filter, literal, long): cluster["k8s_version"], vim_info, json.dumps(cluster["nets"]), + json.dumps(cluster["deployment_methods"]), cluster["_admin"]["operationalState"], op_state_details, trunc_text(cluster.get("description") or "", 40), @@ -4112,11 +4202,7 @@ def k8scluster_show(ctx, name, literal): prompt=True, help="Name of the cloud credentialsto be used for the K8s cloud", ) -@click.option( - "--model-config", - default={}, - help="Configuration options for the models", -) +@click.option("--model-config", default={}, help="Configuration options for the models") @click.option("--description", default=None, help="human readable description") @click.pass_context def vca_add( @@ -4205,10 +4291,7 @@ def load_file(file_path: str) -> Dict: "--k8s-credentials", help="Name of the cloud credentialsto be used for the K8s cloud", ) -@click.option( - "--model-config", - help="Configuration options for the models", -) +@click.option("--model-config", help="Configuration options for the models") @click.option("--description", default=None, help="human readable description") @click.pass_context def vca_update( @@ -4344,6 +4427,193 @@ def vca_show(ctx, name, literal): print(table) +########################### +# PaaS operations +########################### + + +@cli_osm.command(name="paas-add", short_help="adds a PaaS to OSM.") +@click.argument("name") +@click.option( + "--paas_type", + type=click.Choice(["juju"]), + default="juju", + prompt=True, + help="Type of PaaS that can be used. (For the moment, only juju is supported).", +) +@click.option( + "--endpoints", + prompt=True, + help="Comma-separated list of IP or hostnames of the PaaS.", +) +@click.option("--user", prompt=True, help="Username with admin priviledges.") +@click.option("--secret", prompt=True, help="Password of the specified username.") +@click.option( + "--config", default={}, help="Extra configuration needed by PaaS service." +) +@click.option("--description", default=None, help="Human readable description.") +@click.pass_context +def paas_add(ctx, name, paas_type, endpoints, user, secret, config, description): + """adds a PaaS to OSM. + Args: + name (str): Name of the new PaaS. + """ + check_client_version(ctx.obj, ctx.command.name) + paas = { + "name": name, + "paas_type": paas_type, + "endpoints": endpoints.split(","), + "user": user, + "secret": secret, + } + if description: + paas["description"] = description + if config: + config = load(config) + paas["config"] = config + ctx.obj.paas.create(paas) + + +@cli_osm.command(name="paas-update", short_help="updates a PaaS") +@click.argument("name") +@click.option("--newname", help="New name for the PaaS") +@click.option( + "--paas_type", + type=click.Choice(["juju"]), + help="Type of PaaS that can be used. (For the moment, only juju is supported)", +) +@click.option( + "--endpoints", help="Comma-separated list of IP or hostnames of the Juju controller" +) +@click.option("--user", help="Username with admin priviledges") +@click.option("--secret", help="Password of the specified username") +@click.option("--config", help="Extra configuration needed by PaaS service") +@click.option("--description", default=None, help="Human readable description") +@click.pass_context +def paas_update( + ctx, name, newname, paas_type, endpoints, user, secret, config, description +): + """updates a PaaS. + Args: + name (str): Name or ID of the PaaS to update. + """ + check_client_version(ctx.obj, ctx.command.name) + paas = {} + if newname: + paas["name"] = newname + if paas_type: + paas["paas_type"] = paas_type + if endpoints: + paas["endpoints"] = endpoints.split(",") + if user: + paas["user"] = user + if secret: + paas["secret"] = secret + if description: + paas["description"] = description + if config: + config = load(config) + paas["config"] = config + ctx.obj.paas.update(name, paas) + + +@cli_osm.command(name="paas-delete", short_help="deletes a PaaS") +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def paas_delete(ctx, name, force): + """deletes a PaaS. + + Args: + name (str): Name or ID of the PaaS to delete. + """ + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.paas.delete(name, force=force) + + +@cli_osm.command(name="paas-list") +@click.option( + "--filter", + default=None, + multiple=True, + help="Restricts the list to the PaaS matching the filter", +) +@click.option("--literal", is_flag=True, help="Print literally, no pretty table") +@click.option("--long", is_flag=True, help="get more details") +@click.pass_context +def paas_list(ctx, filter, literal, long): + """List PaaSs""" + check_client_version(ctx.obj, ctx.command.name) + if filter: + filter = "&".join(filter) + resp = ctx.obj.paas.list(filter) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + + table = _get_paas_table_header(long) + project_list = ctx.obj.project.list() + for paas in resp: + logger.debug("PaaS details: {}".format(yaml.safe_dump(paas))) + if long: + _add_paas_long_row(table, paas, project_list) + else: + _add_paas_row(table, paas) + table.align = "l" + print(table) + + +def _get_paas_table_header(long): + if long: + return PrettyTable( + ["Name", "Id", "Project", "Operational State", "Detailed Status"] + ) + return PrettyTable(["Name", "Id", "Operational State"]) + + +def _add_paas_long_row(table, paas, project_list): + _, project_name = get_project(project_list, paas) + detailed_status = paas.get("_admin", {}).get("detailed-status", "-") + table.add_row( + [ + paas["name"], + paas["_id"], + project_name, + paas.get("_admin", {}).get("operationalState", "-"), + wrap_text(text=detailed_status, width=40), + ] + ) + + +def _add_paas_row(table, paas): + table.add_row( + [paas["name"], paas["_id"], paas.get("_admin", {}).get("operationalState", "-")] + ) + + +@cli_osm.command(name="paas-show", short_help="Shows the details of a PaaS") +@click.argument("name") +@click.option("--literal", is_flag=True, help="Print literally, no pretty table") +@click.pass_context +def paas_show(ctx, name, literal): + """Shows the details of a PaaS. + + Args: + name (str): Name or ID of the PaaS to show. + """ + resp = ctx.obj.paas.get(name) + if literal: + print(yaml.safe_dump(resp, indent=4, default_flow_style=False)) + return + table = PrettyTable(["key", "attribute"]) + for k, v in list(resp.items()): + table.add_row([k, wrap_text(text=json.dumps(v, indent=2), width=100)]) + table.align = "l" + print(table) + + ########################### # Repo operations ########################### @@ -4799,6 +5069,12 @@ def user_create(ctx, username, password, projects, project_role_mappings, domain multiple=True, help="remove role(s) in a project. Can be used several times: 'project,role1[,role2,...]'", ) +@click.option("--change_password", "change_password", help="user's current password") +@click.option( + "--new_password", + "new_password", + help="user's new password to update in expiry condition", +) @click.pass_context def user_update( ctx, @@ -4809,6 +5085,8 @@ def user_update( remove_project, add_project_role, remove_project_role, + change_password, + new_password, ): """Update a user information @@ -4820,6 +5098,8 @@ def user_update( REMOVE_PROJECT: deleting mappings for project/role(s) ADD_PROJECT_ROLE: adding mappings for project/role(s) REMOVE_PROJECT_ROLE: removing mappings for project/role(s) + CHANGE_PASSWORD: user's current password to change + NEW_PASSWORD: user's new password to update in expiry condition """ logger.debug("") user = {} @@ -4829,10 +5109,15 @@ def user_update( user["remove-project"] = remove_project user["add-project-role"] = add_project_role user["remove-project-role"] = remove_project_role + user["change_password"] = change_password + user["new_password"] = new_password # try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.user.update(username, user) + if not user.get("change_password"): + ctx.obj.user.update(username, user) + else: + ctx.obj.user.update(username, user, pwd_change=True) # except ClientException as e: # print(str(e)) # exit(1) @@ -5487,6 +5772,303 @@ def vnf_scale( # exit(1) +@cli_osm.command( + name="ns-update", short_help="executes an update of a Network Service." +) +@click.argument("ns_name") +@click.option( + "--updatetype", required=True, type=str, help="available types: CHANGE_VNFPKG" +) +@click.option( + "--config", + required=True, + type=str, + help="extra information for update operation as YAML/JSON inline string as --config" + " '{changeVnfPackageData:[{vnfInstanceId: xxx, vnfdId: yyy}]}'", +) +@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 update(ctx, ns_name, updatetype, config, timeout, wait): + """Executes an update of a Network Service. + + The update will check new revisions of the Network Functions that are part of the + Network Service, and it will update them if needed. + Sample update command: osm ns-update ns_instance_id --updatetype CHANGE_VNFPKG + --config '{changeVnfPackageData: [{vnfInstanceId: id_x,vnfdId: id_y}]}' --timeout 300 --wait + + NS_NAME: Network service instance name or ID. + + """ + op_data = {"timeout": timeout, "updateType": updatetype} + if config: + op_data["config"] = yaml.safe_load(config) + + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.ns.update(ns_name, op_data, wait=wait) + + +def iterator_split(iterator, separators): + """ + Splits a tuple or list into several lists whenever a separator is found + For instance, the following tuple will be separated with the separator "--vnf" as follows. + From: + ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1", "--vnf", "B", "--cause", "cause_B", ... + "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", "--vdu", "vdu_B1", "--count_index", "2") + To: + [ + ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1"), + ("--vnf", "B", "--cause", "cause_B", "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", ... + "--vdu", "vdu_B1", "--count_index", "2") + ] + + Returns as many lists as separators are found + """ + logger.debug("") + if iterator[0] not in separators: + raise ClientException(f"Expected one of {separators}. Received: {iterator[0]}.") + list_of_lists = [] + first = 0 + for i in range(len(iterator)): + if iterator[i] in separators: + if i == first: + continue + if i - first < 2: + raise ClientException( + f"Expected at least one argument after separator (possible separators: {separators})." + ) + list_of_lists.append(list(iterator[first:i])) + first = i + if (len(iterator) - first) < 2: + raise ClientException( + f"Expected at least one argument after separator (possible separators: {separators})." + ) + else: + list_of_lists.append(list(iterator[first : len(iterator)])) + # logger.debug(f"List of lists: {list_of_lists}") + return list_of_lists + + +def process_common_heal_params(heal_vnf_dict, args): + logger.debug("") + current_item = "vnf" + i = 0 + while i < len(args): + if args[i] == "--cause": + if (i + 1 >= len(args)) or args[i + 1].startswith("--"): + raise ClientException("No cause was provided after --cause") + heal_vnf_dict["cause"] = args[i + 1] + i = i + 2 + continue + if args[i] == "--run-day1": + if current_item == "vnf": + if "additionalParams" not in heal_vnf_dict: + heal_vnf_dict["additionalParams"] = {} + heal_vnf_dict["additionalParams"]["run-day1"] = True + else: + # if current_item == "vdu" + heal_vnf_dict["additionalParams"]["vdu"][-1]["run-day1"] = True + i = i + 1 + continue + if args[i] == "--vdu": + if "additionalParams" not in heal_vnf_dict: + heal_vnf_dict["additionalParams"] = {} + heal_vnf_dict["additionalParams"]["vdu"] = [] + if (i + 1 >= len(args)) or args[i + 1].startswith("--"): + raise ClientException("No VDU ID was provided after --vdu") + heal_vnf_dict["additionalParams"]["vdu"].append({"vdu-id": args[i + 1]}) + current_item = "vdu" + i = i + 2 + continue + if args[i] == "--count-index": + if current_item == "vnf": + raise ClientException( + "Option --count-index only applies to VDU, not to VNF" + ) + if (i + 1 >= len(args)) or args[i + 1].startswith("--"): + raise ClientException("No count index was provided after --count-index") + heal_vnf_dict["additionalParams"]["vdu"][-1]["count-index"] = int( + args[i + 1] + ) + i = i + 2 + continue + i = i + 1 + return + + +def process_ns_heal_params(ctx, param, value): + """ + Processes the params in the command ns-heal + Click does not allow advanced patterns for positional options like this: + --vnf volumes_vnf --cause "Heal several_volumes-VM of several_volumes_vnf" + --vdu several_volumes-VM + --vnf charm_vnf --cause "Heal two VMs of native_manual_scale_charm_vnf" + --vdu mgmtVM --count-index 1 --run-day1 + --vdu mgmtVM --count-index 2 + + It returns the dictionary with all the params stored in ctx.params["heal_params"] + """ + logger.debug("") + # logger.debug(f"Args: {value}") + if param.name != "args": + raise ClientException(f"Unexpected param: {param.name}") + # Split the tuple "value" by "--vnf" + vnfs = iterator_split(value, ["--vnf"]) + logger.debug(f"VNFs: {vnfs}") + heal_dict = {} + heal_dict["healVnfData"] = [] + for vnf in vnfs: + # logger.debug(f"VNF: {vnf}") + heal_vnf = {} + if vnf[1].startswith("--"): + raise ClientException("Expected a VNF_ID after --vnf") + heal_vnf["vnfInstanceId"] = vnf[1] + process_common_heal_params(heal_vnf, vnf[2:]) + heal_dict["healVnfData"].append(heal_vnf) + ctx.params["heal_params"] = heal_dict + return + + +@cli_osm.command( + name="ns-heal", + short_help="heals (recreates) VNFs or VDUs of a NS instance", + context_settings=dict(ignore_unknown_options=True), +) +@click.argument("ns_name") +@click.argument( + "args", nargs=-1, type=click.UNPROCESSED, callback=process_ns_heal_params +) +@click.option("--timeout", type=int, default=None, help="timeout in seconds") +@click.option( + "--wait", + 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 ns_heal(ctx, ns_name, args, heal_params, timeout, wait): + """heals (recreates) VNFs or VDUs of a NS instance + + NS_NAME: name or ID of the NS instance + + \b + Options: + --vnf TEXT VNF instance ID or VNF id in the NS [required] + --cause TEXT human readable cause of the healing + --run-day1 indicates whether or not to run day1 primitives for the VNF/VDU + --vdu TEXT vdu-id + --count-index INTEGER count-index + + \b + Example: + osm ns-heal NS_NAME|NS_ID --vnf volumes_vnf --cause "Heal several_volumes-VM of several_volumes_vnf" + --vdu several_volumes-VM + --vnf charm_vnf --cause "Heal two VMs of native_manual_scale_charm_vnf" + --vdu mgmtVM --count-index 1 --run-day1 + --vdu mgmtVM --count-index 2 + """ + logger.debug("") + heal_dict = ctx.params["heal_params"] + logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}") + # replace VNF id in the NS by the VNF instance ID + for vnf in heal_dict["healVnfData"]: + vnf_id = vnf["vnfInstanceId"] + if not validate_uuid4(vnf_id): + vnf_filter = f"member-vnf-index-ref={vnf_id}" + vnf_list = ctx.obj.vnf.list(ns=ns_name, filter=vnf_filter) + if len(vnf_list) == 0: + raise ClientException( + f"No VNF found in NS {ns_name} with filter {vnf_filter}" + ) + elif len(vnf_list) == 1: + vnf["vnfInstanceId"] = vnf_list[0]["_id"] + else: + raise ClientException( + f"More than 1 VNF found in NS {ns_name} with filter {vnf_filter}" + ) + logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}") + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.ns.heal(ns_name, heal_dict, wait, timeout) + exit(0) + + +def process_vnf_heal_params(ctx, param, value): + """ + Processes the params in the command vnf-heal + Click does not allow advanced patterns for positional options like this: + --vdu mgmtVM --count-index 1 --run-day1 --vdu mgmtVM --count-index 2 + + It returns the dictionary with all the params stored in ctx.params["heal_params"] + """ + logger.debug("") + # logger.debug(f"Args: {value}") + if param.name != "args": + raise ClientException(f"Unexpected param: {param.name}") + # Split the tuple "value" by "--vnf" + vnf = value + heal_dict = {} + heal_dict["healVnfData"] = [] + logger.debug(f"VNF: {vnf}") + heal_vnf = {"vnfInstanceId": "id_to_be_substituted"} + process_common_heal_params(heal_vnf, vnf) + heal_dict["healVnfData"].append(heal_vnf) + ctx.params["heal_params"] = heal_dict + return + + +@cli_osm.command( + name="vnf-heal", + short_help="heals (recreates) a VNF instance or the VDUs of a VNF instance", + context_settings=dict(ignore_unknown_options=True), +) +@click.argument("vnf_name") +@click.argument( + "args", nargs=-1, type=click.UNPROCESSED, callback=process_vnf_heal_params +) +@click.option("--timeout", type=int, default=None, help="timeout in seconds") +@click.option( + "--wait", + 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_heal2(ctx, vnf_name, args, heal_params, timeout, wait): + """heals (recreates) a VNF instance or the VDUs of a VNF instance + + VNF_NAME: name or ID of the VNF instance + + \b + Options: + --cause TEXT human readable cause of the healing of the VNF + --run-day1 indicates whether or not to run day1 primitives for the VNF/VDU + --vdu TEXT vdu-id + --count-index INTEGER count-index + + \b + Example: + osm vnf-heal VNF_INSTANCE_ID --vdu mgmtVM --count-index 1 --run-day1 + --vdu mgmtVM --count-index 2 + """ + logger.debug("") + heal_dict = ctx.params["heal_params"] + heal_dict["healVnfData"][-1]["vnfInstanceId"] = vnf_name + logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}") + check_client_version(ctx.obj, ctx.command.name) + vnfr = ctx.obj.vnf.get(vnf_name) + ns_id = vnfr["nsr-id-ref"] + ctx.obj.ns.heal(ns_id, heal_dict, wait, timeout) + exit(0) + + @cli_osm.command(name="alarm-show", short_help="show alarm details") @click.argument("uuid") @click.pass_context @@ -5511,8 +6093,7 @@ def alarm_show(ctx, uuid): table = PrettyTable(["key", "attribute"]) try: # Arrange and return the response data - resp = resp.replace("ObjectId", "") - alarm = eval(resp) + alarm = resp.replace("ObjectId", "") for key in alarm_filter: if key == "uuid": value = alarm.get(key) @@ -5554,7 +6135,6 @@ def alarm_list(ctx, ns_id): if resp: # return the response data in a table resp = resp.replace("ObjectId", "") - resp = eval(resp) for alarm in resp: table.add_row( [ @@ -5772,6 +6352,12 @@ def role_show(ctx, name): @click.option( "--netslice-vlds", default=1, help="(NST) Number of netslice vlds. Default 1" ) +@click.option( + "--old", + default=False, + is_flag=True, + help="Flag to create a descriptor using the previous OSM format (pre SOL006, OSM<9)", +) @click.pass_context def package_create( ctx, @@ -5789,6 +6375,7 @@ def package_create( detailed, netslice_subnets, netslice_vlds, + old, ): """ Creates an OSM NS, VNF, NST package @@ -5821,6 +6408,7 @@ def package_create( detailed=detailed, netslice_subnets=netslice_subnets, netslice_vlds=netslice_vlds, + old=old, ) print(resp) # except ClientException as inst: @@ -5985,14 +6573,14 @@ def descriptor_translate(ctx, descriptor_file): def cli(): try: - cli_osm() + cli_osm() # pylint: disable=no-value-for-parameter exit(0) except pycurl.error as exc: print(exc) print( 'Maybe "--hostname" option or OSM_HOSTNAME environment variable needs to be specified' ) - except ClientException as exc: + except (ClientException, NotFound) as exc: print("ERROR: {}".format(exc)) except (FileNotFoundError, PermissionError) as exc: print("Cannot open file: {}".format(exc))