Feature 8532: Added new plugin authconn tacacs
[osm/NBI.git] / osm_nbi / descriptor_topics.py
index c533054..3bdaa03 100644 (file)
@@ -20,8 +20,12 @@ import json
 from hashlib import md5
 from osm_common.dbbase import DbException, deep_update_rfc7396
 from http import HTTPStatus
-from validation import ValidationError, pdu_new_schema, pdu_edit_schema
-from base_topic import BaseTopic, EngineException, get_iterable
+from time import time
+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
 from osm_im.nst import nst as nst_im
@@ -33,11 +37,33 @@ __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
 class DescriptorTopic(BaseTopic):
 
-    def __init__(self, db, fs, msg):
-        BaseTopic.__init__(self, db, fs, msg)
+    def __init__(self, db, fs, msg, auth):
+        BaseTopic.__init__(self, db, fs, msg, auth)
 
     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 = {}
@@ -72,7 +98,15 @@ class DescriptorTopic(BaseTopic):
         content["_admin"]["operationalState"] = "DISABLED"
         content["_admin"]["usageState"] = "NOT_IN_USE"
 
-    def delete_extra(self, session, _id):
+    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)
         self.fs.file_delete(_id + "_", ignore_non_exist=True)  # remove temp folder
 
@@ -110,28 +144,30 @@ class DescriptorTopic(BaseTopic):
         :param indata: data to be inserted
         :param kwargs: used to override the indata descriptor
         :param headers: http request headers
-        :return: _id: identity of the inserted data.
+        :return: _id, None: identity of the inserted data; and None as there is not any operation
         """
 
-        try:
-            # _remove_envelop
-            if indata:
-                if "userDefinedData" in indata:
-                    indata = indata['userDefinedData']
+        # No needed to capture exceptions
+        # Check Quota
+        self.check_quota(session)
 
-            # 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})
-            return _id
-        except ValidationError as e:
-            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+        # _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
 
     def upload_content(self, session, _id, indata, kwargs, headers):
         """
@@ -203,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
@@ -254,7 +290,7 @@ class DescriptorTopic(BaseTopic):
                 indata = json.load(content)
             else:
                 error_text = "Invalid yaml format "
-                indata = yaml.load(content)
+                indata = yaml.load(content, Loader=yaml.SafeLoader)
 
             current_desc["_admin"]["storage"] = storage
             current_desc["_admin"]["onboardingState"] = "ONBOARDED"
@@ -270,11 +306,12 @@ class DescriptorTopic(BaseTopic):
 
             deep_update_rfc7396(current_desc, indata)
             self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
+            current_desc["_admin"]["modified"] = time()
             self.db.replace(self.topic, _id, current_desc)
             self.fs.dir_rename(temp_folder, _id)
 
             indata["_id"] = _id
-            self._send_msg("created", indata)
+            self._send_msg("edited", indata)
 
             # TODO if descriptor has changed because kwargs update content and remove cached zip
             # TODO if zip is not present creates one
@@ -325,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'):
@@ -333,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
@@ -341,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:
@@ -382,13 +424,46 @@ 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"
     topic_msg = "vnfd"
 
-    def __init__(self, db, fs, msg):
-        DescriptorTopic.__init__(self, db, fs, msg)
+    def __init__(self, db, fs, msg, auth):
+        DescriptorTopic.__init__(self, db, fs, msg, auth)
 
     @staticmethod
     def _remove_envelop(indata=None):
@@ -426,100 +501,168 @@ class VnfdTopic(DescriptorTopic):
             final_content["_admin"]["type"] = "vnfd"
         # if neither vud nor pdu do not fill type
 
-    def check_conflict_on_del(self, session, _id):
+    def check_conflict_on_del(self, session, _id, db_content):
         """
         Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
         that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
         that uses this vnfd
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
-        :param _id: vnfd inernal id
+        :param _id: vnfd internal id
+        :param db_content: The database content of the _id.
         :return: None or raises EngineException with the conflict
         """
         if session["force"]:
             return
-        descriptor = self.db.get_one("vnfds", {"_id": _id})
+        descriptor = db_content
         descriptor_id = descriptor.get("id")
         if not descriptor_id:  # empty vnfd not uploaded
             return
 
         _filter = self._get_project_filter(session)
+
         # check vnfrs using this vnfd
         _filter["vnfd-id"] = _id
         if self.db.get_list("vnfrs", _filter):
-            raise EngineException("There is some VNFR that depends on this VNFD", http_code=HTTPStatus.CONFLICT)
+            raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
+
+        # check NSD referencing this VNFD
         del _filter["vnfd-id"]
-        # check NSD using this VNFD
         _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
         if self.db.get_list("nsds", _filter):
