Feature 10916: Remove VNF Instance from NS - NS Update
[osm/NBI.git] / osm_nbi / instance_topics.py
index 39d9834..2df6a7d 100644 (file)
@@ -26,7 +26,9 @@ from osm_nbi.validation import (
     ns_terminate,
     ns_action,
     ns_scale,
+    ns_update,
     nsi_instantiate,
+    ns_migrate,
 )
 from osm_nbi.base_topic import (
     BaseTopic,
@@ -348,6 +350,9 @@ class NsrTopic(BaseTopic):
                 )
                 if vnfd_id not in needed_vnfds:
                     vnfd = self._get_vnfd_from_db(vnfd_id, session)
+                    if "revision" in vnfd["_admin"]:
+                        vnfd["revision"] = vnfd["_admin"]["revision"]
+                    vnfd.pop("_admin")
                     needed_vnfds[vnfd_id] = vnfd
                     nsr_descriptor["vnfd-id"].append(vnfd["_id"])
                 else:
@@ -397,7 +402,6 @@ class NsrTopic(BaseTopic):
         _filter = self._get_project_filter(session)
         _filter["id"] = vnfd_id
         vnfd = self.db.get_one("vnfds", _filter, fail_on_empty=True, fail_on_more=True)
-        vnfd.pop("_admin")
         return vnfd
 
     def _add_nsr_to_db(self, nsr_descriptor, rollback, session):
@@ -507,6 +511,7 @@ class NsrTopic(BaseTopic):
                         )
 
                 vnfd = self._get_vnfd_from_db(vnf_profile.get("vnfd-id"), session)
+                vnfd.pop("_admin")
 
                 for vdu in vnfd.get("vdu", ()):
                     flavor_data = {}
@@ -594,13 +599,26 @@ class NsrTopic(BaseTopic):
 
                 # Add Affinity or Anti-affinity group information to NSR
                 vdu_profiles = vnfd.get("df", [[]])[0].get("vdu-profile", ())
-                ag_prefix_name = "{}-{}".format(nsr_descriptor["name"][:16], vnf_profile.get("id")[:16])
+                affinity_group_prefix_name = "{}-{}".format(
+                    nsr_descriptor["name"][:16], vnf_profile.get("id")[:16]
+                )
 
                 for vdu_profile in vdu_profiles:
