X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fdescriptor_topics.py;h=072485a4afbb18ceae35b11a034af03cf7e5cbb9;hp=deae786f14ffdee736dd91fe792fa4eb4af29b91;hb=cee2ebfe3b4a0e4fe7a566eeb6e3f959649e1fce;hpb=9fb3271fa1d2568ef693d6f71d25884d7e6ea45e diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index deae786..072485a 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -17,6 +17,8 @@ import tarfile import yaml import json import copy +import os +import shutil # import logging from hashlib import md5 @@ -45,6 +47,7 @@ __author__ = "Alfonso Tierno " class DescriptorTopic(BaseTopic): 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): @@ -137,6 +140,13 @@ class DescriptorTopic(BaseTopic): """ self.fs.file_delete(_id, ignore_non_exist=True) self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder + # Remove file revisions + if "revision" in db_content["_admin"]: + revision = db_content["_admin"]["revision"] + while revision > 0: + self.fs.file_delete(_id + ":" + str(revision), ignore_non_exist=True) + revision = revision - 1 + @staticmethod def get_one_by_id(db, session, topic, id): @@ -201,7 +211,11 @@ class DescriptorTopic(BaseTopic): # 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}} + content = {"_admin": { + "userDefinedData": indata, + "revision": 0 + }} + self.format_on_new( content, session["project_id"], make_public=session["public"] ) @@ -245,6 +259,10 @@ class DescriptorTopic(BaseTopic): elif not filename: filename = "package" + revision = 1 + if "revision" in current_desc["_admin"]: + revision = current_desc["_admin"]["revision"] + 1 + # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266 file_pkg = None error_text = "" @@ -262,23 +280,25 @@ class DescriptorTopic(BaseTopic): total = int(content_range[3]) else: start = 0 - temp_folder = ( - _id + "_" + # Rather than using a temp folder, we will store the package in a folder based on + # the current revision. + proposed_revision_path = ( + _id + ":" + str(revision) ) # all the content is upload here and if ok, it is rename from id_ to is folder if start: - if not self.fs.file_exists(temp_folder, "dir"): + if not self.fs.file_exists(proposed_revision_path, "dir"): raise EngineException( "invalid Transaction-Id header", HTTPStatus.NOT_FOUND ) else: - self.fs.file_delete(temp_folder, ignore_non_exist=True) - self.fs.mkdir(temp_folder) + self.fs.file_delete(proposed_revision_path, ignore_non_exist=True) + self.fs.mkdir(proposed_revision_path) storage = self.fs.get_params() storage["folder"] = _id - file_path = (temp_folder, filename) + file_path = (proposed_revision_path, filename) if self.fs.file_exists(file_path, "file"): file_size = self.fs.file_size(file_path) else: @@ -359,9 +379,9 @@ class DescriptorTopic(BaseTopic): ) storage["descriptor"] = descriptor_file_name storage["zipfile"] = filename - self.fs.file_extract(tar, temp_folder) + self.fs.file_extract(tar, proposed_revision_path) with self.fs.file_open( - (temp_folder, descriptor_file_name), "r" + (proposed_revision_path, descriptor_file_name), "r" ) as descriptor_file: content = descriptor_file.read() elif compressed == "zip": @@ -399,10 +419,10 @@ class DescriptorTopic(BaseTopic): ) storage["descriptor"] = descriptor_file_name storage["zipfile"] = filename - self.fs.file_extract(zipfile, temp_folder) + self.fs.file_extract(zipfile, proposed_revision_path) with self.fs.file_open( - (temp_folder, descriptor_file_name), "r" + (proposed_revision_path, descriptor_file_name), "r" ) as descriptor_file: content = descriptor_file.read() else: @@ -416,6 +436,40 @@ class DescriptorTopic(BaseTopic): error_text = "Invalid yaml format " indata = yaml.load(content, Loader=yaml.SafeLoader) + # Need to close the file package here so it can be copied from the + # revision to the current, unrevisioned record + if file_pkg: + file_pkg.close() + file_pkg = None + + # Fetch both the incoming, proposed revision and the original revision so we + # can call a validate method to compare them + current_revision_path = _id + "/" + self.fs.sync(from_path=current_revision_path) + self.fs.sync(from_path=proposed_revision_path) + + if revision > 1: + try: + self._validate_descriptor_changes( + descriptor_file_name, + current_revision_path, + proposed_revision_path) + except Exception as e: + shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True) + shutil.rmtree(self.fs.path + proposed_revision_path, ignore_errors=True) + # Only delete the new revision. We need to keep the original version in place + # as it has not been changed. + 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" @@ -431,8 +485,13 @@ class DescriptorTopic(BaseTopic): session, current_desc, indata, _id=_id ) current_desc["_admin"]["modified"] = time() + current_desc["_admin"]["revision"] = revision self.db.replace(self.topic, _id, current_desc) - self.fs.dir_rename(temp_folder, _id) + + # 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) indata["_id"] = _id self._send_msg("edited", indata) @@ -624,6 +683,17 @@ class DescriptorTopic(BaseTopic): return indata + def _validate_descriptor_changes(self, + descriptor_file_name, + old_descriptor_directory, + new_descriptor_directory): + # Todo: compare changes and throw a meaningful exception for the user to understand + # Example: + # raise EngineException( + # "Error in validating new descriptor: cannot be modified", + # http_code=HTTPStatus.UNPROCESSABLE_ENTITY, + # ) + pass class VnfdTopic(DescriptorTopic): topic = "vnfds" @@ -1098,6 +1168,7 @@ class VnfdTopic(DescriptorTopic): """ super().delete_extra(session, _id, db_content, not_send_msg) self.db.del_list("vnfpkgops", {"vnfPkgId": _id}) + self.db.del_list(self.topic+"_revisions", {"_id": {"$regex": _id}}) def sol005_projection(self, data): data["onboardingState"] = data["_admin"]["onboardingState"] @@ -1375,6 +1446,19 @@ class NsdTopic(DescriptorTopic): http_code=HTTPStatus.CONFLICT, ) + 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(self.topic+"_revisions", { "_id": { "$regex": _id}}) + def sol005_projection(self, data): data["nsdOnboardingState"] = data["_admin"]["onboardingState"] data["nsdOperationalState"] = data["_admin"]["operationalState"]