-            raise EngineException("There is at least a NSD that depends on this VNFD", http_code=HTTPStatus.CONFLICT)
+            raise EngineException("There is at least one NSD referencing this descriptor",
+                                  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")):
@@ -541,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
@@ -576,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")):
@@ -614,35 +762,40 @@ 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):
     topic = "nsds"
     topic_msg = "nsd"
 
-    def __init__(self, db, fs, msg):
-        DescriptorTopic.__init__(self, db, fs, msg)
+    def __init__(self, db, fs, msg, auth):
+        DescriptorTopic.__init__(self, db, fs, msg, auth)
 
     @staticmethod
     def _remove_envelop(indata=None):
@@ -665,34 +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)
+            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")):
+            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):
@@ -705,69 +923,97 @@ 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"])
-                if not vnfd:
-                    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"], referenced_vnfd_cp["member-vnf-index-ref"]),
-                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-                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)
 
         self._check_descriptor_dependencies(session, final_content)
 
-    def check_conflict_on_del(self, session, _id):
+    def check_conflict_on_del(self, session, _id, db_content):
         """
         Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
         that NSD can be public and be used by other projects.
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
-        :param _id: vnfd inernal id
+        :param _id: nsd internal id
+        :param db_content: The database content of the _id
         :return: None or raises EngineException with the conflict
         """
         if session["force"]:
             return
+        descriptor = db_content
+        descriptor_id = descriptor.get("id")
+        if not descriptor_id:  # empty nsd not uploaded
+            return
+
+        # check NSD used by NS
         _filter = self._get_project_filter(session)
-        _filter["nsdId"] = _id
+        _filter["nsd-id"] = _id
         if self.db.get_list("nsrs", _filter):
-            raise EngineException("There is some NSR that depends on this NSD", http_code=HTTPStatus.CONFLICT)
+            raise EngineException("There is at least one NS using this descriptor", http_code=HTTPStatus.CONFLICT)
+
+        # check NSD referenced by NST
+        del _filter["nsd-id"]
+        _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
+        if self.db.get_list("nsts", _filter):
+            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):
-        DescriptorTopic.__init__(self, db, fs, msg)
+    def __init__(self, db, fs, msg, auth):
+        DescriptorTopic.__init__(self, db, fs, msg, auth)
 
     @staticmethod
     def _remove_envelop(indata=None):
@@ -785,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()
 
@@ -815,12 +1060,13 @@ class NstTopic(DescriptorTopic):
 
         self._check_descriptor_dependencies(session, final_content)
 
-    def check_conflict_on_del(self, session, _id):
+    def check_conflict_on_del(self, session, _id, db_content):
         """
         Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
         that NST can be public and be used by other projects.
         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
         :param _id: nst internal id
+        :param db_content: The database content of the _id.
         :return: None or raises EngineException with the conflict
         """
         # TODO: Check this method
@@ -828,26 +1074,33 @@ class NstTopic(DescriptorTopic):
             return
         # Get Network Slice Template from Database
         _filter = self._get_project_filter(session)
-        _filter["_id"] = _id
-        nst = self.db.get_one("nsts", _filter)
-        
-        # Search NSIs using NST via nst-ref
-        _filter = self._get_project_filter(session)
-        _filter["nst-ref"] = nst["id"]
-        nsis_list = self.db.get_list("nsis", _filter)
-        for nsi_item in nsis_list:
-            if nsi_item["_admin"].get("nsiState") != "TERMINATED":
-                raise EngineException("There is some NSIS that depends on this NST", http_code=HTTPStatus.CONFLICT)
+        _filter["_admin.nst-id"] = _id
+        if self.db.get_list("nsis", _filter):
+            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
 
-    def __init__(self, db, fs, msg):
-        BaseTopic.__init__(self, db, fs, msg)
+    def __init__(self, db, fs, msg, auth):
+        BaseTopic.__init__(self, db, fs, msg, auth)
 
     @staticmethod
     def format_on_new(content, project_id=None, make_public=False):
@@ -856,10 +1109,112 @@ class PduTopic(BaseTopic):
         content["_admin"]["operationalState"] = "ENABLED"
         content["_admin"]["usageState"] = "NOT_IN_USE"
 
-    def check_conflict_on_del(self, session, _id):
+    def check_conflict_on_del(self, session, _id, db_content):
+        """
+        Check that there is not any vnfr that uses this PDU
+        :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
+        :param _id: pdu internal id
+        :param db_content: The database content of the _id.
+        :return: None or raises EngineException with the conflict
+        """
         if session["force"]:
             return
-        # TODO Is it needed to check descriptors _admin.project_read/project_write??
-        _filter = {"vdur.pdu-id": _id}
+
+        _filter = self._get_project_filter(session)
+        _filter["vdur.pdu-id"] = _id
         if self.db.get_list("vnfrs", _filter):
-            raise EngineException("There is some NSR that uses this PDU", http_code=HTTPStatus.CONFLICT)
+            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