Extracts individual validations for VnfdTopic and NsdTopic classes and adds unit...
[osm/NBI.git] / osm_nbi / descriptor_topics.py
index 18ce3e7..3bdaa03 100644 (file)
@@ -21,7 +21,10 @@ from hashlib import md5
 from osm_common.dbbase import DbException, deep_update_rfc7396
 from http import HTTPStatus
 from time import time
-from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema
+from uuid import uuid4
+from re import fullmatch
+from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema, \
+    validate_input, vnfpkgop_new_schema
 from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
 from osm_im.vnfd import vnfd as vnfd_im
 from osm_im.nsd import nsd as nsd_im
@@ -39,6 +42,28 @@ class DescriptorTopic(BaseTopic):
 
     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
         super().check_conflict_on_edit(session, final_content, edit_content, _id)
+
+        def _check_unique_id_name(descriptor, position=""):
+            for desc_key, desc_item in descriptor.items():
+                if isinstance(desc_item, list) and desc_item:
+                    used_ids = []
+                    desc_item_id = None
+                    for index, list_item in enumerate(desc_item):
+                        if isinstance(list_item, dict):
+                            _check_unique_id_name(list_item, "{}.{}[{}]"
+                                                  .format(position, desc_key, index))
+                            # Base case
+                            if index == 0 and (list_item.get("id") or list_item.get("name")):
+                                desc_item_id = "id" if list_item.get("id") else "name"
+                            if desc_item_id and list_item.get(desc_item_id):
+                                if list_item[desc_item_id] in used_ids:
+                                    position = "{}.{}[{}]".format(position, desc_key, index)
+                                    raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'"
+                                                          .format(desc_item_id, list_item[desc_item_id],
+                                                                  position), HTTPStatus.UNPROCESSABLE_ENTITY)
+                                used_ids.append(list_item[desc_item_id])
+
+        _check_unique_id_name(final_content)
         # 1. validate again with pyangbind
         # 1.1. remove internal keys
         internal_keys = {}
@@ -73,12 +98,13 @@ class DescriptorTopic(BaseTopic):
         content["_admin"]["operationalState"] = "DISABLED"
         content["_admin"]["usageState"] = "NOT_IN_USE"
 
-    def delete_extra(self, session, _id, db_content):
+    def delete_extra(self, session, _id, db_content, not_send_msg=None):
         """
         Deletes file system storage associated with the descriptor
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
         :param _id: server internal id
         :param db_content: The database content of the descriptor
+        :param not_send_msg: To not send message (False) or store content (list) instead
         :return: None if ok or raises EngineException with the problem
         """
         self.fs.file_delete(_id, ignore_non_exist=True)
@@ -121,29 +147,27 @@ class DescriptorTopic(BaseTopic):
         :return: _id, None: identity of the inserted data; and None as there is not any operation
         """
 
-        try:
-            # Check Quota
-            self.check_quota(session)
+        # No needed to capture exceptions
+        # Check Quota
+        self.check_quota(session)
 
-            # _remove_envelop
-            if indata:
-                if "userDefinedData" in indata:
-                    indata = indata['userDefinedData']
+        # _remove_envelop
+        if indata:
+            if "userDefinedData" in indata:
+                indata = indata['userDefinedData']
 
-            # Override descriptor with query string kwargs
-            self._update_input_with_kwargs(indata, kwargs)
-            # uncomment when this method is implemented.
-            # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
-            # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
-
-            content = {"_admin": {"userDefinedData": indata}}
-            self.format_on_new(content, session["project_id"], make_public=session["public"])
-            _id = self.db.create(self.topic, content)
-            rollback.append({"topic": self.topic, "_id": _id})
-            self._send_msg("created", {"_id": _id})
-            return _id, None
-        except ValidationError as e:
-            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+        # Override descriptor with query string kwargs
+        self._update_input_with_kwargs(indata, kwargs)
+        # uncomment when this method is implemented.
+        # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
+        # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
+
+        content = {"_admin": {"userDefinedData": indata}}
+        self.format_on_new(content, session["project_id"], make_public=session["public"])
+        _id = self.db.create(self.topic, content)
+        rollback.append({"topic": self.topic, "_id": _id})
+        self._send_msg("created", {"_id": _id})
+        return _id, None
 
     def upload_content(self, session, _id, indata, kwargs, headers):
         """
