Support of several VNF and VDU in ns-heal and vnf-heal commands 20/12220/1
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Sat, 18 Jun 2022 22:49:47 +0000 (00:49 +0200)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Sat, 18 Jun 2022 22:52:28 +0000 (00:52 +0200)
Click does not allow advanced patterns for positional options.
This makes impossible to request the healing of several VNF or VDU
with different options like --count-index or --run-day1.

This change introduces a processing of the args used in ns-heal and
vnf-heal commands to allow those patterns.

In addition, the change introduces the logic to use either the VNF
instance ID or the identifier of a VNF inside a NS, known as
"member-vnf-index-ref".

Change-Id: I8b2f18aff6146ce579da33e67054f4a0f98c706a
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
osmclient/scripts/osm.py

index aedf3c7..2d9a231 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
@@ -5635,116 +5636,130 @@ 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,
+@click.argument(
+    'args',
+    nargs=-1,
+    type=click.UNPROCESSED,
     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",
@@ -5754,67 +5769,95 @@ def process_vnf_heal_params(ctx, param, value):
 )
 @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,
+    args,
     heal_params,
-    cause,
-    vnf,
-    run_day1,
-    vdu,
-    count_index,
-    wait,
     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,
+@click.argument(
+    'args',
+    nargs=-1,
+    type=click.UNPROCESSED,
     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",
@@ -5829,28 +5872,39 @@ 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(
+def vnf_heal2(
     ctx,
     vnf_name,
+    args,
     heal_params,
-    cause,
-    run_day1,
-    vdu,
-    count_index,
-    wait,
     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")