Feature 10908: LCM process NS update request 78/11878/17
authoraticig <gulsum.atici@canonical.com>
Tue, 12 Apr 2022 12:27:53 +0000 (15:27 +0300)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Wed, 18 May 2022 13:48:54 +0000 (15:48 +0200)
This feature updates the running VNF instance in a network service.

Change-Id: I5531cdf1b977aff3508da5f105accff34d6817e0
Signed-off-by: aticig <gulsum.atici@canonical.com>
osm_lcm/data_utils/vnfd.py
osm_lcm/lcm.py
osm_lcm/lcm_utils.py
osm_lcm/ns.py
osm_lcm/tests/test_db_descriptors.py
osm_lcm/tests/test_ns.py
requirements.in
requirements.txt

index f8c11ca..ffcb582 100644 (file)
@@ -169,3 +169,25 @@ def get_juju_ee_ref(vnfd, entity_id):
         get_configuration(vnfd, entity_id).get("execution-environment-list", []),
         lambda ee: "juju" in ee,
     )
+
+
+def find_software_version(vnfd: dict) -> str:
+    """Find the sotware version in the VNFD descriptors
+
+    Args:
+        vnfd (dict): Descriptor as a dictionary
+
+    Returns:
+        software-version (str)
+    """
+
+    default_sw_version = "1.0"
+
+    if vnfd.get("vnfd"):
+        vnfd = vnfd["vnfd"]
+
+    if vnfd.get("software-version"):
+        return vnfd["software-version"]
+
+    else:
+        return default_sw_version
index f8b204a..326847c 100644 (file)
@@ -439,6 +439,14 @@ class Lcm:
                 task = asyncio.ensure_future(self.ns.action(nsr_id, nslcmop_id))
                 self.lcm_tasks.register("ns", nsr_id, nslcmop_id, "ns_action", task)
                 return
+            elif command == "update":
+                # self.logger.debug("Update NS {}".format(nsr_id))
+                nslcmop = params
+                nslcmop_id = nslcmop["_id"]
+                nsr_id = nslcmop["nsInstanceId"]
+                task = asyncio.ensure_future(self.ns.update(nsr_id, nslcmop_id))
+                self.lcm_tasks.register("ns", nsr_id, nslcmop_id, "ns_update", task)
+                return
             elif command == "scale":
                 # self.logger.debug("Update NS {}".format(nsr_id))
                 nslcmop = params
@@ -474,6 +482,7 @@ class Lcm:
                 "instantiated",
                 "scaled",
                 "actioned",
+                "updated",
             ):  # "scaled-cooldown-time"
                 return
         elif topic == "nsi":  # netslice LCM processes (instantiate, terminate, etc)
index 16d5b33..235d485 100644 (file)
@@ -17,7 +17,9 @@
 ##
 
 import asyncio
+import checksumdir
 from collections import OrderedDict
+import os
 from time import time
 from osm_lcm.data_utils.database.database import Database
 from osm_lcm.data_utils.filesystem.filesystem import Filesystem
@@ -80,6 +82,63 @@ def get_iterable(in_dict, in_key):
     return in_dict[in_key]
 
 
+def check_juju_bundle_existence(vnfd: dict) -> str:
+    """Checks the existence of juju-bundle in the descriptor
+
+    Args:
+        vnfd:   Descriptor as a dictionary
+
+    Returns:
+        Juju bundle if dictionary has juju-bundle else None
+
+    """
+    if vnfd.get("vnfd"):
+        vnfd = vnfd["vnfd"]
+
+    for kdu in vnfd.get("kdu", []):
+        return kdu.get("juju-bundle", None)
+
+
+def get_charm_artifact_path(base_folder, charm_name, charm_type, revision=str()) -> str:
+    """Finds the charm artifact paths
+
+    Args:
+        base_folder:    Main folder which will be looked up for charm
+        charm_name:   Charm name
+        charm_type:   Type of charm native_charm, lxc_proxy_charm or k8s_proxy_charm
+        revision:   vnf package revision number if there is
+
+    Returns:
+        artifact_path: (str)
+
+    """
+    extension = ""
+    if revision:
+        extension = ":" + str(revision)
+
+    if base_folder.get("pkg-dir"):
+        artifact_path = "{}/{}/{}/{}".format(
+            base_folder["folder"] + extension,
+            base_folder["pkg-dir"],
+            "charms"
+            if charm_type in ("native_charm", "lxc_proxy_charm", "k8s_proxy_charm")
+            else "helm-charts",
+            charm_name,
+        )
+
+    else:
+        # For SOL004 packages
+        artifact_path = "{}/Scripts/{}/{}".format(
+            base_folder["folder"] + extension,
+            "charms"
+            if charm_type in ("native_charm", "lxc_proxy_charm", "k8s_proxy_charm")
+            else "helm-charts",
+            charm_name,
+        )
+
+    return artifact_path
+
+
 def populate_dict(target_dict, key_list, value):
     """
     Update target_dict creating nested dictionaries with the key_list. Last key_list item is asigned the value.
@@ -124,6 +183,40 @@ class LcmBase:
         # except DbException as e:
         #     self.logger.error("Updating {} _id={} with '{}'. Error: {}".format(item, _id, _desc, e))
 
+    def check_charm_hash_changed(
+        self, current_charm_path: str, target_charm_path: str
+    ) -> bool:
+        """Find the target charm has changed or not by checking the hash of
+        old and new charm packages
+
+        Args:
+            current_charm_path (str): Existing charm package artifact path
+            target_charm_path  (str): Target charm package artifact path
+
+        Returns:
+            True/False (bool): if charm has changed it returns True
+
+        """
+        # Check if the charm artifacts are available
+        if os.path.exists(self.fs.path + current_charm_path) and os.path.exists(
+            self.fs.path + target_charm_path
+        ):
+            # Compare the hash of charm folders
+            if checksumdir.dirhash(
+                self.fs.path + current_charm_path
+            ) != checksumdir.dirhash(self.fs.path + target_charm_path):
+
+                return True
+
+            return False
+
+        else:
+            raise LcmException(
+                "Charm artifact {} does not exist in the VNF Package".format(
+                    self.fs.path + target_charm_path
+                )
+            )
+
 
 class TaskRegistry(LcmBase):
     """
index 7bd6a92..af0a1eb 100644 (file)
@@ -17,6 +17,7 @@
 ##
 
 import asyncio
+import shutil
 from typing import Any, Dict, List
 import yaml
 import logging