@@ -215,9 +239,9 @@ class DescriptorTopic(BaseTopic):
                         break
                     file_pkg.write(indata_text)
             if content_range_text:
-                if indata_len != end-start:
+                if indata_len != end - start:
                     raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
-                        start, end-1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
+                        start, end - 1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
                 if end != total:
                     # TODO update to UPLOADING
                     return False
@@ -338,7 +362,7 @@ class DescriptorTopic(BaseTopic):
                                   "onboardingState is {}".format(content["_admin"]["onboardingState"]),
                                   http_code=HTTPStatus.CONFLICT)
         storage = content["_admin"]["storage"]
-        if path is not None and path != "$DESCRIPTOR":   # artifacts
+        if path is not None and path != "$DESCRIPTOR":  # artifacts
             if not storage.get('pkg-dir'):
                 raise EngineException("Packages does not contains artifacts", http_code=HTTPStatus.BAD_REQUEST)
             if self.fs.file_exists((storage['folder'], storage['pkg-dir'], *path), 'dir'):
@@ -346,7 +370,7 @@ class DescriptorTopic(BaseTopic):
                 return folder_content, "text/plain"
                 # TODO manage folders in http
             else:
-                return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"),\
+                return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"), \
                     "application/octet-stream"
 
         # pkgtype   accept  ZIP  TEXT    -> result
@@ -354,10 +378,15 @@ class DescriptorTopic(BaseTopic):
         #                   no   yes     -> error
         # onefile           yes  no      -> zip
         #                   X    yes     -> text
-
-        if accept_text and (not storage.get('pkg-dir') or path == "$DESCRIPTOR"):
+        contain_many_files = False
+        if storage.get('pkg-dir'):
+            # check if there are more than one file in the package, ignoring checksums.txt.
+            pkg_files = self.fs.dir_ls((storage['folder'], storage['pkg-dir']))
+            if len(pkg_files) >= 3 or (len(pkg_files) == 2 and 'checksums.txt' not in pkg_files):
+                contain_many_files = True
+        if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
             return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
-        elif storage.get('pkg-dir') and not accept_zip:
+        elif contain_many_files and not accept_zip:
             raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
                                   "Accept header", http_code=HTTPStatus.NOT_ACCEPTABLE)
         else:
