adding pyangbind validation at VNFD/NSD edition
[osm/NBI.git] / osm_nbi / descriptor_topics.py
index 6ecf7dc..2fd7cc0 100644 (file)
@@ -9,6 +9,10 @@ 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 osm_im.vnfd import vnfd as vnfd_im
+from osm_im.nsd import nsd as nsd_im
+from pyangbind.lib.serialise import pybindJSONDecoder
+import pyangbind.lib.pybindJSON as pybindJSON
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
@@ -19,17 +23,29 @@ class DescriptorTopic(BaseTopic):
         BaseTopic.__init__(self, db, fs, msg)
 
     def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False):
-        # check that this id is not present
-        _filter = {"id": final_content["id"]}
-        if _id:
+        # 1. validate again with pyangbind
+        # 1.1. remove internal keys
+        internal_keys = {}
+        for k in ("_id", "_admin"):
+            if k in final_content:
+                internal_keys[k] = final_content.pop(k)
+        serialized = self._validate_input_new(final_content, force)
+        # 1.2. modify final_content with a serialized version
+        final_content.clear()
+        final_content.update(serialized)
+        # 1.3. restore internal keys
+        for k, v in internal_keys.items():
+            final_content[k] = v
+
+        # 2. check that this id is not present
+        if "id" in edit_content:
+            _filter = self._get_project_filter(session, write=False, show_all=False)
+            _filter["id"] = final_content["id"]
             _filter["_id.neq"] = _id
-
-        _filter.update(self._get_project_filter(session, write=False, show_all=False))
-        if self.db.get_one(self.topic, _filter, fail_on_empty=False):
-            raise EngineException("{} with id '{}' already exists for this project".format(self.topic[:-1],
-                                                                                           final_content["id"]),
-                                  HTTPStatus.CONFLICT)
-        # TODO validate with pyangbind. Load and dumps to convert data types
+            if self.db.get_one(self.topic, _filter, fail_on_empty=False):
+                raise EngineException("{} with id '{}' already exists for this project".format(self.topic[:-1],
+                                                                                               final_content["id"]),
+                                      HTTPStatus.CONFLICT)
 
     @staticmethod
     def format_on_new(content, project_id=None, make_public=False):
@@ -335,6 +351,29 @@ class DescriptorTopic(BaseTopic):
                                       "future versions", http_code=HTTPStatus.NOT_ACCEPTABLE)
             return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
 
+    def pyangbind_validation(self, item, data, force=False):
+        try:
+            if item == "vnfds":
+                myvnfd = vnfd_im()
+                pybindJSONDecoder.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data]}}, None, None, obj=myvnfd,
+                                                 path_helper=True, skip_unknown=force)
+                out = pybindJSON.dumps(myvnfd, mode="ietf")
+            elif item == "nsds":
+                mynsd = nsd_im()
+                pybindJSONDecoder.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data]}}, None, None, obj=mynsd,
+                                                 path_helper=True, skip_unknown=force)
+                out = pybindJSON.dumps(mynsd, mode="ietf")
+            else:
+                raise EngineException("Not possible to validate '{}' item".format(item),
+                                      http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+            desc_out = self._remove_envelop(yaml.safe_load(out))
+            return desc_out
+
+        except Exception as e:
+            raise EngineException("Error in pyangbind validation: {}".format(str(e)),
+                                  http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+
 
 class VnfdTopic(DescriptorTopic):
     topic = "vnfds"
@@ -354,8 +393,12 @@ class VnfdTopic(DescriptorTopic):
             clean_indata = clean_indata['vnfd-catalog']
         if clean_indata.get('vnfd'):
             if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
-                raise EngineException("'vnfd' must be a list only one element")
+                raise EngineException("'vnfd' must be a list of only one element")
             clean_indata = clean_indata['vnfd'][0]
+        elif clean_indata.get('vnfd:vnfd'):
+            if not isinstance(clean_indata['vnfd:vnfd'], list) or len(clean_indata['vnfd:vnfd']) != 1:
+                raise EngineException("'vnfd:vnfd' must be a list of only one element")
+            clean_indata = clean_indata['vnfd:vnfd'][0]
         return clean_indata
 
     def check_conflict_on_del(self, session, _id, force=False):
@@ -387,44 +430,42 @@ class VnfdTopic(DescriptorTopic):
             raise EngineException("There is soame NSD that depends on this VNFD", http_code=HTTPStatus.CONFLICT)
 
     def _validate_input_new(self, indata, force=False):
-        # TODO validate with pyangbind, serialize
-
+        indata = self.pyangbind_validation("vnfds", indata, force)
         # Cross references validation in the descriptor
-        if indata.get("mgmt-interface"):
-            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"].get("cp")),
-                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-        else:
+        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)
 
         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.get("external-connection-point-ref"):
+                        if cp["name"] == interface["external-connection-point-ref"]:
                             break
                     else:
-                        raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{} "
+                        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)
-            for interface in get_iterable(vdu.get("interface")):
-                if interface.get("internal-connection-point-ref"):
+
+                elif interface.get("internal-connection-point-ref"):
                     for internal_cp in get_iterable(vdu.get("internal-connection-point")):
