From: tierno Date: Mon, 1 Jul 2019 15:36:49 +0000 (+0000) Subject: Register operations for VIM, WIM, SDNC X-Git-Tag: v6.0.2rc1~11 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=bdebce96965945c2ce86d80c60c17091c1a7fd42;p=osm%2FNBI.git Register operations for VIM, WIM, SDNC provide http ACCEPTED 202 on asynchronous operations Change-Id: I448dd90e74eca736af5b9e1f3f65bc45992228f0 Signed-off-by: tierno --- diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index 85fe9ff..187ca82 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -101,6 +101,7 @@ class UserTopic(BaseTopic): final_content["_admin"]["salt"] = salt final_content["password"] = sha256(edit_content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest() + return None def edit(self, session, _id, indata=None, kwargs=None, content=None): if not session["admin"]: @@ -196,182 +197,191 @@ class ProjectTopic(BaseTopic): return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers) -class VimAccountTopic(BaseTopic): - topic = "vim_accounts" - topic_msg = "vim_account" - schema_new = vim_account_new_schema - schema_edit = vim_account_edit_schema - vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password") - multiproject = True +class CommonVimWimSdn(BaseTopic): + """Common class for VIM, WIM SDN just to unify methods that are equal to all of them""" + config_to_encrypt = () # what keys at config must be encrypted because contains passwords + password_to_encrypt = "" # key that contains a password - def __init__(self, db, fs, msg): - BaseTopic.__init__(self, db, fs, msg) + @staticmethod + def _create_operation(op_type, params=None): + """ + Creates a dictionary with the information to an operation, similar to ns-lcm-op + :param op_type: can be create, edit, delete + :param params: operation input parameters + :return: new dictionary with + """ + now = time() + return { + "lcmOperationType": op_type, + "operationState": "PROCESSING", + "startTime": now, + "statusEnteredTime": now, + "detailed-status": "", + "operationParams": params, + } def check_conflict_on_new(self, session, indata): + """ + Check that the data to be inserted is valid. It is checked that name is unique + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param indata: data to be inserted + :return: None or raises EngineException + """ self.check_unique_name(session, indata["name"], _id=None) def check_conflict_on_edit(self, session, final_content, edit_content, _id): + """ + Check that the data to be edited/uploaded is valid. It is checked that name is unique + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param final_content: data once modified. This method may change it. + :param edit_content: incremental data that contains the modifications to apply + :param _id: internal _id + :return: None or raises EngineException + """ if not session["force"] and edit_content.get("name"): self.check_unique_name(session, edit_content["name"], _id=_id) + def format_on_edit(self, final_content, edit_content): + """ + Modifies final_content inserting admin information upon edition + :param final_content: final content to be stored at database + :param edit_content: user requested update content + :return: operation id + """ + # encrypt passwords schema_version = final_content.get("schema_version") if schema_version: - if edit_content.get("vim_password"): - final_content["vim_password"] = self.db.encrypt(edit_content["vim_password"], - schema_version=schema_version, salt=_id) - if edit_content.get("config"): - for p in self.vim_config_encrypted: + if edit_content.get(self.password_to_encrypt): + final_content[self.password_to_encrypt] = self.db.encrypt(edit_content[self.password_to_encrypt], + schema_version=schema_version, + salt=final_content["_id"]) + if edit_content.get("config") and self.config_to_encrypt: + for p in self.config_to_encrypt: if edit_content["config"].get(p): final_content["config"][p] = self.db.encrypt(edit_content["config"][p], - schema_version=schema_version, salt=_id) + schema_version=schema_version, + salt=final_content["_id"]) + + # create edit operation + final_content["_admin"]["operations"].append(self._create_operation("edit")) + return "{}:{}".format(final_content["_id"], len(final_content["_admin"]["operations"]) - 1) def format_on_new(self, content, project_id=None, make_public=False): - BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) + """ + Modifies content descriptor to include _admin and insert create operation + :param content: descriptor to be modified + :param project_id: if included, it add project read/write permissions. Can be None or a list + :param make_public: if included it is generated as public for reading. + :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified + """ + super().format_on_new(content, project_id=project_id, make_public=make_public) content["schema_version"] = schema_version = "1.1" # encrypt passwords - if content.get("vim_password"): - content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version, - salt=content["_id"]) - if content.get("config"): - for p in self.vim_config_encrypted: + if content.get(self.password_to_encrypt): + content[self.password_to_encrypt] = self.db.encrypt(content[self.password_to_encrypt], + schema_version=schema_version, + salt=content["_id"]) + if content.get("config") and self.config_to_encrypt: + for p in self.config_to_encrypt: if content["config"].get(p): - content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version, + content["config"][p] = self.db.encrypt(content["config"][p], + schema_version=schema_version, salt=content["_id"]) content["_admin"]["operationalState"] = "PROCESSING" + # create operation + content["_admin"]["operations"] = [self._create_operation("create")] + content["_admin"]["current_operation"] = None + + return "{}:0".format(content["_id"]) + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :param dry_run: make checking but do not delete - :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... + :return: operation id if it is ordered to delete. None otherwise """ - # TODO add admin to filter, validate rights - if dry_run or session["force"]: # delete completely - return BaseTopic.delete(self, session, _id, dry_run) - else: # if not, sent to kafka - v = BaseTopic.delete(self, session, _id, dry_run=True) - self.db.set_one("vim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status - self._send_msg("delete", {"_id": _id}) - return v # TODO indicate an offline operation to return 202 ACCEPTED - -class WimAccountTopic(BaseTopic): - topic = "wim_accounts" - topic_msg = "wim_account" - schema_new = wim_account_new_schema - schema_edit = wim_account_edit_schema - multiproject = True - wim_config_encrypted = () + filter_q = self._get_project_filter(session) + filter_q["_id"] = _id + db_content = self.db.get_one(self.topic, filter_q) - def __init__(self, db, fs, msg): - BaseTopic.__init__(self, db, fs, msg) + self.check_conflict_on_del(session, _id, db_content) + if dry_run: + return None - def check_conflict_on_new(self, session, indata): - self.check_unique_name(session, indata["name"], _id=None) + # remove reference from project_read. If not last delete + if session["project_id"]: + for project_id in session["project_id"]: + if project_id in db_content["_admin"]["projects_read"]: + db_content["_admin"]["projects_read"].remove(project_id) + if project_id in db_content["_admin"]["projects_write"]: + db_content["_admin"]["projects_write"].remove(project_id) + else: + db_content["_admin"]["projects_read"].clear() + db_content["_admin"]["projects_write"].clear() - def check_conflict_on_edit(self, session, final_content, edit_content, _id): - if not session["force"] and edit_content.get("name"): - self.check_unique_name(session, edit_content["name"], _id=_id) + update_dict = {"_admin.projects_read": db_content["_admin"]["projects_read"], + "_admin.projects_write": db_content["_admin"]["projects_write"] + } - # encrypt passwords - schema_version = final_content.get("schema_version") - if schema_version: - if edit_content.get("wim_password"): - final_content["wim_password"] = self.db.encrypt(edit_content["wim_password"], - schema_version=schema_version, salt=_id) - if edit_content.get("config"): - for p in self.wim_config_encrypted: - if edit_content["config"].get(p): - final_content["config"][p] = self.db.encrypt(edit_content["config"][p], - schema_version=schema_version, salt=_id) - - def format_on_new(self, content, project_id=None, make_public=False): - BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) - content["schema_version"] = schema_version = "1.1" + # check if there are projects referencing it (apart from ANY that means public).... + if db_content["_admin"]["projects_read"] and (len(db_content["_admin"]["projects_read"]) > 1 or + db_content["_admin"]["projects_read"][0] != "ANY"): + self.db.set_one(self.topic, filter_q, update_dict=update_dict) # remove references but not delete + return None - # encrypt passwords - if content.get("wim_password"): - content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version, - salt=content["_id"]) - if content.get("config"): - for p in self.wim_config_encrypted: - if content["config"].get(p): - content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version, - salt=content["_id"]) + # It must be deleted + if session["force"]: + self.db.del_one(self.topic, {"_id": _id}) + op_id = None + self._send_msg("deleted", {"_id": _id, "op_id": op_id}) + else: + update_dict["_admin.to_delete"] = True + self.db.set_one(self.topic, {"_id": _id}, + update_dict=update_dict, + push={"_admin.operations": self._create_operation("delete")} + ) + # the number of operations is the operation_id. db_content does not contains the new operation inserted, + # so the -1 is not needed + op_id = "{}:{}".format(db_content["_id"], len(db_content["_admin"]["operations"])) + self._send_msg("delete", {"_id": _id, "op_id": op_id}) + return op_id + + +class VimAccountTopic(CommonVimWimSdn): + topic = "vim_accounts" + topic_msg = "vim_account" + schema_new = vim_account_new_schema + schema_edit = vim_account_edit_schema + multiproject = True + password_to_encrypt = "vim_password" + config_to_encrypt = ("admin_password", "nsx_password", "vcenter_password") - content["_admin"]["operationalState"] = "PROCESSING" - def delete(self, session, _id, dry_run=False): - """ - Delete item by its internal _id - :param session: contains "username", "admin", "force", "public", "project_id", "set_project" - :param _id: server internal id - :param dry_run: make checking but do not delete - :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... - """ - # TODO add admin to filter, validate rights - if dry_run or session["force"]: # delete completely - return BaseTopic.delete(self, session, _id, dry_run) - else: # if not, sent to kafka - v = BaseTopic.delete(self, session, _id, dry_run=True) - self.db.set_one("wim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status - self._send_msg("delete", {"_id": _id}) - return v # TODO indicate an offline operation to return 202 ACCEPTED +class WimAccountTopic(CommonVimWimSdn): + topic = "wim_accounts" + topic_msg = "wim_account" + schema_new = wim_account_new_schema + schema_edit = wim_account_edit_schema + multiproject = True + password_to_encrypt = "wim_password" + config_to_encrypt = () -class SdnTopic(BaseTopic): +class SdnTopic(CommonVimWimSdn): topic = "sdns" topic_msg = "sdn" schema_new = sdn_new_schema schema_edit = sdn_edit_schema multiproject = True - - def __init__(self, db, fs, msg): - BaseTopic.__init__(self, db, fs, msg) - - def check_conflict_on_new(self, session, indata): - self.check_unique_name(session, indata["name"], _id=None) - - def check_conflict_on_edit(self, session, final_content, edit_content, _id): - if not session["force"] and edit_content.get("name"): - self.check_unique_name(session, edit_content["name"], _id=_id) - - # encrypt passwords - schema_version = final_content.get("schema_version") - if schema_version and edit_content.get("password"): - final_content["password"] = self.db.encrypt(edit_content["password"], schema_version=schema_version, - salt=_id) - - def format_on_new(self, content, project_id=None, make_public=False): - BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) - content["schema_version"] = schema_version = "1.1" - # encrypt passwords - if content.get("password"): - content["password"] = self.db.encrypt(content["password"], schema_version=schema_version, - salt=content["_id"]) - - content["_admin"]["operationalState"] = "PROCESSING" - - def delete(self, session, _id, dry_run=False): - """ - Delete item by its internal _id - :param session: contains "username", "admin", "force", "public", "project_id", "set_project" - :param _id: server internal id - :param dry_run: make checking but do not delete - :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... - """ - if dry_run or session["force"]: # delete completely - return BaseTopic.delete(self, session, _id, dry_run) - else: # if not sent to kafka - v = BaseTopic.delete(self, session, _id, dry_run=True) - self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status - self._send_msg("delete", {"_id": _id}) - return v # TODO indicate an offline operation to return 202 ACCEPTED + password_to_encrypt = "password" + config_to_encrypt = () class UserTopicAuth(UserTopic): @@ -1018,6 +1028,7 @@ class RoleTopicAuth(BaseTopic): final_content["permissions"]["default"] = False if "admin" not in final_content["permissions"]: final_content["permissions"]["admin"] = False + return None # @staticmethod # def format_on_show(content): diff --git a/osm_nbi/base_topic.py b/osm_nbi/base_topic.py index c8a7665..20d54bb 100644 --- a/osm_nbi/base_topic.py +++ b/osm_nbi/base_topic.py @@ -164,7 +164,7 @@ class BaseTopic: """ Check that the data to be edited/uploaded is valid :param session: contains "username", "admin", "force", "public", "project_id", "set_project" - :param final_content: data once modified. This methdo may change it. + :param final_content: data once modified. This method may change it. :param edit_content: incremental data that contains the modifications to apply :param _id: internal _id :return: None or raises EngineException @@ -210,7 +210,7 @@ class BaseTopic: :param content: descriptor to be modified :param project_id: if included, it add project read/write permissions. Can be None or a list :param make_public: if included it is generated as public for reading. - :return: None, but content is modified + :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified """ now = time() if "_admin" not in content: @@ -227,12 +227,20 @@ class BaseTopic: content["_admin"]["projects_read"].append("ANY") if not content["_admin"].get("projects_write"): content["_admin"]["projects_write"] = list(project_id) + return None @staticmethod def format_on_edit(final_content, edit_content): + """ + Modifies final_content to admin information upon edition + :param final_content: final content to be stored at database + :param edit_content: user requested update content + :return: operation id, if this edit implies an asynchronous operation; None otherwise + """ if final_content.get("_admin"): now = time() final_content["_admin"]["modified"] = now + return None def _send_msg(self, action, content): if self.topic_msg: @@ -337,7 +345,9 @@ class BaseTopic: :param indata: data to be inserted :param kwargs: used to override the indata descriptor :param headers: http request headers - :return: _id: identity of the inserted data. + :return: _id, op_id: + _id: identity of the inserted data. + op_id: operation id if this is asynchronous, None otherwise """ try: content = self._remove_envelop(indata) @@ -346,11 +356,13 @@ class BaseTopic: self._update_input_with_kwargs(content, kwargs) content = self._validate_input_new(content, force=session["force"]) self.check_conflict_on_new(session, content) - self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) + op_id = self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) _id = self.db.create(self.topic, content) rollback.append({"topic": self.topic, "_id": _id}) + if op_id: + content["op_id"] = op_id self._send_msg("create", content) - return _id + return _id, op_id except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) @@ -400,7 +412,7 @@ class BaseTopic: :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :param dry_run: make checking but do not delete - :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... + :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ... """ # To allow addressing projects and users by name AS WELL AS by _id @@ -417,18 +429,20 @@ class BaseTopic: filter_q.update(self._get_project_filter(session)) if self.multiproject and session["project_id"]: # remove reference from project_read. If not last delete + # if this topic is not part of session["project_id"] no midification at database is done and an exception + # is raised self.db.set_one(self.topic, filter_q, update_dict=None, pull={"_admin.projects_read": {"$in": session["project_id"]}}) # try to delete if there is not any more reference from projects. Ignore if it is not deleted filter_q = {'_id': _id, '_admin.projects_read': [[], ["ANY"]]} v = self.db.del_one(self.topic, filter_q, fail_on_empty=False) if not v or not v["deleted"]: - return v + return None else: - v = self.db.del_one(self.topic, filter_q) + self.db.del_one(self.topic, filter_q) self.delete_extra(session, _id, item_content) self._send_msg("deleted", {"_id": _id}) - return v + return None def edit(self, session, _id, indata=None, kwargs=None, content=None): """ @@ -438,7 +452,7 @@ class BaseTopic: :param indata: contains the changes to apply :param kwargs: modifies indata :param content: original content of the item - :return: + :return: op_id: operation id if this is processed asynchronously, None otherwise """ indata = self._remove_envelop(indata) @@ -455,16 +469,20 @@ class BaseTopic: if not content: content = self.show(session, _id) deep_update_rfc7396(content, indata) + + # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name + _id = content.get("_id") or _id + self.check_conflict_on_edit(session, content, indata, _id=_id) - self.format_on_edit(content, indata) - # To allow project addressing by name AS WELL AS _id - # self.db.replace(self.topic, _id, content) - cid = content.get("_id") - self.db.replace(self.topic, cid if cid else _id, content) + op_id = self.format_on_edit(content, indata) + + self.db.replace(self.topic, _id, content) indata.pop("_admin", None) + if op_id: + indata["op_id"] = op_id indata["_id"] = _id self._send_msg("edit", indata) - return _id + return op_id except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index b63e5d2..eb6a46d 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -117,7 +117,7 @@ class DescriptorTopic(BaseTopic): :param indata: data to be inserted :param kwargs: used to override the indata descriptor :param headers: http request headers - :return: _id: identity of the inserted data. + :return: _id, None: identity of the inserted data; and None as there is not any operation """ try: @@ -136,7 +136,7 @@ class DescriptorTopic(BaseTopic): 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 + return _id, None except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 0ba57cb..1f7f0f0 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -267,7 +267,7 @@ class Engine(object): :param session: contains the used login username and working project :param topic: it can be: users, projects, vnfds, nsds, ... :param _id: server id of the item - :return: dictionary with deleted item _id. It raises exception if not found. + :return: operation id (None if there is not operation), raise exception if error or not found. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) @@ -282,7 +282,7 @@ class Engine(object): :param _id: identifier to be updated :param indata: data to be inserted :param kwargs: used to override the indata descriptor - :return: dictionary, raise exception if not found. + :return: operation id (None if there is not operation), raise exception if error or not found. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/osm_nbi/instance_topics.py b/osm_nbi/instance_topics.py index ce9a3c6..74504b3 100644 --- a/osm_nbi/instance_topics.py +++ b/osm_nbi/instance_topics.py @@ -54,6 +54,7 @@ class NsrTopic(BaseTopic): def format_on_new(content, project_id=None, make_public=False): BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) content["_admin"]["nsState"] = "NOT_INSTANTIATED" + return None def check_conflict_on_del(self, session, _id, db_content): """ @@ -362,7 +363,7 @@ class NsrTopic(BaseTopic): member_vnf["vnfd-id-ref"], member_vnf["member-vnf-index"]) # add at database - BaseTopic.format_on_new(vnfr_descriptor, session["project_id"], make_public=session["public"]) + self.format_on_new(vnfr_descriptor, session["project_id"], make_public=session["public"]) self.db.create("vnfrs", vnfr_descriptor) rollback.append({"topic": "vnfrs", "_id": vnfr_id}) nsr_descriptor["constituent-vnfr-ref"].append(vnfr_id) @@ -375,12 +376,12 @@ class NsrTopic(BaseTopic): step = "creating nsr temporal folder" self.fs.mkdir(nsr_id) - return nsr_id + return nsr_id, None + except ValidationError as e: # TODO remove try Except, it is captured at nbi.py + raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) except Exception as e: self.logger.exception("Exception {} at NsrTopic.new()".format(e), exc_info=True) raise EngineException("Error {}: {}".format(step, e)) - except ValidationError as e: - raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) @@ -811,8 +812,8 @@ class NsLcmOpTopic(BaseTopic): rollback.append({"topic": "nslcmops", "_id": _id}) if not slice_object: self.msg.write("ns", operation, nslcmop_desc) - return _id - except ValidationError as e: + return _id, None + except ValidationError as e: # TODO remove try Except, it is captured at nbi.py raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) # except DbException as e: # raise EngineException("Cannot get ns_instance '{}': {}".format(e), HTTPStatus.NOT_FOUND) @@ -1115,7 +1116,7 @@ class NsiTopic(BaseTopic): break # Creates Nsr objects - _id_nsr = self.nsrTopic.new(rollback, session, indata_ns, kwargs, headers) + _id_nsr, _ = self.nsrTopic.new(rollback, session, indata_ns, kwargs, headers) nsrs_item = {"nsrId": _id_nsr, "shared": service.get("is-shared-nss"), "nsd-id": service["nsd-ref"], "nslcmop_instantiate": None} indata_ns["nss-id"] = service["id"] @@ -1132,8 +1133,8 @@ class NsiTopic(BaseTopic): # Creating the entry in the database self.db.create("nsis", nsi_descriptor) rollback.append({"topic": "nsis", "_id": nsi_id}) - return nsi_id - except Exception as e: + return nsi_id, None + except Exception as e: # TODO remove try Except, it is captured at nbi.py self.logger.exception("Exception {} at NsiTopic.new()".format(e), exc_info=True) raise EngineException("Error {}: {}".format(step, e)) except ValidationError as e: @@ -1297,8 +1298,8 @@ class NsiLcmOpTopic(BaseTopic): indata_ns["netsliceInstanceId"] = netsliceInstanceId # Creating NS_LCM_OP with the flag slice_object=True to not trigger the service instantiation # message via kafka bus - nslcmop = self.nsi_NsLcmOpTopic.new(rollback, session, indata_ns, kwargs, headers, - slice_object=True) + nslcmop, _ = self.nsi_NsLcmOpTopic.new(rollback, session, indata_ns, kwargs, headers, + slice_object=True) nslcmops.append(nslcmop) if operation == "terminate": nslcmop = None @@ -1320,7 +1321,7 @@ class NsiLcmOpTopic(BaseTopic): _id = self.db.create("nsilcmops", nsilcmop_desc) rollback.append({"topic": "nsilcmops", "_id": _id}) self.msg.write("nsi", operation, nsilcmop_desc) - return _id + return _id, None except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 9188834..defb01c 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -982,11 +982,12 @@ class Server(object): _id = args[0] outdata = self.engine.get_item(engine_session, engine_topic, _id) elif method == "POST": + cherrypy.response.status = HTTPStatus.CREATED.value if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"): _id = cherrypy.request.headers.get("Transaction-Id") if not _id: - _id = self.engine.new_item(rollback, engine_session, engine_topic, {}, None, - cherrypy.request.headers) + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None, + cherrypy.request.headers) completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs, cherrypy.request.headers) if completed: @@ -996,43 +997,45 @@ class Server(object): outdata = {"id": _id} elif topic == "ns_instances_content": # creates NSR - _id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) # creates nslcmop indata["lcmOperationType"] = "instantiate" indata["nsInstanceId"] = _id - nslcmop_id = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None) + nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None) self._set_location_header(main_topic, version, topic, _id) outdata = {"id": _id, "nslcmop_id": nslcmop_id} elif topic == "ns_instances" and item: indata["lcmOperationType"] = item indata["nsInstanceId"] = _id - _id = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs) self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id) outdata = {"id": _id} cherrypy.response.status = HTTPStatus.ACCEPTED.value elif topic == "netslice_instances_content": # creates NetSlice_Instance_record (NSIR) - _id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) self._set_location_header(main_topic, version, topic, _id) indata["lcmOperationType"] = "instantiate" indata["netsliceInstanceId"] = _id - nsilcmop_id = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) + nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) outdata = {"id": _id, "nsilcmop_id": nsilcmop_id} elif topic == "netslice_instances" and item: indata["lcmOperationType"] = item indata["netsliceInstanceId"] = _id - _id = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id) outdata = {"id": _id} cherrypy.response.status = HTTPStatus.ACCEPTED.value else: - _id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs, - cherrypy.request.headers) + _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs, + cherrypy.request.headers) self._set_location_header(main_topic, version, topic, _id) outdata = {"id": _id} + if op_id: + outdata["op_id"] = op_id + cherrypy.response.status = HTTPStatus.ACCEPTED.value # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages") - cherrypy.response.status = HTTPStatus.CREATED.value elif method == "DELETE": if not _id: @@ -1046,7 +1049,7 @@ class Server(object): "nsInstanceId": _id, "autoremove": True } - opp_id = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None) + opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None) if opp_id: delete_in_process = True outdata = {"_id": opp_id} @@ -1057,7 +1060,7 @@ class Server(object): "netsliceInstanceId": _id, "autoremove": True } - opp_id = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None) + opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None) if opp_id: delete_in_process = True outdata = {"_id": opp_id} @@ -1069,7 +1072,7 @@ class Server(object): cherrypy.response.status = HTTPStatus.ACCEPTED.value elif method in ("PUT", "PATCH"): - outdata = None + op_id = None if not indata and not kwargs and not engine_session.get("set_project"): raise NbiException("Nothing to update. Provide payload and/or query string", HTTPStatus.BAD_REQUEST) @@ -1079,8 +1082,14 @@ class Server(object): if not completed: cherrypy.response.headers["Transaction-Id"] = id else: - self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs) - cherrypy.response.status = HTTPStatus.NO_CONTENT.value + op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs) + + if op_id: + cherrypy.response.status = HTTPStatus.ACCEPTED.value + outdata = {"op_id": op_id} + else: + cherrypy.response.status = HTTPStatus.NO_CONTENT.value + outdata = None else: raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED) diff --git a/osm_nbi/tests/test.py b/osm_nbi/tests/test.py index 2d7eff3..6c308aa 100755 --- a/osm_nbi/tests/test.py +++ b/osm_nbi/tests/test.py @@ -346,7 +346,7 @@ class TestRest: vim_data = "{schema_version: '1.0', name: fakeVim, vim_type: openstack, vim_url: 'http://10.11.12.13/fake'"\ ", vim_tenant_name: 'vimtenant', vim_user: vimuser, vim_password: vimpassword}" self.test("Create VIM", "POST", "/admin/v1/vim_accounts", headers_yaml, vim_data, - (201), {"Location": "/admin/v1/vim_accounts/", "Content-Type": "application/yaml"}, "yaml") + (201, 202), {"Location": "/admin/v1/vim_accounts/", "Content-Type": "application/yaml"}, "yaml") return self.last_id def print_results(self): @@ -604,6 +604,13 @@ class TestProjectsDescriptors: vnfd_ids = [] engine.set_test_name("ProjectDescriptors") engine.get_autorization() + + project_admin_id = None + res = engine.test("Get my project Padmin", "GET", "/admin/v1/projects/{}".format(engine.project), headers_json, + None, 200, r_header_json, "json") + if res: + response = res.json() + project_admin_id = response["_id"] engine.test("Create project Padmin", "POST", "/admin/v1/projects", headers_json, {"name": "Padmin", "admin": True}, (201, 204), {"Location": "/admin/v1/projects/", "Content-Type": "application/json"}, "json") @@ -658,7 +665,8 @@ class TestProjectsDescriptors: engine.failed_tests += 1 # list vnfds belonging to project "admin" - res = engine.test("List VNFD of admin project", "GET", "/vnfpkgm/v1/vnf_packages?ADMIN=admin", + res = engine.test("List VNFD of admin project", "GET", + "/vnfpkgm/v1/vnf_packages?ADMIN={}".format(project_admin_id), headers_json, None, 200, r_header_json, "json") response = res.json() if len(response) != 3: @@ -769,7 +777,7 @@ class TestFakeVim: engine.set_test_name("FakeVim") engine.get_autorization() - engine.test("Create VIM", "POST", "/admin/v1/vim_accounts", headers_json, self.vim, (201, 204), + engine.test("Create VIM", "POST", "/admin/v1/vim_accounts", headers_json, self.vim, (201, 202), {"Location": "/admin/v1/vim_accounts/", "Content-Type": "application/json"}, "json") vim_id = engine.last_id engine.test("Create VIM without name, bad schema", "POST", "/admin/v1/vim_accounts", headers_json, @@ -814,28 +822,28 @@ class TestVIMSDN(TestFakeVim): engine.set_test_name("VimSdn") engine.get_autorization() # Added SDN - engine.test("Create SDN", "POST", "/admin/v1/sdns", headers_json, self.sdn, (201, 204), + engine.test("Create SDN", "POST", "/admin/v1/sdns", headers_json, self.sdn, (201, 202), {"Location": "/admin/v1/sdns/", "Content-Type": "application/json"}, "json") sdnc_id = engine.last_id # sleep(5) # Edit SDN engine.test("Edit SDN", "PATCH", "/admin/v1/sdns/{}".format(sdnc_id), headers_json, {"name": "new_sdn_name"}, - 204, None, None) + (202, 204), None, None) # sleep(5) # VIM with SDN self.vim["config"]["sdn-controller"] = sdnc_id self.vim["config"]["sdn-port-mapping"] = self.port_mapping - engine.test("Create VIM", "POST", "/admin/v1/vim_accounts", headers_json, self.vim, (200, 204, 201), + engine.test("Create VIM", "POST", "/admin/v1/vim_accounts", headers_json, self.vim, (200, 202, 201), {"Location": "/admin/v1/vim_accounts/", "Content-Type": "application/json"}, "json"), vim_id = engine.last_id self.port_mapping[0]["compute_node"] = "compute node XX" engine.test("Edit VIM change port-mapping", "PUT", "/admin/v1/vim_accounts/{}".format(vim_id), headers_json, - {"config": {"sdn-port-mapping": self.port_mapping}}, 204, None, None) + {"config": {"sdn-port-mapping": self.port_mapping}}, (202, 204), None, None) engine.test("Edit VIM remove port-mapping", "PUT", "/admin/v1/vim_accounts/{}".format(vim_id), headers_json, - {"config": {"sdn-port-mapping": None}}, 204, None, None) + {"config": {"sdn-port-mapping": None}}, (202, 204), None, None) - engine.test("Create WIM", "POST", "/admin/v1/wim_accounts", headers_json, self.wim, (200, 204, 201), + engine.test("Create WIM", "POST", "/admin/v1/wim_accounts", headers_json, self.wim, (200, 202, 201), {"Location": "/admin/v1/wim_accounts/", "Content-Type": "application/json"}, "json"), wim_id = engine.last_id @@ -991,14 +999,14 @@ class TestDeploy: ns_data_text = yaml.safe_dump(ns_data, default_flow_style=True, width=256) # create NS Two steps r = engine.test("Create NS step 1", "POST", "/nslcm/v1/ns_instances", - headers_yaml, ns_data_text, 201, + headers_yaml, ns_data_text, (201, 202), {"Location": "nslcm/v1/ns_instances/", "Content-Type": "application/yaml"}, "yaml") if not r: return self.ns_id = engine.last_id engine.test("Instantiate NS step 2", "POST", "/nslcm/v1/ns_instances/{}/instantiate".format(self.ns_id), headers_yaml, ns_data_text, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop_id = engine.last_id if test_osm: @@ -1010,7 +1018,7 @@ class TestDeploy: # remove deployment if test_osm: engine.test("Terminate NS", "POST", "/nslcm/v1/ns_instances/{}/terminate".format(self.ns_id), headers_yaml, - None, 201, r_headers_yaml_location_nslcmop, "yaml") + None, (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_id = engine.last_id # Wait until status is Ok engine.wait_operation_ready("ns", nslcmop2_id, timeout_deploy) @@ -1275,7 +1283,7 @@ class TestDeployHackfestCirrosScaling(TestDeploy): for i in range(0, 2): engine.test("Execute scale action over NS", "POST", "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_scale_out = engine.last_id engine.wait_operation_ready("ns", nslcmop2_scale_out, timeout_deploy) if manual_check: @@ -1288,7 +1296,7 @@ class TestDeployHackfestCirrosScaling(TestDeploy): for i in range(0, 2): engine.test("Execute scale IN action over NS", "POST", "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_scale_in = engine.last_id engine.wait_operation_ready("ns", nslcmop2_scale_in, timeout_deploy) if manual_check: @@ -1298,7 +1306,7 @@ class TestDeployHackfestCirrosScaling(TestDeploy): # perform scale in that must fail as reached limit engine.test("Execute scale IN out of limit action over NS", "POST", "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_scale_in = engine.last_id engine.wait_operation_ready("ns", nslcmop2_scale_in, timeout_deploy, expected_fail=True) @@ -1478,7 +1486,7 @@ class TestDeployHackfest3Charmed(TestDeploy): payload = '{member_vnf_index: "2", primitive: touch, primitive_params: { filename: /home/ubuntu/OSMTESTNBI }}' engine.test("Exec service primitive over NS", "POST", "/nslcm/v1/ns_instances/{}/action".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_action = engine.last_id # Wait until status is Ok engine.wait_operation_ready("ns", nslcmop2_action, timeout_deploy) @@ -1497,7 +1505,7 @@ class TestDeployHackfest3Charmed(TestDeploy): # '{scaling-group-descriptor: scale_dataVM, member-vnf-index: "1"}}}' # engine.test("Execute scale action over NS", "POST", # "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - # 201, r_headers_yaml_location_nslcmop, "yaml") + # (201, 202), r_headers_yaml_location_nslcmop, "yaml") # nslcmop2_scale_out = engine.last_id # engine.wait_operation_ready("ns", nslcmop2_scale_out, timeout_deploy) # if manual_check: @@ -1509,7 +1517,7 @@ class TestDeployHackfest3Charmed(TestDeploy): # '{scaling-group-descriptor: scale_dataVM, member-vnf-index: "1"}}}' # engine.test("Execute scale action over NS", "POST", # "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - # 201, r_headers_yaml_location_nslcmop, "yaml") + # (201, 202), r_headers_yaml_location_nslcmop, "yaml") # nslcmop2_scale_in = engine.last_id # engine.wait_operation_ready("ns", nslcmop2_scale_in, timeout_deploy) # if manual_check: @@ -1664,7 +1672,7 @@ class TestDeployHackfest3Charmed3(TestDeployHackfest3Charmed): '{scaling-group-descriptor: scale_dataVM, member-vnf-index: "1"}}}' engine.test("Execute scale action over NS", "POST", "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_scale_out = engine.last_id engine.wait_operation_ready("ns", nslcmop2_scale_out, timeout_deploy) if manual_check: @@ -1679,7 +1687,7 @@ class TestDeployHackfest3Charmed3(TestDeployHackfest3Charmed): '{scaling-group-descriptor: scale_dataVM, member-vnf-index: "1"}}}' engine.test("Execute scale action over NS", "POST", "/nslcm/v1/ns_instances/{}/scale".format(self.ns_id), headers_yaml, payload, - 201, r_headers_yaml_location_nslcmop, "yaml") + (201, 202), r_headers_yaml_location_nslcmop, "yaml") nslcmop2_scale_in = engine.last_id engine.wait_operation_ready("ns", nslcmop2_scale_in, timeout_deploy) if manual_check: @@ -2238,7 +2246,7 @@ class TestNetSliceInstances: def create_slice(self, engine, nsi_data, name): ns_data_text = yaml.safe_dump(nsi_data, default_flow_style=True, width=256) r = engine.test(name, "POST", "/nsilcm/v1/netslice_instances", - headers_yaml, ns_data_text, 201, + headers_yaml, ns_data_text, (201, 202), {"Location": "nsilcm/v1/netslice_instances/", "Content-Type": "application/yaml"}, "yaml") return r @@ -2246,11 +2254,11 @@ class TestNetSliceInstances: ns_data_text = yaml.safe_dump(nsi_data, default_flow_style=True, width=256) engine.test(name, "POST", "/nsilcm/v1/netslice_instances/{}/instantiate".format(nsi_id), headers_yaml, ns_data_text, - 201, r_headers_yaml_location_nsilcmop, "yaml") + (201, 202), r_headers_yaml_location_nsilcmop, "yaml") def terminate_slice(self, engine, nsi_id, name): engine.test(name, "POST", "/nsilcm/v1/netslice_instances/{}/terminate".format(nsi_id), - headers_yaml, None, 201, r_headers_yaml_location_nsilcmop, "yaml") + headers_yaml, None, (201, 202), r_headers_yaml_location_nsilcmop, "yaml") def delete_slice(self, engine, nsi_id, name): engine.test(name, "DELETE", "/nsilcm/v1/netslice_instances/{}".format(nsi_id), headers_yaml, None, diff --git a/osm_nbi/validation.py b/osm_nbi/validation.py index b6ef64c..904abbd 100644 --- a/osm_nbi/validation.py +++ b/osm_nbi/validation.py @@ -807,5 +807,5 @@ def is_valid_uuid(x): try: if UUID(x): return True - except (TypeError, ValueError): + except (TypeError, ValueError, AttributeError): return False