@@ -395,6 +424,39 @@ class DescriptorTopic(BaseTopic):
             raise EngineException("Error in pyangbind validation: {}".format(str(e)),
                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
 
+    def _validate_input_edit(self, indata, content, force=False):
+        # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
+        if "_id" in indata:
+            indata.pop("_id")
+        if "_admin" not in indata:
+            indata["_admin"] = {}
+
+        if "operationalState" in indata:
+            if indata["operationalState"] in ("ENABLED", "DISABLED"):
+                indata["_admin"]["operationalState"] = indata.pop("operationalState")
+            else:
+                raise EngineException("State '{}' is not a valid operational state"
+                                      .format(indata["operationalState"]),
+                                      http_code=HTTPStatus.BAD_REQUEST)
+
+        # In the case of user defined data, we need to put the data in the root of the object 
+        # to preserve current expected behaviour
+        if "userDefinedData" in indata:
+            data = indata.pop("userDefinedData")
+            if type(data) == dict:
+                indata["_admin"]["userDefinedData"] = data
+            else:
+                raise EngineException("userDefinedData should be an object, but is '{}' instead"
+                                      .format(type(data)),
+                                      http_code=HTTPStatus.BAD_REQUEST)
+
+        if ("operationalState" in indata["_admin"] and
+                content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
+            raise EngineException("operationalState already {}".format(content["_admin"]["operationalState"]),
+                                  http_code=HTTPStatus.CONFLICT)
+
+        return indata
+
 
 class VnfdTopic(DescriptorTopic):
     topic = "vnfds"
@@ -471,72 +533,136 @@ class VnfdTopic(DescriptorTopic):
                                   http_code=HTTPStatus.CONFLICT)
 
     def _validate_input_new(self, indata, storage_params, force=False):
+        indata.pop("onboardingState", None)
+        indata.pop("operationalState", None)
+        indata.pop("usageState", None)
+
+        indata.pop("links", None)
+
         indata = self.pyangbind_validation("vnfds", indata, force)
         # Cross references validation in the descriptor
-        if indata.get("vdu"):
-            if not indata.get("mgmt-interface"):
-                raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
+        self.validate_mgmt_interfaces_connection_points(indata)
+
+        for vdu in get_iterable(indata.get("vdu")):
+            self.validate_vdu_connection_point_refs(vdu, indata)
+            self._validate_vdu_charms_in_package(storage_params, vdu, indata)
+            self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
+
+        self._validate_vnf_charms_in_package(storage_params, indata)
+
+        self.validate_internal_vlds(indata)
+        self.validate_monitoring_params(indata)
+        self.validate_scaling_group_descriptor(indata)
+
+        return indata
+
+    @staticmethod
+    def validate_mgmt_interfaces_connection_points(indata):
+        if not indata.get("vdu"):
+            return
+        if not indata.get("mgmt-interface"):
+            raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
+                                  http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+        if indata["mgmt-interface"].get("cp"):
+            for cp in get_iterable(indata.get("connection-point")):
+                if cp["name"] == indata["mgmt-interface"]["cp"]:
+                    break
+            else:
+                raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
+                                      .format(indata["mgmt-interface"]["cp"]),
                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-            if indata["mgmt-interface"].get("cp"):
+
+    @staticmethod
+    def validate_vdu_connection_point_refs(vdu, indata):
+        icp_refs = []
+        ecp_refs = []
+        for interface in get_iterable(vdu.get("interface")):
+            if interface.get("external-connection-point-ref"):
+                if interface.get("external-connection-point-ref") in ecp_refs:
+                    raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
+                                          "is referenced by other interface"
+                                          .format(vdu["id"], interface["name"],
+                                                  interface["external-connection-point-ref"]),
+                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+                ecp_refs.append(interface.get("external-connection-point-ref"))
                 for cp in get_iterable(indata.get("connection-point")):
-                    if cp["name"] == indata["mgmt-interface"]["cp"]:
+                    if cp["name"] == interface["external-connection-point-ref"]:
                         break
                 else:
-                    raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
-                                          .format(indata["mgmt-interface"]["cp"]),
+                    raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
+                                          "must match an existing connection-point"
+                                          .format(vdu["id"], interface["name"],
+                                                  interface["external-connection-point-ref"]),
+                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+            elif interface.get("internal-connection-point-ref"):
+                if interface.get("internal-connection-point-ref") in icp_refs:
+                    raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
+                                          "is referenced by other interface"
+                                          .format(vdu["id"], interface["name"],
+                                                  interface["internal-connection-point-ref"]),
+                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+                icp_refs.append(interface.get("internal-connection-point-ref"))
+                for internal_cp in get_iterable(vdu.get("internal-connection-point")):
+                    if interface["internal-connection-point-ref"] == internal_cp.get("id"):
+                        break
+                else:
+                    raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
+                                          "must match an existing vdu:internal-connection-point"
+                                          .format(vdu["id"], interface["name"],
+                                                  interface["internal-connection-point-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
 
-        for vdu in get_iterable(indata.get("vdu")):
-            for interface in get_iterable(vdu.get("interface")):
-                if interface.get("external-connection-point-ref"):
-                    for cp in get_iterable(indata.get("connection-point")):
-                        if cp["name"] == interface["external-connection-point-ref"]:
-                            break
-                    else:
-                        raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
-                                              "must match an existing connection-point"
-                                              .format(vdu["id"], interface["name"],
-                                                      interface["external-connection-point-ref"]),
-                                              http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+    def _validate_vdu_charms_in_package(self, storage_params, vdu, indata):
+        if not vdu.get("vdu-configuration"):
+            return
+        if vdu["vdu-configuration"].get("juju"):
+            if not self._validate_package_folders(storage_params, 'charms'):
+                raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
+                                      "package".format(indata["id"], vdu["id"]))
 
-                elif interface.get("internal-connection-point-ref"):
-                    for internal_cp in get_iterable(vdu.get("internal-connection-point")):
-                        if interface["internal-connection-point-ref"] == internal_cp.get("id"):
-                            break
-                    else:
-                        raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
-                                              "must match an existing vdu:internal-connection-point"
-                                              .format(vdu["id"], interface["name"],
-                                                      interface["internal-connection-point-ref"]),
-                                              http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-            # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
-            if vdu.get("vdu-configuration"):
-                if vdu["vdu-configuration"].get("juju"):
-                    if not self._validate_package_folders(storage_params, 'charms'):
-                        raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
-                                              "package".format(indata["id"], vdu["id"]))
-            # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
-            if vdu.get("cloud-init-file"):
-                if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
-                    raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
-                                          "package".format(indata["id"], vdu["id"]))
-        # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
-        if indata.get("vnf-configuration"):
-            if indata["vnf-configuration"].get("juju"):
-                if not self._validate_package_folders(storage_params, 'charms'):
-                    raise EngineException("Charm defined in vnf[id={}] but not present in "
-                                          "package".format(indata["id"]))
+    def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
+        if not vdu.get("cloud-init-file"):
+            return
+        if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
+            raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
+                                  "package".format(indata["id"], vdu["id"]))
+
+    def _validate_vnf_charms_in_package(self, storage_params, indata):
+        if not indata.get("vnf-configuration"):
+            return
+        if indata["vnf-configuration"].get("juju"):
+            if not self._validate_package_folders(storage_params, 'charms'):
+                raise EngineException("Charm defined in vnf[id={}] but not present in "
+                                      "package".format(indata["id"]))
+
+    def _validate_package_folders(self, storage_params, folder, file=None):
+        if not storage_params or not storage_params.get("pkg-dir"):
+            return False
+        else:
+            if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
+                f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
+            else:
+                f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
+            if file:
+                return self.fs.file_exists("{}/{}".format(f, file), 'file')
+            else:
+                if self.fs.file_exists(f, 'dir'):
+                    if self.fs.dir_ls(f):
+                        return True
+            return False
+
+    @staticmethod
+    def validate_internal_vlds(indata):
         vld_names = []  # For detection of duplicated VLD names
         for ivld in get_iterable(indata.get("internal-vld")):