-                        if interface.get("internal-connection-point-ref") == internal_cp.get("id"):
+                        if interface["internal-connection-point-ref"] == internal_cp.get("id"):
                             break
                     else:
-                        raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' is "
-                                              "not associated to an internal-connection-point".format
-                                              (vdu["id"], interface["name"],
-                                               interface["internal-connection-point-ref"]),
+                        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 ivld in get_iterable(indata.get("internal-vld")):
             for icp in get_iterable(ivld.get("internal-connection-point")):
@@ -437,15 +478,15 @@ class VnfdTopic(DescriptorTopic):
                     if icp_mark:
                         break
                 else:
-                    raise EngineException("'internal-vld='{}':internal-connection-point='{}'' must match an existing "
-                                          "internal-connection-point".format(ivld["id"], icp["id-ref"]),
+                    raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
+                                          "vdu:internal-connection-point".format(ivld["id"], icp["id-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
             if ivld.get("ip-profile-ref"):
-                for ip_prof in get_iterable(indata["ip-profiles"]):
+                for ip_prof in get_iterable(indata.get("ip-profiles")):
                     if ip_prof["name"] == get_iterable(ivld.get("ip-profile-ref")):
                         break
                 else:
-                    raise EngineException("internal-vld='{}':ip-profile-ref='{}' does not exist".format(
+                    raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
                         ivld["id"], ivld["ip-profile-ref"]),
                         http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
         for mp in get_iterable(indata.get("monitoring-param")):
@@ -453,7 +494,7 @@ class VnfdTopic(DescriptorTopic):
                 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"]["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
@@ -461,7 +502,7 @@ class VnfdTopic(DescriptorTopic):
                         break
                 else:
                     raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
-                                          "defined in VDU vdu[id='{}'] or does not exist an VDU with this id"
+                                          "defined at vdu[id='{}'] or vdu does not exist"
                                           .format(mp["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
                                                   mp["vdu-monitoring-param"]["vdu-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
@@ -477,8 +518,8 @@ class VnfdTopic(DescriptorTopic):
                         if mp_vm_mark:
                             break
                 else:
-                    raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined in VDU "
-                                          "vdu[id='{}'] or or does not exist an VDU with this id"
+                    raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
+                                          "vdu[id='{}'] or vdu does not exist"
                                           .format(mp["vdu-metric"]["vdu-metric-name-ref"],
                                                   mp["vdu-metric"]["vdu-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
@@ -490,9 +531,9 @@ class VnfdTopic(DescriptorTopic):
                         if mp["id"] == get_iterable(sc.get("vnf-monitoring-param-ref")):
                             break
                     else:
-                        raise EngineException("scaling-group-descriptor:name='{}':scaling-criteria:name='{}':"
-                                              "vnf-monitoring-param-ref='{}' not defined in any 'monitoring-param"
-                                              "['id']'".format(sgd["name"], sc["name"], sc["vnf-monitoring-param-ref"]),
+                        raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
+                                              "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
+                                              .format(sgd["name"], sc["name"], sc["vnf-monitoring-param-ref"]),
                                               http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
             for sgd_vdu in get_iterable(sgd.get("vdu")):
                 sgd_vdu_mark = False
@@ -503,32 +544,29 @@ class VnfdTopic(DescriptorTopic):
                 if sgd_vdu_mark:
                     break
             else:
-                raise EngineException("scaling-group-descriptor[name='{}']:vdu not defined and"
-                                      "it is mandatory in any 'scaling-group-descriptor'"
-                                      .format(sgd["name"]),
+                raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
+                                      .format(sgd["name"], sgd_vdu["vdu-id-ref"]),
                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
             for sca in get_iterable(sgd.get("scaling-config-action")):
-                sca_vcp_mark = False
-                if indata.get("vnf-configuration"):
-                    for primitive in get_iterable(indata["vnf-configuration"].get("config-primitive")):
-                        if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
-                            sca_vcp_mark = True
-                            break
-                    else:
-                        raise EngineException("scaling-group-descriptor:name='{}':scaling-config-action:vnf-config-"
-                                              "primitive-name-ref='{}' not defined in "
-                                              "'vnf-configuration:config-primitive'"
-                                              .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
-                                              http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+                if not indata.get("vnf-configuration"):
+                    raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
+                                          "scaling-group-descriptor[name='{}']:scaling-config-action"
+                                          .format(sgd["name"]),
+                                          http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
+                for primitive in get_iterable(indata["vnf-configuration"].get("config-primitive")):
+                    if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
+                        break
                 else:
-                    raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced",
+                    raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
+                                          "primitive-name-ref='{}' does not match any "
+                                          "vnf-configuration:config-primitive:name"
+                                          .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
-                if sca_vcp_mark:
-                    break
+        # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
         return indata
 
     def _validate_input_edit(self, indata, force=False):
-        # TODO validate with pyangbind, serialize
+        # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
         return indata
 
 
@@ -551,22 +589,21 @@ class NsdTopic(DescriptorTopic):
             clean_indata = clean_indata['nsd-catalog']
         if clean_indata.get('nsd'):
             if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
-                raise EngineException("'nsd' must be a list only one element")
+                raise EngineException("'nsd' must be a list of only one element")
             clean_indata = clean_indata['nsd'][0]
+        elif clean_indata.get('nsd:nsd'):
+            if not isinstance(clean_indata['nsd:nsd'], list) or len(clean_indata['nsd:nsd']) != 1:
+                raise EngineException("'nsd:nsd' must be a list of only one element")
+            clean_indata = clean_indata['nsd:nsd'][0]
         return clean_indata
 
     def _validate_input_new(self, indata, force=False):
-        # transform constituent-vnfd:member-vnf-index to string
-        if indata.get("constituent-vnfd"):
-            for constituent_vnfd in indata["constituent-vnfd"]:
-                if "member-vnf-index" in constituent_vnfd:
-                    constituent_vnfd["member-vnf-index"] = str(constituent_vnfd["member-vnf-index"])
-
-        # TODO validate with pyangbind, serialize
+        indata = self.pyangbind_validation("nsds", indata, force)
+        # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
         return indata
 
     def _validate_input_edit(self, indata, force=False):
-        # TODO validate with pyangbind, serialize
+        # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
         return indata
 
     def _check_descriptor_dependencies(self, session, descriptor):
@@ -589,7 +626,8 @@ class NsdTopic(DescriptorTopic):
     def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False):
         super().check_conflict_on_edit(session, final_content, edit_content, _id, force=force)
 
-        self._check_descriptor_dependencies(session, final_content)
+        if not force:
+            self._check_descriptor_dependencies(session, final_content)
 
     def check_conflict_on_del(self, session, _id, force=False):
         """