Fix bug 2153 to hide WIM password properly in command wim-show
[osm/osmclient.git] / osmclient / scripts / osm.py
index df638a0..02870e4 100755 (executable)
@@ -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
 ####################
@@ -3095,18 +3083,19 @@ def pdu_delete(ctx, name, force):
 
 
 @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 +3117,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 +3134,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 +3152,26 @@ 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)
+    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 +3185,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 +3211,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 +3229,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 +3260,20 @@ 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)
+    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 +3350,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 +3384,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 +3396,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 +3614,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 +3862,24 @@ 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 +3907,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 +3934,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 +4069,7 @@ def k8scluster_list(ctx, filter, literal, long):
                 "Version",
                 "VIM",
                 "K8s-nets",
+                "Deployment methods",
                 "Operational State",
                 "Op. state (details)",
                 "Description",
@@ -4028,6 +4107,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),
@@ -4799,6 +4879,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 +4895,8 @@ def user_update(
     remove_project,
     add_project_role,
     remove_project_role,
+    change_password,
+    new_password,
 ):
     """Update a user information
 
@@ -4820,6 +4908,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 +4919,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 +5582,323 @@ 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 +5923,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 +5965,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 +6182,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 +6205,7 @@ def package_create(
     detailed,
     netslice_subnets,
     netslice_vlds,
+    old,
 ):
     """
     Creates an OSM NS, VNF, NST package
@@ -5821,6 +6238,7 @@ def package_create(
         detailed=detailed,
         netslice_subnets=netslice_subnets,
         netslice_vlds=netslice_vlds,
+        old=old,
     )
     print(resp)
     # except ClientException as inst:
@@ -5985,7 +6403,7 @@ 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)