X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fdescriptor_topics.py;h=ddec65cecc4e2299e043e7108df56a785ffd7161;hp=4661964a32581cec1d578dfb64b8a31618fb23f8;hb=bc5a52409c5d3690ebf6810a31662c0846847020;hpb=81b30e916f0cdbcfe0da4868325591c34fb5acb2 diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index 4661964..ddec65c 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -19,8 +19,10 @@ import json import copy import os import shutil +import functools # import logging +from deepdiff import DeepDiff from hashlib import md5 from osm_common.dbbase import DbException, deep_update_rfc7396 from http import HTTPStatus @@ -266,6 +268,8 @@ class DescriptorTopic(BaseTopic): # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266 file_pkg = None error_text = "" + fs_rollback = [] + try: if content_range_text: content_range = ( @@ -294,9 +298,10 @@ class DescriptorTopic(BaseTopic): else: self.fs.file_delete(proposed_revision_path, ignore_non_exist=True) self.fs.mkdir(proposed_revision_path) + fs_rollback.append(proposed_revision_path) storage = self.fs.get_params() - storage["folder"] = _id + storage["folder"] = proposed_revision_path file_path = (proposed_revision_path, filename) if self.fs.file_exists(file_path, "file"): @@ -462,17 +467,6 @@ class DescriptorTopic(BaseTopic): self.fs.file_delete(proposed_revision_path, ignore_non_exist=True) raise e - # Copy the revision to the active package name by its original id - shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True) - os.rename(self.fs.path + proposed_revision_path, self.fs.path + current_revision_path) - self.fs.file_delete(current_revision_path, ignore_non_exist=True) - self.fs.mkdir(current_revision_path) - self.fs.reverse_sync(from_path=current_revision_path) - shutil.rmtree(self.fs.path + _id) - - current_desc["_admin"]["storage"] = storage - current_desc["_admin"]["onboardingState"] = "ONBOARDED" - current_desc["_admin"]["operationalState"] = "ENABLED" indata = self._remove_envelop(indata) @@ -480,18 +474,33 @@ class DescriptorTopic(BaseTopic): if kwargs: self._update_input_with_kwargs(indata, kwargs) + current_desc["_admin"]["storage"] = storage + current_desc["_admin"]["onboardingState"] = "ONBOARDED" + current_desc["_admin"]["operationalState"] = "ENABLED" + current_desc["_admin"]["modified"] = time() + current_desc["_admin"]["revision"] = revision + deep_update_rfc7396(current_desc, indata) current_desc = self.check_conflict_on_edit( session, current_desc, indata, _id=_id ) - current_desc["_admin"]["modified"] = time() - current_desc["_admin"]["revision"] = revision + + # Copy the revision to the active package name by its original id + shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True) + os.rename(self.fs.path + proposed_revision_path, self.fs.path + current_revision_path) + self.fs.file_delete(current_revision_path, ignore_non_exist=True) + self.fs.mkdir(current_revision_path) + self.fs.reverse_sync(from_path=current_revision_path) + + shutil.rmtree(self.fs.path + _id) + self.db.replace(self.topic, _id, current_desc) # Store a copy of the package as a point in time revision revision_desc = dict(current_desc) revision_desc["_id"] = _id + ":" + str(revision_desc["_admin"]["revision"]) self.db.create(self.topic + "_revisions", revision_desc) + fs_rollback = [] indata["_id"] = _id self._send_msg("edited", indata) @@ -523,6 +532,8 @@ class DescriptorTopic(BaseTopic): finally: if file_pkg: file_pkg.close() + for file in fs_rollback: + self.fs.file_delete(file, ignore_non_exist=True) def get_file(self, session, _id, path=None, accept_header=None): """ @@ -1184,6 +1195,158 @@ class VnfdTopic(DescriptorTopic): return super().sol005_projection(data) + @staticmethod + def find_software_version(vnfd: dict) -> str: + """Find the sotware version in the VNFD descriptors + + Args: + vnfd (dict): Descriptor as a dictionary + + Returns: + software-version (str) + """ + default_sw_version = "1.0" + if vnfd.get("vnfd"): + vnfd = vnfd["vnfd"] + if vnfd.get("software-version"): + return vnfd["software-version"] + else: + return default_sw_version + + @staticmethod + def extract_policies(vnfd: dict) -> dict: + """Removes the policies from the VNFD descriptors + + Args: + vnfd (dict): Descriptor as a dictionary + + Returns: + vnfd (dict): VNFD which does not include policies + """ + # TODO: Extract the policy related parts from the VNFD + return vnfd + + @staticmethod + def extract_day12_primitives(vnfd: dict) -> dict: + """Removes the day12 primitives from the VNFD descriptors + + Args: + vnfd (dict): Descriptor as a dictionary + + Returns: + vnfd (dict) + """ + for df_id, df in enumerate(vnfd.get("df", {})): + if ( + df.get("lcm-operations-configuration", {}) + .get("operate-vnf-op-config", {}) + .get("day1-2") + ): + day12 = df["lcm-operations-configuration"]["operate-vnf-op-config"].get( + "day1-2" + ) + for config_id, config in enumerate(day12): + for key in [ + "initial-config-primitive", + "config-primitive", + "terminate-config-primitive", + ]: + config.pop(key, None) + day12[config_id] = config + df["lcm-operations-configuration"]["operate-vnf-op-config"][ + "day1-2" + ] = day12 + vnfd["df"][df_id] = df + return vnfd + + def remove_modifiable_items(self, vnfd: dict) -> dict: + """Removes the modifiable parts from the VNFD descriptors + + It calls different extract functions according to different update types + to clear all the modifiable items from VNFD + + Args: + vnfd (dict): Descriptor as a dictionary + + Returns: + vnfd (dict): Descriptor which does not include modifiable contents + """ + if vnfd.get("vnfd"): + vnfd = vnfd["vnfd"] + vnfd.pop("_admin", None) + # If the other extractions need to be done from VNFD, + # the new extract methods could be appended to below list. + for extract_function in [self.extract_day12_primitives, self.extract_policies]: + vnfd_temp = extract_function(vnfd) + vnfd = vnfd_temp + return vnfd + + def _validate_descriptor_changes( + self, + descriptor_file_name: str, + old_descriptor_directory: str, + new_descriptor_directory: str, + ): + """Compares the old and new VNFD descriptors and validates the new descriptor. + + Args: + old_descriptor_directory (str): Directory of descriptor which is in-use + new_descriptor_directory (str): Directory of directory which is proposed to update (new revision) + + Returns: + None + + Raises: + EngineException: In case of error when there are unallowed changes + """ + try: + with self.fs.file_open( + (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r" + ) as old_descriptor_file: + with self.fs.file_open( + (new_descriptor_directory, descriptor_file_name), "r" + ) as new_descriptor_file: + old_content = yaml.load( + old_descriptor_file.read(), Loader=yaml.SafeLoader + ) + new_content = yaml.load( + new_descriptor_file.read(), Loader=yaml.SafeLoader + ) + if old_content and new_content: + if self.find_software_version( + old_content + ) != self.find_software_version(new_content): + return + disallowed_change = DeepDiff( + self.remove_modifiable_items(old_content), + self.remove_modifiable_items(new_content), + ) + if disallowed_change: + changed_nodes = functools.reduce( + lambda a, b: a + " , " + b, + [ + node.lstrip("root") + for node in disallowed_change.get( + "values_changed" + ).keys() + ], + ) + raise EngineException( + f"Error in validating new descriptor: {changed_nodes} cannot be modified, " + "there are disallowed changes in the vnf descriptor.", + http_code=HTTPStatus.UNPROCESSABLE_ENTITY, + ) + except ( + DbException, + AttributeError, + IndexError, + KeyError, + ValueError, + ) as e: + raise type(e)( + "VNF Descriptor could not be processed with error: {}.".format(e) + ) + class NsdTopic(DescriptorTopic): topic = "nsds" @@ -1458,6 +1621,112 @@ class NsdTopic(DescriptorTopic): super().delete_extra(session, _id, db_content, not_send_msg) self.db.del_list(self.topic+"_revisions", { "_id": { "$regex": _id}}) + @staticmethod + def extract_day12_primitives(nsd: dict) -> dict: + """Removes the day12 primitives from the NSD descriptors + + Args: + nsd (dict): Descriptor as a dictionary + + Returns: + nsd (dict): Cleared NSD + """ + if nsd.get("ns-configuration"): + for key in [ + "config-primitive", + "initial-config-primitive", + "terminate-config-primitive", + ]: + nsd["ns-configuration"].pop(key, None) + return nsd + + def remove_modifiable_items(self, nsd: dict) -> dict: + """Removes the modifiable parts from the VNFD descriptors + + It calls different extract functions according to different update types + to clear all the modifiable items from NSD + + Args: + nsd (dict): Descriptor as a dictionary + + Returns: + nsd (dict): Descriptor which does not include modifiable contents + """ + while isinstance(nsd, dict) and nsd.get("nsd"): + nsd = nsd["nsd"] + if isinstance(nsd, list): + nsd = nsd[0] + nsd.pop("_admin", None) + # If the more extractions need to be done from NSD, + # the new extract methods could be appended to below list. + for extract_function in [self.extract_day12_primitives]: + nsd_temp = extract_function(nsd) + nsd = nsd_temp + return nsd + + def _validate_descriptor_changes( + self, + descriptor_file_name: str, + old_descriptor_directory: str, + new_descriptor_directory: str, + ): + """Compares the old and new NSD descriptors and validates the new descriptor + + Args: + old_descriptor_directory: Directory of descriptor which is in-use + new_descriptor_directory: Directory of directory which is proposed to update (new revision) + + Returns: + None + + Raises: + EngineException: In case of error if the changes are not allowed + """ + + try: + with self.fs.file_open( + (old_descriptor_directory, descriptor_file_name), "r" + ) as old_descriptor_file: + with self.fs.file_open( + (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r" + ) as new_descriptor_file: + old_content = yaml.load( + old_descriptor_file.read(), Loader=yaml.SafeLoader + ) + new_content = yaml.load( + new_descriptor_file.read(), Loader=yaml.SafeLoader + ) + if old_content and new_content: + disallowed_change = DeepDiff( + self.remove_modifiable_items(old_content), + self.remove_modifiable_items(new_content), + ) + if disallowed_change: + changed_nodes = functools.reduce( + lambda a, b: a + ", " + b, + [ + node.lstrip("root") + for node in disallowed_change.get( + "values_changed" + ).keys() + ], + ) + raise EngineException( + f"Error in validating new descriptor: {changed_nodes} cannot be modified, " + "there are disallowed changes in the ns descriptor. ", + http_code=HTTPStatus.UNPROCESSABLE_ENTITY, + ) + except ( + DbException, + AttributeError, + IndexError, + KeyError, + ValueError, + ) as e: + raise type(e)( + "NS Descriptor could not be processed with error: {}.".format(e) + ) + def sol005_projection(self, data): data["nsdOnboardingState"] = data["_admin"]["onboardingState"] data["nsdOperationalState"] = data["_admin"]["operationalState"]