-                    ag_data = {}
-                    for ag in vdu_profile.get("affinity-or-anti-affinity-group", ()):
-                        ag_data = self._get_affinity_or_anti_affinity_group_data_from_vnfd(vnfd, ag["id"])
-                        self._add_affinity_or_anti_affinity_group_to_nsr(nsr_descriptor, ag_data, ag_prefix_name)
+                    affinity_group_data = {}
+                    for affinity_group in vdu_profile.get(
+                        "affinity-or-anti-affinity-group", ()
+                    ):
+                        affinity_group_data = (
+                            self._get_affinity_or_anti_affinity_group_data_from_vnfd(
+                                vnfd, affinity_group["id"]
+                            )
+                        )
+                        affinity_group_data["member-vnf-index"] = vnf_profile.get("id")
+                        self._add_affinity_or_anti_affinity_group_to_nsr(
+                            nsr_descriptor,
+                            affinity_group_data,
+                            affinity_group_prefix_name,
+                        )
 
             for vld in nsr_vld:
                 vld["vnfd-connection-point-ref"] = all_vld_connection_point_data.get(
@@ -611,38 +629,50 @@ class NsrTopic(BaseTopic):
 
         return nsr_descriptor
 
-    def _get_affinity_or_anti_affinity_group_data_from_vnfd(self, vnfd, ag_id):
+    def _get_affinity_or_anti_affinity_group_data_from_vnfd(
+        self, vnfd, affinity_group_id
+    ):
         """
         Gets affinity-or-anti-affinity-group info from df and returns the desired affinity group
         """
-        affinity_or_anti_affinity_group = utils.find_in_list(
-            vnfd.get("df", [[]])[0].get("affinity-or-anti-affinity-group", ()), lambda ag: ag["id"] == ag_id
+        affinity_group = utils.find_in_list(
+            vnfd.get("df", [[]])[0].get("affinity-or-anti-affinity-group", ()),
+            lambda ag: ag["id"] == affinity_group_id,
         )
-        ag_data = {}
-        if affinity_or_anti_affinity_group and affinity_or_anti_affinity_group.get("id"):
-            ag_data["ag-id"] = affinity_or_anti_affinity_group["id"]
-        if affinity_or_anti_affinity_group and affinity_or_anti_affinity_group.get("type"):
-            ag_data["type"] = affinity_or_anti_affinity_group["type"]
-        if affinity_or_anti_affinity_group and affinity_or_anti_affinity_group.get("scope"):
-            ag_data["scope"] = affinity_or_anti_affinity_group["scope"]
-        return ag_data
-
-    def _add_affinity_or_anti_affinity_group_to_nsr(self, nsr_descriptor, ag_data, ag_prefix_name):
+        affinity_group_data = {}
+        if affinity_group:
+            if affinity_group.get("id"):
+                affinity_group_data["ag-id"] = affinity_group["id"]
+            if affinity_group.get("type"):
+                affinity_group_data["type"] = affinity_group["type"]
+            if affinity_group.get("scope"):
+                affinity_group_data["scope"] = affinity_group["scope"]
+        return affinity_group_data
+
+    def _add_affinity_or_anti_affinity_group_to_nsr(
+        self, nsr_descriptor, affinity_group_data, affinity_group_prefix_name
+    ):
         """
         Adds affinity-or-anti-affinity-group to nsr checking first it is not already added
         """
-        ag = next(
+        affinity_group = next(
             (
                 f
                 for f in nsr_descriptor["affinity-or-anti-affinity-group"]
-                if all(f.get(k) == ag_data[k] for k in ag_data)
+                if all(f.get(k) == affinity_group_data[k] for k in affinity_group_data)
             ),
             None,
         )
-        if not ag:
-            ag_data["id"] = str(len(nsr_descriptor["affinity-or-anti-affinity-group"]))
-            ag_data["name"] = "{}-{}-{}".format(ag_prefix_name, ag_data["ag-id"][:32], ag_data.get("id") or 0)
-            nsr_descriptor["affinity-or-anti-affinity-group"].append(ag_data)
+        if not affinity_group:
+            affinity_group_data["id"] = str(
+                len(nsr_descriptor["affinity-or-anti-affinity-group"])
+            )
+            affinity_group_data["name"] = "{}-{}".format(
+                affinity_group_prefix_name, affinity_group_data["ag-id"][:32]
+            )
+            nsr_descriptor["affinity-or-anti-affinity-group"].append(
+                affinity_group_data
+            )
 
     def _get_image_data_from_vnfd(self, vnfd, sw_image_id):
         sw_image_desc = utils.find_in_list(
@@ -706,6 +736,13 @@ class NsrTopic(BaseTopic):
             "connection-point": [],
             "ip-address": None,  # mgmt-interface filled by LCM
         }
+
+        # Revision backwards compatility.  Only specify the revision in the record if
+        # the original VNFD has a revision.
+        if "revision" in vnfd:
+            vnfr_descriptor["revision"] = vnfd["revision"]
+
+
         vnf_k8s_namespace = ns_k8s_namespace
         if vnf_params:
             if vnf_params.get("k8s-namespace"):
@@ -987,28 +1024,46 @@ class NsrTopic(BaseTopic):
 
             # Adding Affinity groups information to vdur
             try:
-                ags_vdu_profile = utils.find_in_list(
+                vdu_profile_affinity_group = utils.find_in_list(
                     vnfd.get("df")[0]["vdu-profile"],
                     lambda a_vdu: a_vdu["id"] == vdu["id"],
                 )
             except Exception:
-                ags_vdu_profile = None
-
-            if ags_vdu_profile:
-                ags_ids = []
-                for ag in ags_vdu_profile.get("affinity-or-anti-affinity-group", ()):
-                    vdu_ag = utils.find_in_list(
-                        ags_vdu_profile.get("affinity-or-anti-affinity-group", ()),
-                        lambda ag_fp: ag_fp["id"] == ag["id"],
+                vdu_profile_affinity_group = None
+
+            if vdu_profile_affinity_group:
+                affinity_group_ids = []
+                for affinity_group in vdu_profile_affinity_group.get(
+                    "affinity-or-anti-affinity-group", ()
+                ):
+                    vdu_affinity_group = utils.find_in_list(
+                        vdu_profile_affinity_group.get(
+                            "affinity-or-anti-affinity-group", ()
+                        ),
+                        lambda ag_fp: ag_fp["id"] == affinity_group["id"],
                     )
-                    nsr_ags_data = utils.find_in_list(
+                    nsr_affinity_group = utils.find_in_list(
                         nsr_descriptor["affinity-or-anti-affinity-group"],
                         lambda nsr_ag: (
-                            nsr_ag.get("ag-id") == vdu_ag.get("id")
+                            nsr_ag.get("ag-id") == vdu_affinity_group.get("id")
+                            and nsr_ag.get("member-vnf-index")
+                            == vnfr_descriptor.get("member-vnf-index-ref")
                         ),
                     )
-                    ags_ids.append(nsr_ags_data["id"])
-                vdur["affinity-or-anti-affinity-group-id"] = ags_ids
+                    # Update Affinity Group VIM name if VDU instantiation parameter is present
+                    if vnf_params and vnf_params.get("affinity-or-anti-affinity-group"):
+                        vnf_params_affinity_group = utils.find_in_list(
+                            vnf_params["affinity-or-anti-affinity-group"],
+                            lambda vnfp_ag: (
+                                vnfp_ag.get("id") == vdu_affinity_group.get("id")
+                            ),
+                        )
+                        if vnf_params_affinity_group.get("vim-affinity-group-id"):
+                            nsr_affinity_group[
+                                "vim-affinity-group-id"
+                            ] = vnf_params_affinity_group["vim-affinity-group-id"]
+                    affinity_group_ids.append(nsr_affinity_group["id"])
+                vdur["affinity-or-anti-affinity-group-id"] = affinity_group_ids
 
             if vdu_instantiation_level:
                 count = vdu_instantiation_level.get("number-of-instances")
@@ -1101,8 +1156,10 @@ class NsLcmOpTopic(BaseTopic):
     operation_schema = {  # mapping between operation and jsonschema to validate
         "instantiate": ns_instantiate,
         "action": ns_action,
+        "update": ns_update,
         "scale": ns_scale,
         "terminate": ns_terminate,
+        "migrate": ns_migrate,
     }
 
     def __init__(self, db, fs, msg, auth):
@@ -1112,7 +1169,7 @@ class NsLcmOpTopic(BaseTopic):
         """
         Check that user has enter right parameters for the operation
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
-        :param operation: it can be: instantiate, terminate, action, TODO: update, heal
+        :param operation: it can be: instantiate, terminate, action, update. TODO: heal
         :param indata: descriptor with the parameters of the operation
         :return: None
         """
@@ -1120,6 +1177,8 @@ class NsLcmOpTopic(BaseTopic):
             self._check_action_ns_operation(indata, nsr)
         elif operation == "scale":
             self._check_scale_ns_operation(indata, nsr)
+        elif operation == "update":
+            self._check_update_ns_operation(indata, nsr)
         elif operation == "instantiate":
             self._check_instantiate_ns_operation(indata, nsr, session)
 
@@ -1212,6 +1271,96 @@ class NsLcmOpTopic(BaseTopic):
                 )
             )
 
+    def _check_update_ns_operation(self, indata, nsr) -> None:
+        """Validates the ns-update request according to updateType
+
+        If updateType is CHANGE_VNFPKG:
+        - it checks the vnfInstanceId, whether it's available under ns instance
+        - it checks the vnfdId whether it matches with the vnfd-id in the vnf-record of specified VNF.
+        Otherwise exception will be raised.
+        If updateType is REMOVE_VNF:
+        - it checks if the vnfInstanceId is available in the ns instance
+        - Otherwise exception will be raised.
+
+        Args:
+            indata: includes updateType such as CHANGE_VNFPKG,
+            nsr: network service record
+
+        Raises:
+           EngineException:
+                a meaningful error if given update parameters are not proper such as
+                "Error in validating ns-update request: <ID> does not match
+                with the vnfd-id of vnfinstance
+                http_code=HTTPStatus.UNPROCESSABLE_ENTITY"
+
+        """
+        try:
+            if indata["updateType"] == "CHANGE_VNFPKG":
+                # vnfInstanceId, nsInstanceId, vnfdId are mandatory
+                vnf_instance_id = indata["changeVnfPackageData"]["vnfInstanceId"]
+                ns_instance_id = indata["nsInstanceId"]
+                vnfd_id_2update = indata["changeVnfPackageData"]["vnfdId"]
+
+                if vnf_instance_id not in nsr["constituent-vnfr-ref"]:
+
+                    raise EngineException(
+                        f"Error in validating ns-update request: vnf {vnf_instance_id} does not "
+                        f"belong to NS {ns_instance_id}",
+                        http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+                    )
+
+                # Getting vnfrs through the ns_instance_id
+                vnfrs = self.db.get_list("vnfrs", {"nsr-id-ref": ns_instance_id})
+                constituent_vnfd_id = next(
+                    (
+                        vnfr["vnfd-id"]
+                        for vnfr in vnfrs
+                        if vnfr["id"] == vnf_instance_id
+                    ),
+                    None,
+                )
+
+                # Check the given vnfd-id belongs to given vnf instance
+                if constituent_vnfd_id and (vnfd_id_2update != constituent_vnfd_id):
+
+                    raise EngineException(
+                        f"Error in validating ns-update request: vnfd-id {vnfd_id_2update} does not "
+                        f"match with the vnfd-id: {constituent_vnfd_id} of VNF instance: {vnf_instance_id}",
+                        http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+                    )
+
+                # Validating the ns update timeout
+                if (
+                    indata.get("timeout_ns_update")
+                    and indata["timeout_ns_update"] < 300
+                ):
+                    raise EngineException(
+                        "Error in validating ns-update request: {} second is not enough "
+                        "to upgrade the VNF instance: {}".format(
+                            indata["timeout_ns_update"], vnf_instance_id
+                        ),
+                        http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+                    )
+            elif indata["updateType"] == "REMOVE_VNF":
+                vnf_instance_id = indata["removeVnfInstanceId"]
+                ns_instance_id = indata["nsInstanceId"]
+                if vnf_instance_id not in nsr["constituent-vnfr-ref"]:
+                    raise EngineException(
+                        "Invalid VNF Instance Id. '{}' is not "
+                        "present in the NS '{}'".format(vnf_instance_id, ns_instance_id)
+                    )
+
+        except (
+            DbException,
+            AttributeError,
+            IndexError,
+            KeyError,
+            ValueError,
+        ) as e:
+            raise type(e)(
+                "Ns update request could not be processed with error: {}.".format(e)
+            )
+
     def _check_scale_ns_operation(self, indata, nsr):
         vnfd = self._get_vnfd_from_vnf_member_index(
             indata["scaleVnfData"]["scaleByStepData"]["member-vnf-index"], nsr["_id"]
@@ -1282,7 +1431,14 @@ class NsLcmOpTopic(BaseTopic):
                 "Invalid parameter member_vnf_index='{}' is not one of the "
                 "nsd:constituent-vnfd".format(member_vnf_index)
             )
-        vnfd = self.db.get_one("vnfds", {"_id": vnfr["vnfd-id"]}, fail_on_empty=False)
+
+        ## Backwards compatibility: if there is no revision, get it from the one and only VNFD entry
+        if "revision" in vnfr:
+            vnfd_revision = vnfr["vnfd-id"] + ":" + str(vnfr["revision"])
+            vnfd = self.db.get_one("vnfds_revisions", {"_id": vnfd_revision}, fail_on_empty=False)
+        else:
+            vnfd = self.db.get_one("vnfds", {"_id": vnfr["vnfd-id"]}, fail_on_empty=False)
+
         if not vnfd:
             raise EngineException(
                 "vnfd id={} has been deleted!. Operation cannot be performed".format(
@@ -1941,7 +2097,7 @@ class NsLcmOpTopic(BaseTopic):
         """
         Creates a ns-lcm-opp content to be stored at database.
         :param nsr_id: internal id of the instance
-        :param operation: instantiate, terminate, scale, action, ...
+        :param operation: instantiate, terminate, scale, action, update ...
         :param params: user parameters for the operation
         :return: dictionary following SOL005 format
         """
@@ -1997,7 +2153,7 @@ class NsLcmOpTopic(BaseTopic):
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
         :param indata: descriptor with the parameters of the operation. It must contains among others
             nsInstanceId: _id of the nsr to perform the operation
-            operation: it can be: instantiate, terminate, action, TODO: update, heal
+            operation: it can be: instantiate, terminate, action, update TODO: heal
         :param kwargs: used to override the indata descriptor
         :param headers: http request headers
         :return: id of the nslcmops