-            # BEGIN Detection of duplicated VLD names
-            ivld_name = ivld["name"]
-            if ivld_name in vld_names:
+            ivld_name = ivld.get("name")
+            if ivld_name and ivld_name in vld_names:
                 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
                                       .format(ivld["name"], indata["id"], ivld["id"]),
                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
             else:
                 vld_names.append(ivld_name)
-            # END Detection of duplicated VLD names
+
             for icp in get_iterable(ivld.get("internal-connection-point")):
                 icp_mark = False
                 for vdu in get_iterable(indata.get("vdu")):
@@ -558,12 +684,15 @@ class VnfdTopic(DescriptorTopic):
                     raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
                         ivld["id"], ivld["ip-profile-ref"]),
                         http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    @staticmethod
+    def validate_monitoring_params(indata):
         for mp in get_iterable(indata.get("monitoring-param")):
             if mp.get("vdu-monitoring-param"):
                 mp_vmp_mark = False
                 for vdu in get_iterable(indata.get("vdu")):
                     for vmp in get_iterable(vdu.get("monitoring-param")):
-                        if vmp["id"] == mp["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu["id"] ==\
+                        if vmp["id"] == mp["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu["id"] == \
                                 mp["vdu-monitoring-param"]["vdu-ref"]:
                             mp_vmp_mark = True
                             break
@@ -593,6 +722,8 @@ class VnfdTopic(DescriptorTopic):
                                                   mp["vdu-metric"]["vdu-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
 
+    @staticmethod
+    def validate_scaling_group_descriptor(indata):
         for sgd in get_iterable(indata.get("scaling-group-descriptor")):
             for sp in get_iterable(sgd.get("scaling-policy")):
                 for sc in get_iterable(sp.get("scaling-criteria")):
@@ -631,27 +762,32 @@ class VnfdTopic(DescriptorTopic):
                                           "vnf-configuration:config-primitive:name"
                                           .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-        return indata
 
-    def _validate_input_edit(self, indata, force=False):
-        # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
-        return indata
+    def delete_extra(self, session, _id, db_content, not_send_msg=None):
+        """
+        Deletes associate file system storage (via super)
+        Deletes associated vnfpkgops from database.
+        :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
+        :param _id: server internal id
+        :param db_content: The database content of the descriptor
+        :return: None
+        :raises: FsException in case of error while deleting associated storage
+        """
+        super().delete_extra(session, _id, db_content, not_send_msg)
+        self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
 
-    def _validate_package_folders(self, storage_params, folder, file=None):
-        if not storage_params or not storage_params.get("pkg-dir"):
-            return False
-        else:
-            if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
-                f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
-            else:
-                f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
-            if file:
-                return self.fs.file_exists("{}/{}".format(f, file), 'file')
-            else:
-                if self.fs.file_exists(f, 'dir'):
-                    if self.fs.dir_ls(f):
-                        return True
-            return False
+    def sol005_projection(self, data):
+        data["onboardingState"] = data["_admin"]["onboardingState"]
+        data["operationalState"] = data["_admin"]["operationalState"]
+        data["usageState"] = data["_admin"]["usageState"]
+
+        links = {}
+        links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
+        links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
+        links["packageContent"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])}
+        data["_links"] = links
+
+        return super().sol005_projection(data)
 
 
 class NsdTopic(DescriptorTopic):
