X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fdescriptor_topics.py;h=24f47a9bf9494cec265bde6fba2e5899df227952;hp=95ced0d62f93c402ebeb2d3459d270f9bcab212b;hb=ee00275276c75cb1737539b48db33952177ec777;hpb=b19cadcf9ff2169f34f6db79b866bad91f9c8aed diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index 95ced0d..24f47a9 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -20,7 +20,11 @@ import json from hashlib import md5 from osm_common.dbbase import DbException, deep_update_rfc7396 from http import HTTPStatus -from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema +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 @@ -38,6 +42,27 @@ class DescriptorTopic(BaseTopic): def check_conflict_on_edit(self, session, final_content, edit_content, _id): super().check_conflict_on_edit(session, final_content, edit_content, _id) + + def _check_unique_id_name(descriptor, position=""): + for desc_key, desc_item in descriptor.items(): + if isinstance(desc_item, list) and desc_item: + used_ids = [] + desc_item_id = None + for index, list_item in enumerate(desc_item): + if isinstance(list_item, dict): + _check_unique_id_name(list_item, "{}.{}[{}]" + .format(position, desc_key, index)) + # Base case + if index == 0 and (list_item.get("id") or list_item.get("name")): + desc_item_id = "id" if list_item.get("id") else "name" + if desc_item_id and list_item.get(desc_item_id): + if list_item[desc_item_id] in used_ids: + position = "{}.{}[{}]".format(position, desc_key, index) + raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'" + .format(desc_item_id, list_item[desc_item_id], + position), HTTPStatus.UNPROCESSABLE_ENTITY) + used_ids.append(list_item[desc_item_id]) + _check_unique_id_name(final_content) # 1. validate again with pyangbind # 1.1. remove internal keys internal_keys = {} @@ -72,12 +97,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,28 +146,27 @@ class DescriptorTopic(BaseTopic): :return: _id, None: identity of the inserted data; and None as there is not any operation """ - try: - # Check Quota - self.check_quota(session) + # No needed to capture exceptions + # Check Quota + self.check_quota(session) - # _remove_envelop - if indata: - if "userDefinedData" in indata: - indata = indata['userDefinedData'] + # _remove_envelop + if indata: + if "userDefinedData" in indata: + indata = indata['userDefinedData'] - # Override descriptor with query string kwargs - self._update_input_with_kwargs(indata, kwargs) - # uncomment when this method is implemented. - # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors - # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"]) - - content = {"_admin": {"userDefinedData": indata}} - self.format_on_new(content, session["project_id"], make_public=session["public"]) - _id = self.db.create(self.topic, content) - rollback.append({"topic": self.topic, "_id": _id}) - return _id, None - except ValidationError as e: - raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) + # Override descriptor with query string kwargs + self._update_input_with_kwargs(indata, kwargs) + # uncomment when this method is implemented. + # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors + # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"]) + + content = {"_admin": {"userDefinedData": indata}} + self.format_on_new(content, session["project_id"], make_public=session["public"]) + _id = self.db.create(self.topic, content) + rollback.append({"topic": self.topic, "_id": _id}) + self._send_msg("created", {"_id": _id}) + return _id, None def upload_content(self, session, _id, indata, kwargs, headers): """ @@ -280,11 +305,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 @@ -351,10 +377,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: @@ -392,6 +423,39 @@ class DescriptorTopic(BaseTopic): raise EngineException("Error in pyangbind validation: {}".format(str(e)), http_code=HTTPStatus.UNPROCESSABLE_ENTITY) + def _validate_input_edit(self, indata, content, force=False): + # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit + if "_id" in indata: + indata.pop("_id") + if "_admin" not in indata: + indata["_admin"] = {} + + if "operationalState" in indata: + if indata["operationalState"] in ("ENABLED", "DISABLED"): + indata["_admin"]["operationalState"] = indata.pop("operationalState") + else: + raise EngineException("State '{}' is not a valid operational state" + .format(indata["operationalState"]), + http_code=HTTPStatus.BAD_REQUEST) + + # In the case of user defined data, we need to put the data in the root of the object + # to preserve current expected behaviour + if "userDefinedData" in indata: + data = indata.pop("userDefinedData") + if type(data) == dict: + indata["_admin"]["userDefinedData"] = data + else: + raise EngineException("userDefinedData should be an object, but is '{}' instead" + .format(type(data)), + http_code=HTTPStatus.BAD_REQUEST) + + if ("operationalState" in indata["_admin"] and + content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]): + raise EngineException("operationalState already {}".format(content["_admin"]["operationalState"]), + http_code=HTTPStatus.CONFLICT) + + return indata + class VnfdTopic(DescriptorTopic): topic = "vnfds" @@ -468,6 +532,12 @@ 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"): @@ -484,8 +554,17 @@ class VnfdTopic(DescriptorTopic): http_code=HTTPStatus.UNPROCESSABLE_ENTITY) for vdu in get_iterable(indata.get("vdu")): + 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"] == interface["external-connection-point-ref"]: break @@ -495,8 +574,14 @@ class VnfdTopic(DescriptorTopic): .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 @@ -526,13 +611,14 @@ class VnfdTopic(DescriptorTopic): 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: - 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) + ivld_name = ivld.get("name") + if ivld_name: + if 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 @@ -630,10 +716,6 @@ class VnfdTopic(DescriptorTopic): 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 _validate_package_folders(self, storage_params, folder, file=None): if not storage_params or not storage_params.get("pkg-dir"): return False @@ -650,6 +732,32 @@ class VnfdTopic(DescriptorTopic): return True return False + 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 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" @@ -679,6 +787,12 @@ 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 @@ -703,10 +817,56 @@ class NsdTopic(DescriptorTopic): "does not match any constituent-vnfd:member-vnf-index" .format(vld["id"], vnfd_cp["member-vnf-index-ref"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY) + # Check VNFFGD + for fgd in get_iterable(indata.get("vnffgd")): + for cls in get_iterable(fgd.get("classifier")): + rspref = cls.get("rsp-id-ref") + for rsp in get_iterable(fgd.get("rsp")): + rspid = rsp.get("id") + if rspid and rspref and rspid == rspref: + break + else: + raise EngineException( + "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id" + .format(fgd["id"], cls["id"], rspref), + http_code=HTTPStatus.UNPROCESSABLE_ENTITY) return indata - def _validate_input_edit(self, indata, force=False): + 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): @@ -739,11 +899,6 @@ class NsdTopic(DescriptorTopic): 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 @@ -788,11 +943,24 @@ class NsdTopic(DescriptorTopic): 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, auth): DescriptorTopic.__init__(self, db, fs, msg, auth) @@ -813,11 +981,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() @@ -862,10 +1029,23 @@ class NstTopic(DescriptorTopic): raise EngineException("there is at least one Netslice Instance using this descriptor", http_code=HTTPStatus.CONFLICT) + def sol005_projection(self, data): + data["onboardingState"] = data["_admin"]["onboardingState"] + data["operationalState"] = data["_admin"]["operationalState"] + data["usageState"] = data["_admin"]["usageState"] + + links = {} + links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])} + links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])} + data["_links"] = links + + return super().sol005_projection(data) + class PduTopic(BaseTopic): topic = "pdus" topic_msg = "pdu" + quota_name = "pduds" schema_new = pdu_new_schema schema_edit = pdu_edit_schema @@ -894,3 +1074,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