@@ -55,6 +56,8 @@ from osm_lcm.lcm_utils import (
     deep_get,
     get_iterable,
     populate_dict,
+    check_juju_bundle_existence,
+    get_charm_artifact_path,
 )
 from osm_lcm.data_utils.nsd import (
     get_ns_configuration_relation_list,
@@ -78,6 +81,7 @@ from osm_lcm.data_utils.vnfd import (
     get_number_of_instances,
     get_juju_ee_ref,
     get_kdu_resource_profile,
+    find_software_version,
 )
 from osm_lcm.data_utils.list_utils import find_in_list
 from osm_lcm.data_utils.vnfr import get_osm_params, get_vdur_index, get_kdur
@@ -118,6 +122,7 @@ class NsLcm(LcmBase):
     timeout_ns_terminate = 1800  # default global timeout for un deployment a ns
     timeout_charm_delete = 10 * 60
     timeout_primitive = 30 * 60  # timeout for primitive execution
+    timeout_ns_update = 30 * 60  # timeout for ns update
     timeout_progress_primitive = (
         10 * 60
     )  # timeout for some progress in a primitive execution
@@ -5292,6 +5297,375 @@ class NsLcm(LcmBase):
             self.lcm_tasks.remove("ns", nsr_id, nslcmop_id, "ns_action")
             return nslcmop_operation_state, detailed_status
 
+    async def _ns_charm_upgrade(
+        self,
+        ee_id,
+        charm_id,
+        charm_type,
+        path,
+        timeout: float = None,
+    ) -> (str, str):
+        """This method upgrade charms in VNF instances
+
+        Args:
+            ee_id:  Execution environment id
+            path:   Local path to the charm
+            charm_id: charm-id
+            charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm
+            timeout: (Float)    Timeout for the ns update operation
+
+        Returns:
+            result: (str, str) COMPLETED/FAILED, details
+        """
+        try:
+            charm_type = charm_type or "lxc_proxy_charm"
+            output = await self.vca_map[charm_type].upgrade_charm(
+                ee_id=ee_id,
+                path=path,
+                charm_id=charm_id,
+                charm_type=charm_type,
+                timeout=timeout or self.timeout_ns_update,
+            )
+
+            if output:
+                return "COMPLETED", output
+
+        except (LcmException, asyncio.CancelledError):
+            raise
+
+        except Exception as e:
+
+            self.logger.debug("Error upgrading charm {}".format(path))
+
+            return "FAILED", "Error upgrading charm {}: {}".format(path, e)
+
+    async def update(self, nsr_id, nslcmop_id):
+        """Update NS according to different update types
+
+        This method performs upgrade of VNF instances then updates the revision
+        number in VNF record
+
+        Args:
+            nsr_id: Network service will be updated
+            nslcmop_id: ns lcm operation id
+
+        Returns:
+             It may raise DbException, LcmException, N2VCException, K8sException
+
+        """
+        # Try to lock HA task here
+        task_is_locked_by_me = self.lcm_tasks.lock_HA("ns", "nslcmops", nslcmop_id)
+        if not task_is_locked_by_me:
+            return
+
+        logging_text = "Task ns={} update={} ".format(nsr_id, nslcmop_id)
+        self.logger.debug(logging_text + "Enter")
+
+        # Set the required variables to be filled up later
+        db_nsr = None
+        db_nslcmop_update = {}
+        vnfr_update = {}
+        nslcmop_operation_state = None
+        db_nsr_update = {}
+        error_description_nslcmop = ""
+        exc = None
+        change_type = ""
+        detailed_status = ""
+
+        try:
+            # wait for any previous tasks in process
+            step = "Waiting for previous operations to terminate"
+            await self.lcm_tasks.waitfor_related_HA("ns", "nslcmops", nslcmop_id)
+            self._write_ns_status(
+                nsr_id=nsr_id,
+                ns_state=None,
+                current_operation="UPDATING",
+                current_operation_id=nslcmop_id,
+            )
+
+            step = "Getting nslcmop from database"
+            db_nslcmop = self.db.get_one(
+                "nslcmops", {"_id": nslcmop_id}, fail_on_empty=False
+            )
+            update_type = db_nslcmop["operationParams"]["updateType"]
+
+            step = "Getting nsr from database"
+            db_nsr = self.db.get_one("nsrs", {"_id": nsr_id})
+            old_operational_status = db_nsr["operational-status"]
+            db_nsr_update["operational-status"] = "updating"
+            self.update_db_2("nsrs", nsr_id, db_nsr_update)
+            nsr_deployed = db_nsr["_admin"].get("deployed")
+
+            if update_type == "CHANGE_VNFPKG":
+
+                # Get the input parameters given through update request
+                vnf_instance_id = db_nslcmop["operationParams"][
+                    "changeVnfPackageData"
+                ].get("vnfInstanceId")
+
+                vnfd_id = db_nslcmop["operationParams"]["changeVnfPackageData"].get(
+                    "vnfdId"
+                )
+                timeout_seconds = db_nslcmop["operationParams"].get("timeout_ns_update")
+
+                step = "Getting vnfr from database"
+                db_vnfr = self.db.get_one(
+                    "vnfrs", {"_id": vnf_instance_id}, fail_on_empty=False
+                )
+
+                step = "Getting vnfds from database"
+                # Latest VNFD
+                latest_vnfd = self.db.get_one(
+                    "vnfds", {"_id": vnfd_id}, fail_on_empty=False
+                )
+                latest_vnfd_revision = latest_vnfd["_admin"].get("revision")
+
+                # Current VNFD
+                current_vnf_revision = db_vnfr.get("revision", 1)
+                current_vnfd = self.db.get_one(
+                    "vnfds_revisions",
+                    {"_id": vnfd_id + ":" + str(current_vnf_revision)},
+                    fail_on_empty=False,
+                )
+                # Charm artifact paths will be filled up later
+                (
+                    current_charm_artifact_path,
+                    target_charm_artifact_path,
+                    charm_artifact_paths,
+                ) = ([], [], [])
+
+                step = "Checking if revision has changed in VNFD"
+                if current_vnf_revision != latest_vnfd_revision:
+
+                    # There is new revision of VNFD, update operation is required
+                    current_vnfd_path = vnfd_id + ":" + str(current_vnf_revision)
+                    latest_vnfd_path = vnfd_id
+
+                    step = "Removing the VNFD packages if they exist in the local path"
+                    shutil.rmtree(self.fs.path + current_vnfd_path, ignore_errors=True)
+                    shutil.rmtree(self.fs.path + latest_vnfd_path, ignore_errors=True)
+
+                    step = "Get the VNFD packages from FSMongo"
+                    self.fs.sync(from_path=latest_vnfd_path)
+                    self.fs.sync(from_path=current_vnfd_path)
+
+                    step = (
+                        "Get the charm-type, charm-id, ee-id if there is deployed VCA"
+                    )
+                    base_folder = latest_vnfd["_admin"]["storage"]
+
+                    for charm_index, charm_deployed in enumerate(
+                        get_iterable(nsr_deployed, "VCA")
+                    ):
+                        vnf_index = db_vnfr.get("member-vnf-index-ref")
+
+                        # Getting charm-id and charm-type
+                        if charm_deployed.get("member-vnf-index") == vnf_index:
+                            charm_id = self.get_vca_id(db_vnfr, db_nsr)
+                            charm_type = charm_deployed.get("type")
+
+                            # Getting ee-id
+                            ee_id = charm_deployed.get("ee_id")
+
+                            step = "Getting descriptor config"
+                            descriptor_config = get_configuration(
+                                current_vnfd, current_vnfd["id"]
+                            )
+
+                            if "execution-environment-list" in descriptor_config:
+                                ee_list = descriptor_config.get(
+                                    "execution-environment-list", []
+                                )
+                            else:
+                                ee_list = []
+
+                            # There could be several charm used in the same VNF
+                            for ee_item in ee_list:
+                                if ee_item.get("juju"):
+
+                                    step = "Getting charm name"
+                                    charm_name = ee_item["juju"].get("charm")
+
+                                    step = "Setting Charm artifact paths"
+                                    current_charm_artifact_path.append(
+                                        get_charm_artifact_path(
+                                            base_folder,
+                                            charm_name,
+                                            charm_type,
+                                            current_vnf_revision,
+                                        )
+                                    )
+                                    target_charm_artifact_path.append(
+                                        get_charm_artifact_path(
+                                            base_folder,
+                                            charm_name,
+                                            charm_type,
+                                        )
+                                    )
+
+                            charm_artifact_paths = zip(
+                                current_charm_artifact_path, target_charm_artifact_path
+                            )
+
+                    step = "Checking if software version has changed in VNFD"
+                    if find_software_version(current_vnfd) != find_software_version(
+                        latest_vnfd
+                    ):
+
+                        step = "Checking if existing VNF has charm"
+                        for current_charm_path, target_charm_path in list(
+                            charm_artifact_paths
+                        ):
+                            if current_charm_path:
+                                raise LcmException(
+                                    "Software version change is not supported as VNF instance {} has charm.".format(
+                                        vnf_instance_id
+                                    )
+                                )
+
+                        # There is no change in the charm package, then redeploy the VNF
+                        # based on new descriptor
+                        step = "Redeploying VNF"
+                        # This part is in https://osm.etsi.org/gerrit/11943
+
+                    else:
+                        step = "Checking if any charm package has changed or not"
+                        for current_charm_path, target_charm_path in list(
+                            charm_artifact_paths
+                        ):
+                            if (
+                                current_charm_path
+                                and target_charm_path
+                                and self.check_charm_hash_changed(
+                                    current_charm_path, target_charm_path
+                                )
+                            ):
+
+                                step = "Checking whether VNF uses juju bundle"
+                                if check_juju_bundle_existence(current_vnfd):
+
+                                    raise LcmException(
+                                        "Charm upgrade is not supported for the instance which"
+                                        " uses juju-bundle: {}".format(
+                                            check_juju_bundle_existence(current_vnfd)
+                                        )
+                                    )
+
+                                step = "Upgrading Charm"
+                                (
+                                    result,
+                                    detailed_status,
+                                ) = await self._ns_charm_upgrade(
+                                    ee_id=ee_id,
+                                    charm_id=charm_id,
+                                    charm_type=charm_type,
+                                    path=self.fs.path + target_charm_path,
+                                    timeout=timeout_seconds,
+                                )
+
+                                if result == "FAILED":
+                                    nslcmop_operation_state = result
+                                    error_description_nslcmop = detailed_status
+
+                                db_nslcmop_update["detailed-status"] = detailed_status
+                                self.logger.debug(
+                                    logging_text
+                                    + " step {} Done with result {} {}".format(
+                                        step, nslcmop_operation_state, detailed_status
+                                    )
+                                )
+
+                        step = "Updating policies"
+                        # This part is in https://osm.etsi.org/gerrit/11943
+
+                    #  If nslcmop_operation_state is None, so any operation is not failed.
+                    if not nslcmop_operation_state:
+                        nslcmop_operation_state = "COMPLETED"
+
+                        # If update CHANGE_VNFPKG nslcmop_operation is successful
+                        # vnf revision need to be updated
+                        vnfr_update["revision"] = latest_vnfd_revision
+                        self.update_db_2("vnfrs", db_vnfr["_id"], vnfr_update)
+
+                    self.logger.debug(
+                        logging_text
+                        + " task Done with result {} {}".format(
+                            nslcmop_operation_state, detailed_status
+                        )
+                    )
+            elif update_type == "REMOVE_VNF":
+                # This part is included in https://osm.etsi.org/gerrit/11876
+                pass
+
+            #  If nslcmop_operation_state is None, so any operation is not failed.
+            #  All operations are executed in overall.
+            if not nslcmop_operation_state:
+                nslcmop_operation_state = "COMPLETED"
+            db_nsr_update["operational-status"] = old_operational_status
+
+        except (DbException, LcmException, N2VCException, K8sException) as e:
+            self.logger.error(logging_text + "Exit Exception {}".format(e))
+            exc = e
+        except asyncio.CancelledError:
+            self.logger.error(
+                logging_text + "Cancelled Exception while '{}'".format(step)
+            )
+            exc = "Operation was cancelled"
+        except asyncio.TimeoutError:
+            self.logger.error(logging_text + "Timeout while '{}'".format(step))
+            exc = "Timeout"
+        except Exception as e:
+            exc = traceback.format_exc()
+            self.logger.critical(
+                logging_text + "Exit Exception {} {}".format(type(e).__name__, e),
+                exc_info=True,
+            )
+        finally:
+            if exc:
+                db_nslcmop_update[
+                    "detailed-status"
+                ] = (
+                    detailed_status
+                ) = error_description_nslcmop = "FAILED {}: {}".format(step, exc)
+                nslcmop_operation_state = "FAILED"
+                db_nsr_update["operational-status"] = old_operational_status
+            if db_nsr:
+                self._write_ns_status(
+                    nsr_id=nsr_id,
+                    ns_state=db_nsr["nsState"],
+                    current_operation="IDLE",
+                    current_operation_id=None,
+                    other_update=db_nsr_update,
+                )
+
+            self._write_op_status(
+                op_id=nslcmop_id,
+                stage="",
+                error_message=error_description_nslcmop,
+                operation_state=nslcmop_operation_state,
+                other_update=db_nslcmop_update,
+            )
+
+            if nslcmop_operation_state:
+                try:
+                    await self.msg.aiowrite(
+                        "ns",
+                        "updated",
+                        {
+                            "nsr_id": nsr_id,
+                            "nslcmop_id": nslcmop_id,
+                            "operationState": nslcmop_operation_state,
+                        },
+                        loop=self.loop,
+                    )
+                except Exception as e:
+                    self.logger.error(
+                        logging_text + "kafka_write notification Exception {}".format(e)
+                    )
+            self.logger.debug(logging_text + "Exit")
+            self.lcm_tasks.remove("ns", nsr_id, nslcmop_id, "ns_update")
+            return nslcmop_operation_state, detailed_status
+
     async def scale(self, nsr_id, nslcmop_id):
         # Try to lock HA task here
         task_is_locked_by_me = self.lcm_tasks.lock_HA("ns", "nslcmops", nslcmop_id)
index 34491a7..457ea30 100644 (file)
@@ -120,6 +120,38 @@ db_nsds_text = """
 
 db_nslcmops_text = """
 ---
+-   _admin:
+        created: 1651100375.77829
+        modified: 1651100481.36625
+        projects_read:
+        - 7f563445c74147f78e29b193a6da42bb
+        projects_write:
+        - 7f563445c74147f78e29b193a6da42bb
+        worker: a5adf5972b63
+    detailed-status: success
+    _id: 6bd4362f-da74-4bd8-a825-fd00e610c644
+    id: 6bd4362f-da74-4bd8-a825-fd00e610c644
+    operationState: COMPLETED
+    queuePosition: 0
+    stage: ''
+    errorMessage: ''
+    detailedStatus:
+    statusEnteredTime: 1651100481.36625
+    nsInstanceId: 7e3ad9ce-39b8-4636-a661-7870f25bf800
+    lcmOperationType: update
+    startTime: 1651100375.77823
+    isAutomaticInvocation: false
+    operationParams:
+        updateType: CHANGE_VNFPKG
+        changeVnfPackageData:
+            vnfInstanceId: 6421c7c9-d865-4fb4-9a13-d4275d243e01
+            vnfdId: 7637bcf8-cf14-42dc-ad70-c66fcf1e6e77
+        lcmOperationType: update
+        nsInstanceId: f48163a6-c807-47bc-9682-f72caef5af85
+    isCancelPending: false
+    links:
+        self: "/osm/nslcm/v1/ns_lcm_op_occs/6bd4362f-da74-4bd8-a825-fd00e610c644"
+        nsInstance: "/osm/nslcm/v1/ns_instances/f48163a6-c807-47bc-9682-f72caef5af85"       
 -   _admin:
         created: 1566823354.4148262
         modified: 1566823354.4148262
@@ -315,6 +347,7 @@ db_nsrs_text = """
                 vdu_count_index: null
                 vdu_id: null
                 vdu_name: null
+                type: lxc_proxy_charm
                 vnfd_id: hackfest3charmed-vnf
             -   application: alf-c-ab
                 ee_id: f48163a6-c807-47bc-9682-f72caef5af85.alf-c-ab
@@ -329,6 +362,7 @@ db_nsrs_text = """
                 vdu_count_index: null
                 vdu_id: null
                 vdu_name: null
+                type: lxc_proxy_charm
                 vnfd_id: hackfest3charmed-vnf
             VCA-model-name: f48163a6-c807-47bc-9682-f72caef5af85
         modified: 1566823354.3716335
@@ -861,9 +895,10 @@ db_nsrs_text = """
     nsd-name-ref: hackfest3charmed-ns
     nsd-ref: hackfest3charmed-ns
     operational-events: []
-    operational-status: failed
+    operational-status: running
     orchestration-progress: {}
     resource-orchestrator: osmopenmano
+    nsState: INSTANTIATED
     short-name: ALF
     ssh-authorized-key: null
     flavor : [{"vcpu-count":1,"memory-mb":1024,"storage-gb":"10","vim_info":[],"name":"mgmtVM-flv","id":"0"}]
@@ -1412,6 +1447,196 @@ db_k8sclusters_text = """
     vim_account: ea958ba5-4e58-4405-bf42-6e3be15d4c3a
 """
 
+db_vnfds_revisions_text = """
+---
+-   _admin:
+      created: 1566823352.7154346
+      modified: 1566823353.9295402
+      onboardingState: ONBOARDED
+      operationalState: ENABLED
+      projects_read:
+      - 25b5aebf-3da1-49ed-99de-1d2b4a86d6e4
+      projects_write:
+      - 25b5aebf-3da1-49ed-99de-1d2b4a86d6e4
+      storage:
+          descriptor: hackfest_3charmed_vnfd/hackfest_3charmed_vnfd.yaml
+          folder: 7637bcf8-cf14-42dc-ad70-c66fcf1e6e77
+          fs: local
+          path: /app/storage/
+          pkg-dir: hackfest_3charmed_vnfd
+          zipfile: package.tar.gz
+      type: vnfd
+      usageState: NOT_IN_USE
+      userDefinedData: {}
+    _id: 7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1
+    id: hackfest3charmed-vnf
+    description: >-
+      A VNF consisting of 2 VDUs connected to an internal VL, and one VDU
+      with cloud-init
+    product-name: hackfest3charmed-vnf
+    version: '1.0'
+    mgmt-cp: vnf-mgmt-ext
+    virtual-compute-desc:
+      - id: mgmt-compute
+        virtual-cpu:
+          num-virtual-cpu: 1
+        virtual-memory:
+          size: 1
+      - id: data-compute
+        virtual-cpu:
+          num-virtual-cpu: 1
+        virtual-memory:
+          size: 1
+    kdu:
+    - juju-bundle: stable/native-kdu
+      name: native-kdu
+    virtual-storage-desc:
+      - id: mgmt-storage
+        block-storage-data:
+          size-of-storage: 10
+      - id: data-storage
+        block-storage-data:
+          size-of-storage: 10
+
+    sw-image-desc:
+      - id: hackfest3-mgmt
+        name: hackfest3-mgmt
+
+    vdu:
+      - id: mgmtVM
+        name: mgmtVM
+        cloud-init-file: cloud-config.txt
+        sw-image-desc: hackfest3-mgmt
+        virtual-compute-desc: mgmt-compute
+        virtual-storage-desc: mgmt-storage
+        int-cpd:
+          - id: vnf-mgmt
+            order: 1
+            virtual-network-interface-requirement:
+              - name: mgmtVM-eth0
+                virtual-interface:
+                  type: VIRTIO
+          - id: mgmtVM-internal
+            int-virtual-link-desc: internal
+            order: 2
+            virtual-network-interface-requirement:
+              - name: mgmtVM-eth1
+                virtual-interface:
+                  type: VIRTIO
+      - id: dataVM
+        name: dataVM
+        sw-image-desc: hackfest3-mgmt
+        virtual-compute-desc: data-compute
+        virtual-storage-desc: data-storage
+        int-cpd:
+          - id: dataVM-internal
+            int-virtual-link-desc: internal
+            order: 1
+            virtual-network-interface-requirement:
+              - name: dataVM-eth1
+                virtual-interface:
+                  type: VIRTIO
+          - id: vnf-data
+            order: 2
+            virtual-network-interface-requirement:
+              - name: dataVM-eth0
+                virtual-interface:
+                  type: VIRTIO
+        monitoring-parameter:
+          - id: dataVM_cpu_util
+            name: dataVM_cpu_util
+            performance-metric: cpu_utilization
+
+    int-virtual-link-desc:
+      - id: internal
+
+    ext-cpd:
+      - id: vnf-mgmt-ext
+        int-cpd: # Connection to int-cpd
+          vdu-id: mgmtVM
+          cpd: vnf-mgmt
+      - id: vnf-data-ext
+        int-cpd: # Connection to int-cpd
+          vdu-id: dataVM
+          cpd: vnf-data
+
+    df:
+      - id: hackfest_default
+        vdu-profile:
+          - id: mgmtVM
+            min-number-of-instances: 1
+          - id: dataVM
+            min-number-of-instances: 1
+            max-number-of-instances: 10
+        instantiation-level:
+          - id: default
+            vdu-level:
+              - vdu-id: mgmtVM
+                number-of-instances: 1
+              - vdu-id: dataVM
+                number-of-instances: 1
+        scaling-aspect:
+          - id: scale_dataVM
+            name: scale_dataVM
+            max-scale-level: 10
+            aspect-delta-details:
+              deltas:
+              - id: delta1
+                vdu-delta:
+                - id: vdudelta1
+                  number-of-instances: 1
+            scaling-policy:
+              - name: auto_cpu_util_above_threshold
+                scaling-type: automatic
+                enabled: true
+                threshold-time: 0
+                cooldown-time: 60
+                scaling-criteria:
+                  - name: cpu_util_above_threshold
+                    scale-in-relational-operation: LE
+                    scale-in-threshold: '15.0000000000'
+                    scale-out-relational-operation: GE
+                    scale-out-threshold: '60.0000000000'
+                    vnf-monitoring-param-ref: dataVM_cpu_util
+            scaling-config-action:
+              - trigger: post-scale-out
+                vnf-config-primitive-name-ref: touch
+              - trigger: pre-scale-in
+                vnf-config-primitive-name-ref: touch
+        lcm-operations-configuration:
+          operate-vnf-op-config:
+            day1-2:
+            - id: hackfest3charmed-vnf
+              execution-environment-list:
+                - id: simple-ee
+                  juju:
+                    charm: simple
+              initial-config-primitive:
+                - seq: "1"
+                  execution-environment-ref: simple-ee
+                  name: config
+                  parameter:
+                    - name: ssh-hostname
+                      value: <rw_mgmt_ip>
+                    - name: ssh-username
+                      value: ubuntu
+                    - name: ssh-password
+                      value: osm4u
+                - seq: "2"
+                  execution-environment-ref: simple-ee
+                  name: touch
+                  parameter:
+                    - name: filename
+                      value: <touch_filename>
+              config-primitive:
+                - name: touch
+                  execution-environment-ref: simple-ee
+                  parameter:
+                    - data-type: STRING
+                      default-value: <touch_filename2>
+                      name: filename
+"""
+
 db_vnfds_text = """
 ---
 -   _admin:
@@ -1810,6 +2035,89 @@ db_vnfrs_text = """
         vim-id: ff181e6d-2597-4244-b40b-bb0174bdfeb6
     vnfd-id: 7637bcf8-cf14-42dc-ad70-c66fcf1e6e77
     vnfd-ref: hackfest3charmed-vnf
+-   _admin:
+        created: 1566823354.3668208
+        modified: 1566823354.3668208
+        nsState: NOT_INSTANTIATED
+        projects_read:
+        - 25b5aebf-3da1-49ed-99de-1d2b4a86d6e4
+        projects_write:
+        - 25b5aebf-3da1-49ed-99de-1d2b4a86d6e4
+    _id: 6421c7c9-d865-4fb4-9a13-d4275d243e01
+    additionalParamsForVnf:
+        touch_filename: /home/ubuntu/first-touch-1
+        touch_filename2: /home/ubuntu/second-touch-1
+    connection-point:
+    -   connection-point-id: vnf-mgmt
+        id: vnf-mgmt
+        name: vnf-mgmt
+    -   connection-point-id: vnf-data
+        id: vnf-data
+        name: vnf-data
+    created-time: 1566823354.36234
+    id: 6421c7c9-d865-4fb4-9a13-d4275d243e01
+    ip-address: 10.205.1.46
+    member-vnf-index-ref: '1'
+    nsr-id-ref: f48163a6-c807-47bc-9682-f72caef5af85
+    vdur:
+    -   _id: f0e7d7ce-2443-4dcb-ad0b-5ab9f3b13d37
+        count-index: 0
+        interfaces:
+        -   ip-address: 10.205.1.46
+            mac-address: fa:16:3e:b4:3e:b1
+            mgmt-vnf: true
+            name: mgmtVM-eth0
+            ns-vld-id: mgmt
+        -   ip-address: 192.168.54.2
+            mac-address: fa:16:3e:6e:7e:78
+            name: mgmtVM-eth1
+            vnf-vld-id: internal
+        internal-connection-point:
+        -   connection-point-id: mgmtVM-internal
+            id: mgmtVM-internal
+            name: mgmtVM-internal
+        ip-address: 10.205.1.46
+        name: ALF-1-mgmtVM-1
+        status: ACTIVE
+        status-detailed: null
+        vdu-id-ref: mgmtVM
+        vim-id: c2538499-4c30-41c0-acd5-80cb92f48061
+        ns-image-id: 0
+        ns-flavor-id: 0
+        affinity-or-anti-affinity-group-id : []
+    -   _id: ab453219-2d9a-45c2-864d-2c0788385028
+        count-index: 0
+        interfaces:
+        -   ip-address: 192.168.54.3
+            mac-address: fa:16:3e:d9:7a:5d
+            name: dataVM-eth0
+            vnf-vld-id: internal
+        -   ip-address: 192.168.24.3
+            mac-address: fa:16:3e:d1:6c:0d
+            name: dataVM-xe0
+            ns-vld-id: datanet
+        internal-connection-point:
+        -   connection-point-id: dataVM-internal
+            id: dataVM-internal
+            name: dataVM-internal
+        ip-address: null
+        name: ALF-1-dataVM-1
+        status: ACTIVE
+        status-detailed: null
+        vdu-id-ref: dataVM
+        vim-id: 87973c3f-365d-4227-95c2-7a8abc74349c
+        ns-image-id: 0
+        ns-flavor-id: 0
+        affinity-or-anti-affinity-group-id : []
+    vim-account-id: ea958ba5-4e58-4405-bf42-6e3be15d4c3a
+    vld:
+    -   id: internal
+        name: ALF-internal
+        status: ACTIVE
+        status-detailed: null
+        vim-id: ff181e6d-2597-4244-b40b-bb0174bdfeb6
+    vnfd-id: 7637bcf8-cf14-42dc-ad70-c66fcf1e6e77
+    vnfd-ref: hackfest3charmed-vnf
 -   _admin:
         created: 1566823354.3703845
         modified: 1566823354.3703845
@@ -2038,6 +2346,7 @@ test_ids = {
         "ns": "f48163a6-c807-47bc-9682-f72caef5af85",
         "instantiate": "a639fac7-e0bb-4225-8ecb-c1f8efcc125e",
         "terminate": "a639fac7-e0bb-4225-ffff-c1f8efcc125e",
+        "update": "6bd4362f-da74-4bd8-a825-fd00e610c644",
     },
     "TEST-KDU": {
         "ns": "0bcb701c-ee4d-41ab-8ee6-f4156f7f114d",
index ef87a4c..86e2136 100644 (file)
@@ -27,7 +27,11 @@ from osm_lcm.lcm_utils import TaskRegistry
 from osm_lcm.ng_ro import NgRoClient
 from osm_lcm.data_utils.database.database import Database
 from osm_lcm.data_utils.filesystem.filesystem import Filesystem
+from osm_lcm.data_utils.vnfd import find_software_version
+from osm_lcm.lcm_utils import check_juju_bundle_existence, get_charm_artifact_path
+from osm_lcm.lcm_utils import LcmException
 from uuid import uuid4
+from unittest.mock import Mock, patch
 
 from osm_lcm.tests import test_db_descriptors as descriptors
 
@@ -168,6 +172,10 @@ class TestMyNS(asynctest.TestCase):
             self.db.create_list(
                 "vnfds", yaml.load(descriptors.db_vnfds_text, Loader=yaml.Loader)
             )
+            self.db.create_list(
+                "vnfds_revisions",
+                yaml.load(descriptors.db_vnfds_revisions_text, Loader=yaml.Loader),
+            )
             self.db.create_list(
                 "nsds", yaml.load(descriptors.db_nsds_text, Loader=yaml.Loader)
             )
@@ -855,6 +863,748 @@ class TestMyNS(asynctest.TestCase):
     #     self.assertEqual(db_nsr.get("errorDescription "), None, "errorDescription different than None")
     #     self.assertEqual(db_nsr.get("errorDetail"), None, "errorDetail different than None")
 
+    # Test update method
+
+    async def test_update(self):
+
+        nsr_id = descriptors.test_ids["TEST-A"]["ns"]
+        nslcmop_id = descriptors.test_ids["TEST-A"]["update"]
+        vnfr_id = "6421c7c9-d865-4fb4-9a13-d4275d243e01"
+        vnfd_id = "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77"
+
+        def mock_reset():
+            mock_charm_hash.reset_mock()
+            mock_juju_bundle.reset_mock()
+            fs.sync.reset_mock()
+            mock_charm_upgrade.reset_mock()
+            mock_software_version.reset_mock()
+
+        with self.subTest(
+            i=1,
+            t="Update type: CHANGE_VNFPKG, latest_vnfd revision changed,"
+            "Charm package changed, sw-version is not changed.",
+        ):
+
+            self.db.set_one(
+                "vnfds",
+                q_filter={"_id": vnfd_id},
+                update_dict={"_admin.revision": 3, "kdu": []},
+            )
+
+            self.db.set_one(
+                "vnfds_revisions",
+                q_filter={"_id": vnfd_id + ":1"},
+                update_dict={"_admin.revision": 1, "kdu": []},
+            )
+
+            self.db.set_one(
+                "vnfrs", q_filter={"_id": vnfr_id}, update_dict={"revision": 1}
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = True
+
+            mock_juju_bundle = Mock(return_value=None)
+
+            mock_software_version = Mock(autospec=True)
+            mock_software_version.side_effect = ["1.0", "1.0"]
+
+            mock_charm_upgrade = asynctest.Mock(autospec=True)
+            task = asyncio.Future()
+            task.set_result(("COMPLETED", "some_output"))
+            mock_charm_upgrade.return_value = task
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "COMPLETED"
+            expected_operation_error = ""
+            expected_vnfr_revision = 3
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.ns.NsLcm._ns_charm_upgrade", mock_charm_upgrade), patch(
+                "osm_lcm.data_utils.vnfd.find_software_version", mock_software_version
+            ), patch(
+                "osm_lcm.lcm_utils.check_juju_bundle_existence", mock_juju_bundle
+            ):
+
+                await instance.update(nsr_id, nslcmop_id)
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                mock_charm_hash.assert_called_with(
+                    "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1/hackfest_3charmed_vnfd/charms/simple",
+                    "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/hackfest_3charmed_vnfd/charms/simple",
+                )
+
+                self.assertEqual(fs.sync.call_count, 2)
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+        with self.subTest(
+            i=2, t="Update type: CHANGE_VNFPKG, latest_vnfd revision not changed"
+        ):
+
+            self.db.set_one(
+                "vnfds", q_filter={"_id": vnfd_id}, update_dict={"_admin.revision": 1}
+            )
+
+            self.db.set_one(
+                "vnfrs", q_filter={"_id": vnfr_id}, update_dict={"revision": 1}
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = True
+
+            mock_juju_bundle = Mock(return_value=None)
+            mock_software_version = Mock(autospec=True)
+
+            mock_charm_upgrade = asynctest.Mock(autospec=True)
+            task = asyncio.Future()
+            task.set_result(("COMPLETED", "some_output"))
+            mock_charm_upgrade.return_value = task
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "COMPLETED"
+            expected_operation_error = ""
+            expected_vnfr_revision = 1
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.ns.NsLcm._ns_charm_upgrade", mock_charm_upgrade), patch(
+                "osm_lcm.lcm_utils.check_juju_bundle_existence", mock_juju_bundle
+            ):
+
+                await instance.update(nsr_id, nslcmop_id)
+
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                mock_charm_hash.assert_not_called()
+                mock_software_version.assert_not_called()
+                mock_juju_bundle.assert_not_called()
+                mock_charm_upgrade.assert_not_called()
+                fs.sync.assert_not_called()
+
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+        with self.subTest(
+            i=3,
+            t="Update type: CHANGE_VNFPKG, latest_vnfd revision changed, "
+            "Charm package is not changed, sw-version is not changed.",
+        ):
+
+            self.db.set_one(
+                "vnfds", q_filter={"_id": vnfd_id}, update_dict={"_admin.revision": 3}
+            )
+
+            self.db.set_one(
+                "vnfds_revisions",
+                q_filter={"_id": vnfd_id + ":1"},
+                update_dict={"_admin.revision": 1},
+            )
+
+            self.db.set_one(
+                "vnfrs", q_filter={"_id": vnfr_id}, update_dict={"revision": 1}
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = False
+
+            mock_juju_bundle = Mock(return_value=None)
+
+            mock_software_version = Mock(autospec=True)
+
+            mock_charm_upgrade = asynctest.Mock(autospec=True)
+            task = asyncio.Future()
+            task.set_result(("COMPLETED", "some_output"))
+            mock_charm_upgrade.return_value = task
+            mock_software_version.side_effect = ["1.0", "1.0"]
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "COMPLETED"
+            expected_operation_error = ""
+            expected_vnfr_revision = 3
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.ns.NsLcm._ns_charm_upgrade", mock_charm_upgrade), patch(
+                "osm_lcm.lcm_utils.check_juju_bundle_existence", mock_juju_bundle
+            ):
+
+                await instance.update(nsr_id, nslcmop_id)
+
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                mock_charm_hash.assert_called_with(
+                    "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1/hackfest_3charmed_vnfd/charms/simple",
+                    "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/hackfest_3charmed_vnfd/charms/simple",
+                )
+
+                self.assertEqual(fs.sync.call_count, 2)
+                self.assertEqual(mock_charm_hash.call_count, 1)
+
+                mock_juju_bundle.assert_not_called()
+                mock_charm_upgrade.assert_not_called()
+
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+        with self.subTest(
+            i=4,
+            t="Update type: CHANGE_VNFPKG, latest_vnfd revision changed, "
+            "Charm package exists, sw-version changed.",
+        ):
+
+            self.db.set_one(
+                "vnfds",
+                q_filter={"_id": vnfd_id},
+                update_dict={"_admin.revision": 3, "software-version": "3.0"},
+            )
+
+            self.db.set_one(
+                "vnfds_revisions",
+                q_filter={"_id": vnfd_id + ":1"},
+                update_dict={"_admin.revision": 1},
+            )
+
+            self.db.set_one(
+                "vnfrs",
+                q_filter={"_id": vnfr_id},
+                update_dict={"revision": 1},
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = False
+
+            mock_juju_bundle = Mock(return_value=None)
+
+            mock_charm_upgrade = asynctest.Mock(autospec=True)
+            task = asyncio.Future()
+            task.set_result(("COMPLETED", "some_output"))
+            mock_charm_upgrade.return_value = task
+
+            mock_charm_artifact = Mock(autospec=True)
+            mock_charm_artifact.side_effect = [
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1/hackfest_3charmed_vnfd/charms/simple",
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/hackfest_3charmed_vnfd/charms/simple",
+            ]
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "FAILED"
+            expected_operation_error = "FAILED Checking if existing VNF has charm: Software version change is not supported as VNF instance 6421c7c9-d865-4fb4-9a13-d4275d243e01 has charm."
+            expected_vnfr_revision = 1
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.ns.NsLcm._ns_charm_upgrade", mock_charm_upgrade), patch(
+                "osm_lcm.lcm_utils.get_charm_artifact_path", mock_charm_artifact
+            ):
+
+                await instance.update(nsr_id, nslcmop_id)
+
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                self.assertEqual(fs.sync.call_count, 2)
+                mock_charm_hash.assert_not_called()
+
+                mock_juju_bundle.assert_not_called()
+                mock_charm_upgrade.assert_not_called()
+
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+        with self.subTest(
+            i=5,
+            t="Update type: CHANGE_VNFPKG, latest_vnfd revision changed,"
+            "Charm package exists, sw-version not changed, juju-bundle exists",
+        ):
+
+            self.db.set_one(
+                "vnfds",
+                q_filter={"_id": vnfd_id},
+                update_dict={
+                    "_admin.revision": 3,
+                    "software-version": "1.0",
+                    "kdu.0.juju-bundle": "stable/native-kdu",
+                },
+            )
+
+            self.db.set_one(
+                "vnfds_revisions",
+                q_filter={"_id": vnfd_id + ":1"},
+                update_dict={
+                    "_admin.revision": 1,
+                    "software-version": "1.0",
+                    "kdu.0.juju-bundle": "stable/native-kdu",
+                },
+            )
+
+            self.db.set_one(
+                "vnfrs", q_filter={"_id": vnfr_id}, update_dict={"revision": 1}
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = True
+
+            mock_charm_artifact = Mock(autospec=True)
+            mock_charm_artifact.side_effect = [
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1/hackfest_3charmed_vnfd/charms/simple",
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/hackfest_3charmed_vnfd/charms/simple",
+            ]
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "FAILED"
+            expected_operation_error = "FAILED Checking whether VNF uses juju bundle: Charm upgrade is not supported for the instance which uses juju-bundle: stable/native-kdu"
+            expected_vnfr_revision = 1
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.lcm_utils.get_charm_artifact_path", mock_charm_artifact):
+
+                await instance.update(nsr_id, nslcmop_id)
+
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                self.assertEqual(fs.sync.call_count, 2)
+                self.assertEqual(mock_charm_hash.call_count, 1)
+                self.assertEqual(mock_charm_hash.call_count, 1)
+
+                mock_charm_upgrade.assert_not_called()
+
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+        with self.subTest(
+            i=6,
+            t="Update type: CHANGE_VNFPKG, latest_vnfd revision changed,"
+            "Charm package exists, sw-version not changed, charm-upgrade failed",
+        ):
+
+            self.db.set_one(
+                "vnfds",
+                q_filter={"_id": vnfd_id},
+                update_dict={
+                    "_admin.revision": 3,
+                    "software-version": "1.0",
+                    "kdu": [],
+                },
+            )
+
+            self.db.set_one(
+                "vnfds_revisions",
+                q_filter={"_id": vnfd_id + ":1"},
+                update_dict={
+                    "_admin.revision": 1,
+                    "software-version": "1.0",
+                    "kdu": [],
+                },
+            )
+
+            self.db.set_one(
+                "vnfrs", q_filter={"_id": vnfr_id}, update_dict={"revision": 1}
+            )
+
+            mock_charm_hash = Mock(autospec=True)
+            mock_charm_hash.return_value = True
+
+            mock_charm_upgrade = asynctest.Mock(autospec=True)
+            task = asyncio.Future()
+            task.set_result(("FAILED", "some_error"))
+            mock_charm_upgrade.return_value = task
+
+            mock_charm_artifact = Mock(autospec=True)
+            mock_charm_artifact.side_effect = [
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:1/hackfest_3charmed_vnfd/charms/simple",
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/hackfest_3charmed_vnfd/charms/simple",
+            ]
+
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+            fs.sync.side_effect = [None, None]
+
+            instance = self.my_ns
+
+            expected_operation_state = "FAILED"
+            expected_operation_error = "some_error"
+            expected_vnfr_revision = 1
+            expected_ns_state = "INSTANTIATED"
+            expected_ns_operational_state = "running"
+
+            with patch.object(instance, "fs", fs), patch(
+                "osm_lcm.lcm_utils.LcmBase.check_charm_hash_changed", mock_charm_hash
+            ), patch("osm_lcm.ns.NsLcm._ns_charm_upgrade", mock_charm_upgrade):
+
+                await instance.update(nsr_id, nslcmop_id)
+
+                return_operation_state = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("operationState")
+
+                return_operation_error = self.db.get_one(
+                    "nslcmops", {"_id": nslcmop_id}
+                ).get("errorMessage")
+
+                return_ns_operational_state = self.db.get_one(
+                    "nsrs", {"_id": nsr_id}
+                ).get("operational-status")
+
+                return_vnfr_revision = self.db.get_one("vnfrs", {"_id": vnfr_id}).get(
+                    "revision"
+                )
+
+                return_ns_state = self.db.get_one("nsrs", {"_id": nsr_id}).get(
+                    "nsState"
+                )
+
+                self.assertEqual(fs.sync.call_count, 2)
+                self.assertEqual(mock_charm_hash.call_count, 1)
+                self.assertEqual(mock_charm_upgrade.call_count, 1)
+
+                self.assertEqual(return_ns_state, expected_ns_state)
+                self.assertEqual(return_operation_state, expected_operation_state)
+                self.assertEqual(return_operation_error, expected_operation_error)
+                self.assertEqual(
+                    return_ns_operational_state, expected_ns_operational_state
+                )
+                self.assertEqual(return_vnfr_revision, expected_vnfr_revision)
+
+            mock_reset()
+
+    def test_ns_update_helper_methods(self):
+        def mock_reset():
+            fs.mock_reset()
+            mock_path.mock_reset()
+            mock_checksumdir.mock_reset()
+
+        with self.subTest(
+            i=1, t="Find software version, VNFD does not have have software version"
+        ):
+            # Testing method find_software_version
+
+            db_vnfd = self.db.get_one(
+                "vnfds", {"_id": "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77"}
+            )
+            expected_result = "1.0"
+            result = find_software_version(db_vnfd)
+            self.assertEqual(
+                result, expected_result, "Default sw version should be 1.0"
+            )
+
+        with self.subTest(
+            i=2, t="Find software version, VNFD includes software version"
+        ):
+            # Testing method find_software_version
+
+            db_vnfd = self.db.get_one(
+                "vnfds", {"_id": "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77"}
+            )
+            db_vnfd["software-version"] = "3.1"
+            expected_result = "3.1"
+            result = find_software_version(db_vnfd)
+            self.assertEqual(result, expected_result, "VNFD software version is wrong")
+
+        with self.subTest(i=3, t="Check charm hash, Hash has did not change"):
+            # Testing method check_charm_hash_changed
+
+            current_path, target_path = "/tmp/charm1", "/tmp/charm1"
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+
+            mock_path = Mock(autospec=True)
+            mock_path.exists.side_effect = [True, True]
+
+            mock_checksumdir = Mock(autospec=True)
+            mock_checksumdir.dirhash.side_effect = ["hash_value", "hash_value"]
+
+            instance = self.my_ns
+            expected_result = False
+
+            with patch.object(instance, "fs", fs), patch(
+                "checksumdir.dirhash", mock_checksumdir.dirhash
+            ), patch("os.path.exists", mock_path.exists):
+
+                result = instance.check_charm_hash_changed(current_path, target_path)
+                self.assertEqual(
+                    result, expected_result, "Wrong charm hash control value"
+                )
+                self.assertEqual(mock_path.exists.call_count, 2)
+                self.assertEqual(mock_checksumdir.dirhash.call_count, 2)
+
+            mock_reset()
+
+        with self.subTest(i=4, t="Check charm hash, Hash has changed"):
+            # Testing method check_charm_hash_changed
+
+            current_path, target_path = "/tmp/charm1", "/tmp/charm2"
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+
+            mock_path = Mock(autospec=True)
+            mock_path.exists.side_effect = [True, True]
+
+            mock_checksumdir = Mock(autospec=True)
+            mock_checksumdir.dirhash.side_effect = ["hash_value", "another_hash_value"]
+
+            instance = self.my_ns
+            expected_result = True
+
+            with patch.object(instance, "fs", fs), patch(
+                "checksumdir.dirhash", mock_checksumdir.dirhash
+            ), patch("os.path.exists", mock_path.exists):
+
+                result = instance.check_charm_hash_changed(current_path, target_path)
+                self.assertEqual(
+                    result, expected_result, "Wrong charm hash control value"
+                )
+                self.assertEqual(mock_path.exists.call_count, 2)
+                self.assertEqual(mock_checksumdir.dirhash.call_count, 2)
+
+            mock_reset()
+
+        with self.subTest(i=5, t="Check charm hash, Charm path does not exists"):
+            # Testing method check_charm_hash_changed
+
+            current_path, target_path = "/tmp/charm1", "/tmp/charm2"
+            fs = Mock(autospec=True)
+            fs.path.__add__ = Mock()
+            fs.path.side_effect = ["/", "/", "/", "/"]
+
+            mock_path = Mock(autospec=True)
+            mock_path.exists.side_effect = [True, False]
+
+            mock_checksumdir = Mock(autospec=True)
+            mock_checksumdir.dirhash.side_effect = ["hash_value", "hash_value"]
+
+            instance = self.my_ns
+
+            with patch.object(instance, "fs", fs), patch(
+                "checksumdir.dirhash", mock_checksumdir.dirhash
+            ), patch("os.path.exists", mock_path.exists):
+
+                with self.assertRaises(LcmException):
+
+                    instance.check_charm_hash_changed(current_path, target_path)
+                    self.assertEqual(mock_path.exists.call_count, 2)
+                    self.assertEqual(mock_checksumdir.dirhash.call_count, 0)
+
+            mock_reset()
+
+        with self.subTest(i=6, t="Check juju bundle existence"):
+            # Testing method check_juju_bundle_existence
+
+            test_vnfd1 = self.db.get_one(
+                "vnfds", {"_id": "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77"}
+            )
+            test_vnfd2 = self.db.get_one(
+                "vnfds", {"_id": "d96b1cdf-5ad6-49f7-bf65-907ada989293"}
+            )
+
+            expected_result = None
+            result = check_juju_bundle_existence(test_vnfd1)
+            self.assertEqual(result, expected_result, "Wrong juju bundle name")
+
+            expected_result = "stable/native-kdu"
+            result = check_juju_bundle_existence(test_vnfd2)
+            self.assertEqual(result, expected_result, "Wrong juju bundle name")
+
+        with self.subTest(i=7, t="Check charm artifacts"):
+            # Testing method check_juju_bundle_existence
+
+            base_folder = {
+                "folder": "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77",
+                "pkg-dir": "hackfest_3charmed_vnfd",
+            }
+            charm_name = "simple"
+            charm_type = "lxc_proxy_charm"
+            revision = 3
+
+            expected_result = "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77:3/hackfest_3charmed_vnfd/charms/simple"
+            result = get_charm_artifact_path(
+                base_folder, charm_name, charm_type, revision
+            )
+            self.assertEqual(result, expected_result, "Wrong charm artifact path")
+
+            # SOL004 packages
+            base_folder = {
+                "folder": "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77",
+            }
+            charm_name = "basic"
+            charm_type = ""
+            revision = ""
+
+            expected_result = (
+                "7637bcf8-cf14-42dc-ad70-c66fcf1e6e77/Scripts/helm-charts/basic"
+            )
+            result = get_charm_artifact_path(
+                base_folder, charm_name, charm_type, revision
+            )
+            self.assertEqual(result, expected_result, "Wrong charm artifact path")
+
 
 if __name__ == "__main__":
     asynctest.main()
index 6efebb2..0ec21a3 100644 (file)
@@ -14,6 +14,7 @@
 
 aiohttp<3.8
 async-timeout<4
+checksumdir
 grpcio-tools
 grpclib
 idna
index 0756c97..2e90430 100644 (file)
@@ -24,9 +24,11 @@ attrs==21.4.0
     # via aiohttp
 chardet==4.0.0
     # via aiohttp
-grpcio==1.43.0
+checksumdir==1.2.0
+    # via -r requirements.in
+grpcio==1.44.0
     # via grpcio-tools
-grpcio-tools==1.43.0
+grpcio-tools==1.44.0
     # via -r requirements.in
 grpclib==0.4.2
     # via -r requirements.in
@@ -40,11 +42,11 @@ idna==3.3
     # via
     #   -r requirements.in
     #   yarl
-jinja2==3.0.3
+jinja2==3.1.1
     # via -r requirements.in
-markupsafe==2.0.1
+markupsafe==2.1.1
     # via jinja2
-multidict==5.2.0
+multidict==6.0.2
     # via
     #   aiohttp
     #   grpclib