@@ -682,47 +818,99 @@ class NsdTopic(DescriptorTopic):
         return clean_indata
 
     def _validate_input_new(self, indata, storage_params, force=False):
+        indata.pop("nsdOnboardingState", None)
+        indata.pop("nsdOperationalState", None)
+        indata.pop("nsdUsageState", None)
+
+        indata.pop("links", None)
+
         indata = self.pyangbind_validation("nsds", indata, force)
         # Cross references validation in the descriptor
         # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
         for vld in get_iterable(indata.get("vld")):
-            if vld.get("mgmt-network") and vld.get("ip-profile-ref"):
-                raise EngineException("Error at vld[id='{}']:ip-profile-ref"
-                                      " You cannot set an ip-profile when mgmt-network is True"
-                                      .format(vld["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-            for vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
-                for constituent_vnfd in get_iterable(indata.get("constituent-vnfd")):
-                    if vnfd_cp["member-vnf-index-ref"] == constituent_vnfd["member-vnf-index"]:
-                        if vnfd_cp.get("vnfd-id-ref") and vnfd_cp["vnfd-id-ref"] != constituent_vnfd["vnfd-id-ref"]:
-                            raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
-                                                  "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
-                                                  " '{}'".format(vld["id"], vnfd_cp["vnfd-id-ref"],
-                                                                 constituent_vnfd["member-vnf-index"],
-                                                                 constituent_vnfd["vnfd-id-ref"]),
-                                                  http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-                        break
-                else:
-                    raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
-                                          "does not match any constituent-vnfd:member-vnf-index"
-                                          .format(vld["id"], vnfd_cp["member-vnf-index-ref"]),
-                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-        # Check VNFFGD
+            self.validate_vld_mgmt_network_with_ip_profile_ref(vld)
+            self.validate_vld_connection_point_refs(vld, indata)
+
         for fgd in get_iterable(indata.get("vnffgd")):
-            for cls in get_iterable(fgd.get("classifier")):
-                rspref = cls.get("rsp-id-ref")
-                for rsp in get_iterable(fgd.get("rsp")):
-                    rspid = rsp.get("id")
-                    if rspid and rspref and rspid == rspref:
-                        break
-                else:
-                    raise EngineException(
-                        "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
-                        .format(fgd["id"], cls["id"], rspref),
-                        http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+            self.validate_fgd_classifiers(fgd)
+
         return indata
 
-    def _validate_input_edit(self, indata, force=False):
+    @staticmethod
+    def validate_vld_mgmt_network_with_ip_profile_ref(vld):
+        if vld.get("mgmt-network") and vld.get("ip-profile-ref"):
+            raise EngineException("Error at vld[id='{}']:ip-profile-ref"
+                                  " You cannot set an ip-profile when mgmt-network is True"
+                                  .format(vld["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    @staticmethod
+    def validate_vld_connection_point_refs(vld, indata):
+        for vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
+            for constituent_vnfd in get_iterable(indata.get("constituent-vnfd")):
+                if vnfd_cp["member-vnf-index-ref"] == constituent_vnfd["member-vnf-index"]:
+                    if vnfd_cp.get("vnfd-id-ref") and vnfd_cp["vnfd-id-ref"] != constituent_vnfd["vnfd-id-ref"]:
+                        raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
+                                              "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
+                                              " '{}'".format(vld["id"], vnfd_cp["vnfd-id-ref"],
+                                                             constituent_vnfd["member-vnf-index"],
+                                                             constituent_vnfd["vnfd-id-ref"]),
+                                              http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+                    break
+            else:
+                raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
+                                      "does not match any constituent-vnfd:member-vnf-index"
+                                      .format(vld["id"], vnfd_cp["member-vnf-index-ref"]),
+                                      http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    @staticmethod
+    def validate_fgd_classifiers(fgd):
+        for cls in get_iterable(fgd.get("classifier")):
+            rspref = cls.get("rsp-id-ref")
+            for rsp in get_iterable(fgd.get("rsp")):
+                rspid = rsp.get("id")
+                if rspid and rspref and rspid == rspref:
+                    break
+            else:
+                raise EngineException(
+                    "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
+                    .format(fgd["id"], cls["id"], rspref),
+                    http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    def _validate_input_edit(self, indata, content, force=False):
         # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
+        """
+        indata looks as follows:
+            - In the new case (conformant) 
+                {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23', 
+                '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
+            - In the old case (backwards-compatible)
+                {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
+        """
+        if "_admin" not in indata:
+            indata["_admin"] = {}
+
+        if "nsdOperationalState" in indata:
+            if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
+                indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
+            else:
+                raise EngineException("State '{}' is not a valid operational state"
+                                      .format(indata["nsdOperationalState"]),
+                                      http_code=HTTPStatus.BAD_REQUEST)
+
+        # In the case of user defined data, we need to put the data in the root of the object 
+        # to preserve current expected behaviour
+        if "userDefinedData" in indata:
+            data = indata.pop("userDefinedData")
+            if type(data) == dict:
+                indata["_admin"]["userDefinedData"] = data
+            else:
+                raise EngineException("userDefinedData should be an object, but is '{}' instead"
+                                      .format(type(data)),
+                                      http_code=HTTPStatus.BAD_REQUEST)
+        if ("operationalState" in indata["_admin"] and
+                content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
+            raise EngineException("nsdOperationalState already {}".format(content["_admin"]["operationalState"]),
+                                  http_code=HTTPStatus.CONFLICT)
         return indata
 
     def _check_descriptor_dependencies(self, session, descriptor):
@@ -735,36 +923,42 @@ class NsdTopic(DescriptorTopic):
         """
         if session["force"]:
             return
+        member_vnfd_index = self._get_descriptor_constituent_vnfds_by_member_vnfd_index(session, descriptor)
+
+        # Cross references validation in the descriptor and vnfd connection point validation
+        for vld in get_iterable(descriptor.get("vld")):
+            self.validate_vld_connection_point_refs_vnfd_connection_points(vld, member_vnfd_index)
+
+    def _get_descriptor_constituent_vnfds_by_member_vnfd_index(self, session, descriptor):
         member_vnfd_index = {}
         if descriptor.get("constituent-vnfd") and not session["force"]:
             for vnf in descriptor["constituent-vnfd"]:
                 vnfd_id = vnf["vnfd-id-ref"]
-                filter_q = self._get_project_filter(session)
-                filter_q["id"] = vnfd_id
-                vnf_list = self.db.get_list("vnfds", filter_q)
+                query_filter = self._get_project_filter(session)
+                query_filter["id"] = vnfd_id
+                vnf_list = self.db.get_list("vnfds", query_filter)
                 if not vnf_list:
                     raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
                                           "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
-                # elif len(vnf_list) > 1:
-                #     raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
-                #                           http_code=HTTPStatus.CONFLICT)
+
                 member_vnfd_index[vnf["member-vnf-index"]] = vnf_list[0]
+        return member_vnfd_index
 
-        # Cross references validation in the descriptor and vnfd connection point validation
-        for vld in get_iterable(descriptor.get("vld")):
-            for referenced_vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
-                # look if this vnfd contains this connection point
-                vnfd = member_vnfd_index.get(referenced_vnfd_cp["member-vnf-index-ref"])
-                for vnfd_cp in get_iterable(vnfd.get("connection-point")):
-                    if referenced_vnfd_cp.get("vnfd-connection-point-ref") == vnfd_cp["name"]:
-                        break
-                else:
-                    raise EngineException(
-                        "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
-                        "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
-                        .format(vld["id"], referenced_vnfd_cp["member-vnf-index-ref"],
-                                referenced_vnfd_cp["vnfd-connection-point-ref"], vnfd["id"]),
-                        http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+    @staticmethod
+    def validate_vld_connection_point_refs_vnfd_connection_points(vld, member_vnfd_index):
+        for referenced_vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
+            # look if this vnfd contains this connection point
+            vnfd = member_vnfd_index.get(referenced_vnfd_cp["member-vnf-index-ref"])
+            for vnfd_cp in get_iterable(vnfd.get("connection-point")):
+                if referenced_vnfd_cp.get("vnfd-connection-point-ref") == vnfd_cp["name"]:
+                    break
+            else:
+                raise EngineException(
+                    "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
+                    "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
+                    .format(vld["id"], referenced_vnfd_cp["member-vnf-index-ref"],
+                            referenced_vnfd_cp["vnfd-connection-point-ref"], vnfd["id"]),
+                    http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
 
     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
         super().check_conflict_on_edit(session, final_content, edit_content, _id)
@@ -800,10 +994,23 @@ class NsdTopic(DescriptorTopic):
             raise EngineException("There is at least one NetSlice Template referencing this descriptor",
                                   http_code=HTTPStatus.CONFLICT)
 
+    def sol005_projection(self, data):
+        data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
+        data["nsdOperationalState"] = data["_admin"]["operationalState"]
+        data["nsdUsageState"] = data["_admin"]["usageState"]
+
+        links = {}
+        links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
+        links["nsd_content"] = {"href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])}
+        data["_links"] = links
+
+        return super().sol005_projection(data)
+
 
 class NstTopic(DescriptorTopic):
     topic = "nsts"
     topic_msg = "nst"
+    quota_name = "slice_templates"
 
     def __init__(self, db, fs, msg, auth):
         DescriptorTopic.__init__(self, db, fs, msg, auth)
@@ -824,11 +1031,10 @@ class NstTopic(DescriptorTopic):
             clean_indata = clean_indata['nst:nst'][0]
         return clean_indata
 
-    def _validate_input_edit(self, indata, force=False):
-        # TODO validate with pyangbind, serialize
-        return indata
-
     def _validate_input_new(self, indata, storage_params, force=False):
+        indata.pop("onboardingState", None)
+        indata.pop("operationalState", None)
+        indata.pop("usageState", None)
         indata = self.pyangbind_validation("nsts", indata, force)
         return indata.copy()
 
@@ -873,10 +1079,23 @@ class NstTopic(DescriptorTopic):
             raise EngineException("there is at least one Netslice Instance using this descriptor",
                                   http_code=HTTPStatus.CONFLICT)
 
+    def sol005_projection(self, data):
+        data["onboardingState"] = data["_admin"]["onboardingState"]
+        data["operationalState"] = data["_admin"]["operationalState"]
+        data["usageState"] = data["_admin"]["usageState"]
+
+        links = {}
+        links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
+        links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
+        data["_links"] = links
+
+        return super().sol005_projection(data)
+
 
 class PduTopic(BaseTopic):
     topic = "pdus"
     topic_msg = "pdu"
+    quota_name = "pduds"
     schema_new = pdu_new_schema
     schema_edit = pdu_edit_schema
 
@@ -905,3 +1124,97 @@ class PduTopic(BaseTopic):
         _filter["vdur.pdu-id"] = _id
         if self.db.get_list("vnfrs", _filter):
             raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
+
+
+class VnfPkgOpTopic(BaseTopic):
+    topic = "vnfpkgops"
+    topic_msg = "vnfd"
+    schema_new = vnfpkgop_new_schema
+    schema_edit = None
+
+    def __init__(self, db, fs, msg, auth):
+        BaseTopic.__init__(self, db, fs, msg, auth)
+
+    def edit(self, session, _id, indata=None, kwargs=None, content=None):
+        raise EngineException("Method 'edit' not allowed for topic '{}'".format(self.topic),
+                              HTTPStatus.METHOD_NOT_ALLOWED)
+
+    def delete(self, session, _id, dry_run=False):
+        raise EngineException("Method 'delete' not allowed for topic '{}'".format(self.topic),
+                              HTTPStatus.METHOD_NOT_ALLOWED)
+
+    def delete_list(self, session, filter_q=None):
+        raise EngineException("Method 'delete_list' not allowed for topic '{}'".format(self.topic),
+                              HTTPStatus.METHOD_NOT_ALLOWED)
+
+    def new(self, rollback, session, indata=None, kwargs=None, headers=None):
+        """
+        Creates a new entry into database.
+        :param rollback: list to append created items at database in case a rollback may to be done
+        :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param headers: http request headers
+        :return: _id, op_id:
+            _id: identity of the inserted data.
+             op_id: None
+        """
+        self._update_input_with_kwargs(indata, kwargs)
+        validate_input(indata, self.schema_new)
+        vnfpkg_id = indata["vnfPkgId"]
+        filter_q = BaseTopic._get_project_filter(session)
+        filter_q["_id"] = vnfpkg_id
+        vnfd = self.db.get_one("vnfds", filter_q)
+        operation = indata["lcmOperationType"]
+        kdu_name = indata["kdu_name"]
+        for kdu in vnfd.get("kdu", []):
+            if kdu["name"] == kdu_name:
+                helm_chart = kdu.get("helm-chart")
+                juju_bundle = kdu.get("juju-bundle")
+                break
+        else:
+            raise EngineException("Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name))
+        if helm_chart:
+            indata["helm-chart"] = helm_chart
+            match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
+            repo_name = match.group(1) if match else None
+        elif juju_bundle:
+            indata["juju-bundle"] = juju_bundle
+            match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
+            repo_name = match.group(1) if match else None
+        else:
+            raise EngineException("Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']"
+                                  .format(vnfpkg_id, kdu_name))
+        if repo_name:
+            del filter_q["_id"]
+            filter_q["name"] = repo_name
+            repo = self.db.get_one("k8srepos", filter_q)
+            k8srepo_id = repo.get("_id")
+            k8srepo_url = repo.get("url")
+        else:
+            k8srepo_id = None
+            k8srepo_url = None
+        indata["k8srepoId"] = k8srepo_id
+        indata["k8srepo_url"] = k8srepo_url
+        vnfpkgop_id = str(uuid4())
+        vnfpkgop_desc = {
+            "_id": vnfpkgop_id,
+            "operationState": "PROCESSING",
+            "vnfPkgId": vnfpkg_id,
+            "lcmOperationType": operation,
+            "isAutomaticInvocation": False,
+            "isCancelPending": False,
+            "operationParams": indata,
+            "links": {
+                "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
+                "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
+            }
+        }
+        self.format_on_new(vnfpkgop_desc, session["project_id"], make_public=session["public"])
+        ctime = vnfpkgop_desc["_admin"]["created"]
+        vnfpkgop_desc["statusEnteredTime"] = ctime
+        vnfpkgop_desc["startTime"] = ctime
+        self.db.create(self.topic, vnfpkgop_desc)
+        rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
+        self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
+        return vnfpkgop_id, None