X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fdescriptor_topics.py;h=3bdaa03381bec85df94ddfe3d508776209790ff5;hp=eb6a46d1d4beee73984802265d3238093c1ab6ed;hb=960531ab7417bf6705399dae790899463a659da9;hpb=bdebce96965945c2ce86d80c60c17091c1a7fd42 diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index eb6a46d..3bdaa03 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -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 " 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,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) @@ -120,25 +147,27 @@ class DescriptorTopic(BaseTopic): :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, None - 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): """ @@ -210,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 @@ -261,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" @@ -277,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 @@ -332,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'): @@ -340,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 @@ -348,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: @@ -389,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): @@ -465,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("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("mgmt-interface:cp='{}' must match an existing connection-point" - .format(indata["mgmt-interface"]["cp"]), + 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")): @@ -552,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 @@ -587,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")): @@ -625,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): @@ -676,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): @@ -716,41 +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"]) - 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) @@ -786,13 +994,26 @@ 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): - 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): @@ -810,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() @@ -854,20 +1074,33 @@ class NstTopic(DescriptorTopic): return # Get Network Slice Template from Database _filter = self._get_project_filter(session) - _filter["nst-id"] = _id + _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): @@ -891,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