Addition of PaaS
[osm/osmclient.git] / osmclient / scripts / osm.py
index aedf3c7..de121b1 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
@@ -95,14 +96,14 @@ def get_vim_name(vim_list, vim_id):
 
 
 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:
@@ -123,11 +124,6 @@ def create_config(config_file, json_string):
     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,
@@ -174,26 +170,6 @@ def create_config(config_file, json_string):
     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
@@ -208,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")
 
@@ -1056,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"]:
@@ -2362,7 +2320,7 @@ def ns_create(
     config,
     config_file,
     wait,
-    timeout
+    timeout,
 ):
     """creates a new NS instance"""
     logger.debug("")
@@ -3124,14 +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", 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("--tenant", "--project", "tenant", default=None, help="VIM tenant/project name")
+@click.option(
+    "--tenant", "--project", "tenant", default=None, help="VIM tenant/project 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(
+    "--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(
@@ -3153,8 +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.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,
@@ -3172,7 +3156,7 @@ def vim_create(
     wait,
     vca,
     creds,
-    prometheus_config_file
+    prometheus_config_file,
 ):
     """creates a new VIM account"""
     logger.debug("")
@@ -3196,10 +3180,13 @@ def vim_create(
     if vca:
         vim["vca"] = vca
     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)
+    ctx.obj.vim.create(
+        name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait
+    )
     # except ClientException as e:
     #     print(str(e))
     #     exit(1)
@@ -3213,7 +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(
+    "--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(
@@ -3235,8 +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.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,
@@ -3254,7 +3251,7 @@ def vim_update(
     sdn_port_mapping,
     wait,
     creds,
-    prometheus_config_file
+    prometheus_config_file,
 ):
     """updates a VIM account
 
@@ -3281,6 +3278,7 @@ def vim_update(
     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())
@@ -3289,7 +3287,9 @@ def vim_update(
             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)
+    ctx.obj.vim.update(
+        name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait
+    )
     # except ClientException as e:
     #     print(str(e))
     #     exit(1)
@@ -3630,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)
@@ -3879,16 +3879,10 @@ def sdnc_show(ctx, name):
     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",
+    "--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",
+    "--init-helm3/--skip-helm3", required=False, default=True, help="Initialize helm v3"
 )
 @click.option(
     "--init-jujubundle/--skip-jujubundle",
@@ -3923,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, init_helm2, init_helm3, init_jujubundle, 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
 
@@ -3939,9 +3945,11 @@ def k8scluster_add(
     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}
+        cluster["deployment_methods"] = {
+            "helm-chart": init_helm2,
+            "juju-bundle": init_jujubundle,
+            "helm-chart-v3": init_helm3,
+        }
     if description:
         cluster["description"] = description
     if namespace:
@@ -4194,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(
@@ -4287,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(
@@ -4426,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
 ###########################
@@ -4881,15 +5069,11 @@ 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("--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"
+    help="user's new password to update in expiry condition",
 )
 @click.pass_context
 def user_update(
@@ -5624,10 +5808,7 @@ def update(ctx, ns_name, updatetype, config, timeout, wait):
     NS_NAME: Network service instance name or ID.
 
     """
-    op_data = {
-        "timeout": timeout,
-        "updateType": updatetype,
-    }
+    op_data = {"timeout": timeout, "updateType": updatetype}
     if config:
         op_data["config"] = yaml.safe_load(config)
 
@@ -5635,193 +5816,224 @@ def update(ctx, ns_name, updatetype, config, timeout, wait):
     ctx.obj.ns.update(ns_name, op_data, wait=wait)
 
 
-def process_common_heal_params(ctx, param, value):
+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 not value:
-        return
-    logger.debug(f"Param name: {param.name}")
-    if not ctx.params.get("heal_params", {}).get("healVnfData"):
-        raise ClientException(f"Expected option --vnf before {param.name}")
-    if param.name == "cause":
-        param_dict = ctx.params["heal_params"]["healVnfData"][-1]["cause"] = value
-        return
-    # If not "vnf" and not "cause", then the param lies on "additionalParams"
-    if not ctx.params["heal_params"]["healVnfData"][-1].get("additionalParams"):
-        ctx.params["heal_params"]["healVnfData"][-1]["additionalParams"] = {}
-    if param.name == "vdu":
-        # Check VDU id ?
-        if not ctx.params["heal_params"]["healVnfData"][-1]["additionalParams"].get(
-            "vdu"
-        ):
-            ctx.params["heal_params"]["healVnfData"][-1]["additionalParams"][
-                "vdu"
-            ] = []
-        vdu = {"vdu-id": value}
-        ctx.params["heal_params"]["healVnfData"][-1]["additionalParams"][
-            "vdu"
-        ].append(vdu)
-        ctx.params["heal_params"]["current_item"] = "vdu"
+    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:
-        current_item = ctx.params["heal_params"]["current_item"]
-        if current_item == "vnf":
-            param_dict = ctx.params["heal_params"]["healVnfData"][-1][
-                "additionalParams"
-            ]
-        else:
-            # if current_item == "vdu":
-            param_dict = ctx.params["heal_params"]["healVnfData"][-1][
-                "additionalParams"
-            ]["vdu"][-1]
-        if param.name == "count_index":
-            param_name = "count-index"
-        elif param.name == "run_day1":
-            param_name = "run-day1"
-        else:
-            param_name = param.name
-        param_dict[param_name] = value
+        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("")
-    if not ctx.params.get("heal_params"):
-        ctx.params["heal_params"] = {}
-        ctx.params["heal_params"]["healVnfData"] = []
-    if param.name == "vnf" and value:
-        # Check VNF id ?
-        logger.debug(f"Param name: {param.name}")
-        vnf = {"vnfInstanceId": value}
-        ctx.params["heal_params"]["healVnfData"].append(vnf)
-        ctx.params["heal_params"]["current_item"] = "vnf"
-    else:
-        process_common_heal_params(ctx, param, value)
-
-
-def process_vnf_heal_params(ctx, param, value):
-    logger.debug("")
-    if not ctx.params.get("heal_params"):
-        ctx.params["heal_params"] = {}
-        ctx.params["heal_params"]["healVnfData"] = []
-        vnf = {"vnfInstanceId": "id_to_be_substituted"}
-        ctx.params["heal_params"]["healVnfData"].append(vnf)
-        ctx.params["heal_params"]["current_item"] = "vnf"
-    else:
-        process_common_heal_params(ctx, param, value)
+    # 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"
+    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.option(
-    "--vnf",
-    required=True,
-    default=None,
-    callback=process_ns_heal_params,
-    help="vnf-id if the target is a vnf instead of a ns)",
-)
-@click.option(
-    "--cause",
-    default=None,
-    callback=process_ns_heal_params,
-    help="human readable cause of the healing",
-)
-@click.option(
-    "--run-day1",
-    is_flag=True,
-    default=False,
-    callback=process_ns_heal_params,
-    help="indicates whether or not to run day1 primitives for the VNF/VDU",
-)
-@click.option(
-    "--vdu",
-    default=None,
-    callback=process_ns_heal_params,
-    help="vdu-id",
-)
-@click.option(
-    "--count-index",
-    type=int,
-    default=None,
-    callback=process_ns_heal_params,
-    help="count-index",
-)
-@click.option(
-    "--timeout",
-    type=int,
-    default=None,
-    help="timeout in seconds"
+@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",
-    is_flag=True,
     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,
-    heal_params,
-    cause,
-    vnf,
-    run_day1,
-    vdu,
-    count_index,
-    wait,
-    timeout,
-):
+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"]
-    heal_dict.pop("current_item")
-    if cause:
-        heal_dict["cause"] = cause
-    logger.debug(f"Heal dict: {heal_dict}")
+    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.option(
-    "--cause",
-    default=None,
-    callback=process_vnf_heal_params,
-    help="human readable cause of the healing",
-)
-@click.option(
-    "--run-day1",
-    is_flag=True,
-    default=False,
-    callback=process_vnf_heal_params,
-    help="indicates whether or not to run day1 primitives for the VNF/VDU",
-)
-@click.option(
-    "--vdu",
-    default=None,
-    callback=process_vnf_heal_params,
-    help="vdu-id",
-)
-@click.option(
-    "--count-index",
-    type=int,
-    default=None,
-    callback=process_vnf_heal_params,
-    help="count-index",
-)
-@click.option(
-    "--timeout",
-    type=int,
-    default=None,
-    help="timeout in seconds"
+@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,
@@ -5829,28 +6041,32 @@ def ns_heal(
     help="do not return the control immediately, but keep it until the operation is completed, or timeout",
 )
 @click.pass_context
-def vnf_heal(
-    ctx,
-    vnf_name,
-    heal_params,
-    cause,
-    run_day1,
-    vdu,
-    count_index,
-    wait,
-    timeout,
-):
+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.pop("current_item")
     heal_dict["healVnfData"][-1]["vnfInstanceId"] = vnf_name
-    logger.debug(f"Heal dict: {heal_dict}")
+    logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}")
     check_client_version(ctx.obj, ctx.command.name)
-    ctx.obj.vnf.heal(vnf_name, heal_dict, wait, timeout)
+    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")
@@ -6357,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))