From 65ca36d13f895d0a361d59a5962029d6e3ef7a99 Mon Sep 17 00:00:00 2001 From: tierno Date: Tue, 12 Feb 2019 19:27:52 +0100 Subject: [PATCH] Complete multiproject. Adding admin query string: FORCE,ADMIN,PUBLIC,SET_PROJECT Change-Id: I96bbb050ea8ade55edb948b925127049882ff191 Signed-off-by: tierno --- osm_nbi/admin_topics.py | 254 +++++++++++++++++------------------ osm_nbi/auth.py | 6 +- osm_nbi/base_topic.py | 192 +++++++++++++++++--------- osm_nbi/descriptor_topics.py | 115 +++++++--------- osm_nbi/engine.py | 30 ++--- osm_nbi/instance_topics.py | 126 +++++++---------- osm_nbi/nbi.py | 113 +++++++++++++--- osm_nbi/subscriptions.py | 12 +- osm_nbi/tests/test.py | 195 ++++++++++++++++++++++----- 9 files changed, 626 insertions(+), 417 deletions(-) diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index 76e4065..d894a09 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -34,18 +34,17 @@ class UserTopic(BaseTopic): topic_msg = "users" schema_new = user_new_schema schema_edit = user_edit_schema + multiproject = False def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) @staticmethod - def _get_project_filter(session, write=False, show_all=True): + def _get_project_filter(session): """ Generates a filter dictionary for querying database users. Current policy is admin can show all, non admin, only its own user. - :param session: contains "username", if user is "admin" and the working "project_id" - :param write: if operation is for reading (False) or writing (True) - :param show_all: if True it will show public or + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :return: """ if session["admin"]: # allows all @@ -53,21 +52,19 @@ class UserTopic(BaseTopic): else: return {"username": session["username"]} - def check_conflict_on_new(self, session, indata, force=False): + def check_conflict_on_new(self, session, indata): # check username not exists if self.db.get_one(self.topic, {"username": indata.get("username")}, fail_on_empty=False, fail_on_more=False): raise EngineException("username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT) # check projects - if not force: - for p in indata["projects"]: - if p == "admin": - continue + if not session["force"]: + for p in indata.get("projects"): # To allow project addressing by Name as well as ID if not self.db.get_one("projects", {BaseTopic.id_field("projects", p): p}, fail_on_empty=False, fail_on_more=False): raise EngineException("project '{}' does not exist".format(p), HTTPStatus.CONFLICT) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): if _id == session["username"]: raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT) @@ -90,7 +87,7 @@ class UserTopic(BaseTopic): final_content["password"] = sha256(edit_content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest() - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): if not session["admin"]: raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) # Names that look like UUIDs are not allowed @@ -98,9 +95,9 @@ class UserTopic(BaseTopic): if is_valid_uuid(name): raise EngineException("Usernames that look like UUIDs are not allowed", http_code=HTTPStatus.UNPROCESSABLE_ENTITY) - return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, force=force, content=content) + return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): if not session["admin"]: raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) # Names that look like UUIDs are not allowed @@ -108,8 +105,7 @@ class UserTopic(BaseTopic): if is_valid_uuid(name): raise EngineException("Usernames that look like UUIDs are not allowed", http_code=HTTPStatus.UNPROCESSABLE_ENTITY) - return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers, force=force, - make_public=make_public) + return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers) class ProjectTopic(BaseTopic): @@ -117,11 +113,25 @@ class ProjectTopic(BaseTopic): topic_msg = "projects" schema_new = project_new_schema schema_edit = project_edit_schema + multiproject = False def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) - def check_conflict_on_new(self, session, indata, force=False): + @staticmethod + def _get_project_filter(session): + """ + Generates a filter dictionary for querying database users. + Current policy is admin can show all, non admin, only its own user. + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :return: + """ + if session["admin"]: # allows all + return {} + else: + return {"_id.cont": session["project_id"]} + + def check_conflict_on_new(self, session, indata): if not indata.get("name"): raise EngineException("missing 'name'") # check name not exists @@ -134,16 +144,16 @@ class ProjectTopic(BaseTopic): # Removed so that the UUID is kept, to allow Project Name modification # content["_id"] = content["name"] - def check_conflict_on_del(self, session, _id, force=False): - if _id == session["project_id"]: + def check_conflict_on_del(self, session, _id): + if _id in session["project_id"]: raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT) - if force: + if session["force"]: return _filter = {"projects": _id} if self.db.get_list("users", _filter): raise EngineException("There is some USER that contains this project", http_code=HTTPStatus.CONFLICT) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): if not session["admin"]: raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) # Names that look like UUIDs are not allowed @@ -151,9 +161,9 @@ class ProjectTopic(BaseTopic): if is_valid_uuid(name): raise EngineException("Project names that look like UUIDs are not allowed", http_code=HTTPStatus.UNPROCESSABLE_ENTITY) - return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, force=force, content=content) + return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): if not session["admin"]: raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) # Names that look like UUIDs are not allowed @@ -161,8 +171,7 @@ class ProjectTopic(BaseTopic): if is_valid_uuid(name): raise EngineException("Project names that look like UUIDs are not allowed", http_code=HTTPStatus.UNPROCESSABLE_ENTITY) - return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers, force=force, - make_public=make_public) + return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers) class VimAccountTopic(BaseTopic): @@ -171,15 +180,16 @@ class VimAccountTopic(BaseTopic): schema_new = vim_account_new_schema schema_edit = vim_account_edit_schema vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password") + multiproject = True def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) - def check_conflict_on_new(self, session, indata, force=False): + 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, force=False): - if not force and edit_content.get("name"): + 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 @@ -210,20 +220,19 @@ class VimAccountTopic(BaseTopic): content["_admin"]["operationalState"] = "PROCESSING" - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :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 force: # delete completely - return BaseTopic.delete(self, session, _id, force, dry_run) + 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, force, dry_run=True) + 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 @@ -234,16 +243,17 @@ class WimAccountTopic(BaseTopic): topic_msg = "wim_account" schema_new = wim_account_new_schema schema_edit = wim_account_edit_schema + multiproject = True wim_config_encrypted = () def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) - def check_conflict_on_new(self, session, indata, force=False): + 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, force=False): - if not force and edit_content.get("name"): + 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 @@ -274,20 +284,19 @@ class WimAccountTopic(BaseTopic): content["_admin"]["operationalState"] = "PROCESSING" - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :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 force: # delete completely - return BaseTopic.delete(self, session, _id, force, dry_run) + 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, force, dry_run=True) + 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 @@ -298,15 +307,16 @@ class SdnTopic(BaseTopic): 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, force=False): + 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, force=False): - if not force and edit_content.get("name"): + 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 @@ -325,41 +335,39 @@ class SdnTopic(BaseTopic): content["_admin"]["operationalState"] = "PROCESSING" - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :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 force: # delete completely - return BaseTopic.delete(self, session, _id, force, dry_run) + 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, force, dry_run=True) + 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 class UserTopicAuth(UserTopic): - topic = "users" - topic_msg = "users" - schema_new = user_new_schema - schema_edit = user_edit_schema + # topic = "users" + # topic_msg = "users" + # schema_new = user_new_schema + # schema_edit = user_edit_schema def __init__(self, db, fs, msg, auth): UserTopic.__init__(self, db, fs, msg) self.auth = auth - def check_conflict_on_new(self, session, indata, force=False): + def check_conflict_on_new(self, session, indata): """ Check that the data to be inserted is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: data to be inserted - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ username = indata.get("username") @@ -368,15 +376,14 @@ class UserTopicAuth(UserTopic): if username in user_list: raise EngineException("username '{}' exists".format(username), HTTPStatus.CONFLICT) - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): + def check_conflict_on_edit(self, session, final_content, edit_content, _id): """ Check that the data to be edited/uploaded is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param final_content: data once modified :param edit_content: incremental data that contains the modifications to apply :param _id: internal _id - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ users = self.auth.get_user_list() @@ -391,13 +398,12 @@ class UserTopicAuth(UserTopic): raise EngineException("You cannot remove system_admin role from admin user", http_code=HTTPStatus.FORBIDDEN) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check if deletion can be done because of dependencies if it is not force. To override - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: internal _id - :param force: Avoid this checking :return: None if ok or raises EngineException with the conflict """ if _id == session["username"]: @@ -437,19 +443,17 @@ class UserTopicAuth(UserTopic): else: final_content["project_role_mappings"] = edit_content["project_role_mappings"] - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Creates a new entry into the authentication backend. NOTE: Overrides BaseTopic functionality because it doesn't require access to database. :param rollback: list to append created items at database in case a rollback may to be done - :param session: contains the used login username and working project + :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 - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: _id: identity of the inserted data. """ try: @@ -457,9 +461,9 @@ class UserTopicAuth(UserTopic): # Override descriptor with query string kwargs BaseTopic._update_input_with_kwargs(content, kwargs) - content = self._validate_input_new(content, force) - self.check_conflict_on_new(session, content, force=force) - self.format_on_new(content, project_id=session["project_id"], make_public=make_public) + content = self._validate_input_new(content, session["force"]) + self.check_conflict_on_new(session, content) + self.format_on_new(content, session["project_id"], make_public=session["public"]) _id = self.auth.create_user(content["username"], content["password"]) rollback.append({"topic": self.topic, "_id": _id}) del content["password"] @@ -472,7 +476,7 @@ class UserTopicAuth(UserTopic): """ Get complete information on an topic - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :return: dictionary, raise exception if not found. """ @@ -485,15 +489,14 @@ class UserTopicAuth(UserTopic): else: raise EngineException("User not found", HTTPStatus.NOT_FOUND) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): """ Updates an user entry. - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: :param indata: data to be inserted :param kwargs: used to override the indata descriptor - :param force: If True avoid some dependence checks :param content: :return: _id: identity of the inserted data. """ @@ -503,11 +506,11 @@ class UserTopicAuth(UserTopic): if kwargs: BaseTopic._update_input_with_kwargs(indata, kwargs) try: - indata = self._validate_input_edit(indata, force=force) + indata = self._validate_input_edit(indata, force=session["force"]) if not content: content = self.show(session, _id) - self.check_conflict_on_edit(session, content, indata, _id=_id, force=force) + self.check_conflict_on_edit(session, content, indata, _id=_id) self.format_on_edit(content, indata) if "password" in content: @@ -552,23 +555,23 @@ class UserTopicAuth(UserTopic): def list(self, session, filter_q=None): """ Get a list of the topic that matches a filter - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ return self.auth.get_user_list() - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :param force: indicates if deletion must be forced in case of conflict :param dry_run: make checking but do not delete :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... """ - self.check_conflict_on_del(session, _id, force) + self.check_conflict_on_del(session, _id) if not dry_run: v = self.auth.delete_user(_id) return v @@ -576,22 +579,21 @@ class UserTopicAuth(UserTopic): class ProjectTopicAuth(ProjectTopic): - topic = "projects" - topic_msg = "projects" - schema_new = project_new_schema - schema_edit = project_edit_schema + # topic = "projects" + # topic_msg = "projects" + # schema_new = project_new_schema + # schema_edit = project_edit_schema def __init__(self, db, fs, msg, auth): ProjectTopic.__init__(self, db, fs, msg) self.auth = auth - def check_conflict_on_new(self, session, indata, force=False): + def check_conflict_on_new(self, session, indata): """ Check that the data to be inserted is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: data to be inserted - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ project = indata.get("name") @@ -600,13 +602,12 @@ class ProjectTopicAuth(ProjectTopic): if project in project_list: raise EngineException("project '{}' exists".format(project), HTTPStatus.CONFLICT) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check if deletion can be done because of dependencies if it is not force. To override - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: internal _id - :param force: Avoid this checking :return: None if ok or raises EngineException with the conflict """ projects = self.auth.get_project_list() @@ -616,19 +617,17 @@ class ProjectTopicAuth(ProjectTopic): if _id == current_project["_id"]: raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Creates a new entry into the authentication backend. NOTE: Overrides BaseTopic functionality because it doesn't require access to database. :param rollback: list to append created items at database in case a rollback may to be done - :param session: contains the used login username and working project + :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 - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: _id: identity of the inserted data. """ try: @@ -636,9 +635,9 @@ class ProjectTopicAuth(ProjectTopic): # Override descriptor with query string kwargs BaseTopic._update_input_with_kwargs(content, kwargs) - content = self._validate_input_new(content, force) - self.check_conflict_on_new(session, content, force=force) - self.format_on_new(content, project_id=session["project_id"], make_public=make_public) + content = self._validate_input_new(content, session["force"]) + self.check_conflict_on_new(session, content) + self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) _id = self.auth.create_project(content["name"]) rollback.append({"topic": self.topic, "_id": _id}) # self._send_msg("create", content) @@ -650,7 +649,7 @@ class ProjectTopicAuth(ProjectTopic): """ Get complete information on an topic - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :return: dictionary, raise exception if not found. """ @@ -667,23 +666,22 @@ class ProjectTopicAuth(ProjectTopic): """ Get a list of the topic that matches a filter - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ return self.auth.get_project_list() - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :param dry_run: make checking but do not delete :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... """ - self.check_conflict_on_del(session, _id, force) + self.check_conflict_on_del(session, _id) if not dry_run: v = self.auth.delete_project(_id) return v @@ -695,6 +693,7 @@ class RoleTopicAuth(BaseTopic): topic_msg = "roles" schema_new = roles_new_schema schema_edit = roles_edit_schema + multiproject = False def __init__(self, db, fs, msg, auth, ops): BaseTopic.__init__(self, db, fs, msg) @@ -750,13 +749,12 @@ class RoleTopicAuth(BaseTopic): self.validate_role_definition(self.operations, input["definition"]) return input - def check_conflict_on_new(self, session, indata, force=False): + def check_conflict_on_new(self, session, indata): """ Check that the data to be inserted is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: data to be inserted - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ role = indata.get("name") @@ -765,15 +763,14 @@ class RoleTopicAuth(BaseTopic): if role in role_list: raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT) - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): + def check_conflict_on_edit(self, session, final_content, edit_content, _id): """ Check that the data to be edited/uploaded is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param final_content: data once modified :param edit_content: incremental data that contains the modifications to apply :param _id: internal _id - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ roles = self.auth.get_role_list() @@ -783,13 +780,12 @@ class RoleTopicAuth(BaseTopic): if _id == system_admin_role["_id"]: raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check if deletion can be done because of dependencies if it is not force. To override - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: internal _id - :param force: Avoid this checking :return: None if ok or raises EngineException with the conflict """ roles = self.auth.get_role_list() @@ -886,7 +882,7 @@ class RoleTopicAuth(BaseTopic): """ Get complete information on an topic - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :return: dictionary, raise exception if not found. """ @@ -903,7 +899,7 @@ class RoleTopicAuth(BaseTopic): """ Get a list of the topic that matches a filter - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ @@ -920,17 +916,15 @@ class RoleTopicAuth(BaseTopic): return new_roles - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + 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 the used login username and working project + :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 - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: _id: identity of the inserted data. """ try: @@ -938,9 +932,9 @@ class RoleTopicAuth(BaseTopic): # Override descriptor with query string kwargs BaseTopic._update_input_with_kwargs(content, kwargs) - content = self._validate_input_new(content, force) - self.check_conflict_on_new(session, content, force=force) - self.format_on_new(content, project_id=session["project_id"], make_public=make_public) + content = self._validate_input_new(content, session["force"]) + self.check_conflict_on_new(session, content) + self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) role_name = content["name"] role = self.auth.create_role(role_name) content["_id"] = role["_id"] @@ -951,17 +945,16 @@ class RoleTopicAuth(BaseTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :param dry_run: make checking but do not delete :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... """ - self.check_conflict_on_del(session, _id, force) + self.check_conflict_on_del(session, _id) filter_q = self._get_project_filter(session, write=True, show_all=True) filter_q["_id"] = _id if not dry_run: @@ -970,15 +963,14 @@ class RoleTopicAuth(BaseTopic): return v return None - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): """ Updates a role entry. - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: :param indata: data to be inserted :param kwargs: used to override the indata descriptor - :param force: If True avoid some dependence checks :param content: :return: _id: identity of the inserted data. """ @@ -988,11 +980,11 @@ class RoleTopicAuth(BaseTopic): if kwargs: BaseTopic._update_input_with_kwargs(indata, kwargs) try: - indata = self._validate_input_edit(indata, force=force) + indata = self._validate_input_edit(indata, force=session["force"]) if not content: content = self.show(session, _id) - self.check_conflict_on_edit(session, content, indata, _id=_id, force=force) + self.check_conflict_on_edit(session, content, indata, _id=_id) self.format_on_edit(content, indata) self.db.replace(self.topic, _id, content) return id diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 9171c94..fcebad4 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -55,6 +55,7 @@ class Authenticator: Authorization. Initially it should support Openstack Keystone as a backend through a plugin model where more backends can be added and a RBAC model to manage permissions on operations. + This class must be threading safe """ periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned @@ -480,7 +481,8 @@ class Authenticator: now = time() session = self.tokens_cache.get(token_id) if session and session["expires"] < now: - del self.tokens_cache[token_id] + # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del + self.tokens_cache.pop(token_id, None) session = None if session: return session @@ -501,7 +503,7 @@ class Authenticator: if self.config["global"].get("test.user_not_authorized"): return {"id": "fake-token-id-for-test", "project_id": self.config["global"].get("test.project_not_authorized", "admin"), - "username": self.config["global"]["test.user_not_authorized"]} + "username": self.config["global"]["test.user_not_authorized"], "admin": True} else: raise diff --git a/osm_nbi/base_topic.py b/osm_nbi/base_topic.py index 095e265..72707ad 100644 --- a/osm_nbi/base_topic.py +++ b/osm_nbi/base_topic.py @@ -55,6 +55,7 @@ class BaseTopic: topic_msg = None # to_override schema_new = None # to_override schema_edit = None # to_override + multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read # Alternative ID Fields for some Topics alt_id_field = { @@ -70,7 +71,7 @@ class BaseTopic: @staticmethod def id_field(topic, value): - "Returns ID Field for given topic and field value" + """Returns ID Field for given topic and field value""" if topic in ["projects", "users"] and not is_valid_uuid(value): return BaseTopic.alt_id_field[topic] else: @@ -105,58 +106,92 @@ class BaseTopic: return input @staticmethod - def _get_project_filter(session, write=False, show_all=True): + def _get_project_filter(session): """ Generates a filter dictionary for querying database, so that only allowed items for this project can be addressed. Only propietary or public can be used. Allowed projects are at _admin.project_read/write. If it is not present or contains ANY mean public. - :param session: contains "username", if user is "admin" and the working "project_id" - :param write: if operation is for reading (False) or writing (True) - :param show_all: if True it will show public or - :return: - """ - if write: - k = "_admin.projects_write.cont" - else: - k = "_admin.projects_read.cont" - if not show_all: - return {k: session["project_id"]} - elif session["admin"]: # and show_all: # allows all - return {} - else: - return {k: ["ANY", session["project_id"], None]} - - def check_conflict_on_new(self, session, indata, force=False): + :param session: contains: + project_id: project list this session has rights to access. Can be empty, one or several + set_project: items created will contain this project list + force: True or False + public: True, False or None + method: "list", "show", "write", "delete" + admin: True or False + :return: dictionary with project filter + """ + p_filter = {} + project_filter_n = [] + project_filter = list(session["project_id"]) + + if session["method"] not in ("list", "delete"): + if project_filter: + project_filter.append("ANY") + elif session["public"] is not None: + if session["public"]: + project_filter.append("ANY") + else: + project_filter_n.append("ANY") + + if session.get("PROJECT.ne"): + project_filter_n.append(session["PROJECT.ne"]) + + if project_filter: + if session["method"] in ("list", "show", "delete") or session.get("set_project"): + p_filter["_admin.projects_read.cont"] = project_filter + else: + p_filter["_admin.projects_write.cont"] = project_filter + if project_filter_n: + if session["method"] in ("list", "show", "delete") or session.get("set_project"): + p_filter["_admin.projects_read.ncont"] = project_filter_n + else: + p_filter["_admin.projects_write.ncont"] = project_filter_n + + return p_filter + + def check_conflict_on_new(self, session, indata): """ Check that the data to be inserted is valid - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: data to be inserted - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ pass - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): + def check_conflict_on_edit(self, session, final_content, edit_content, _id): """ Check that the data to be edited/uploaded is valid - :param session: contains "username", if user is "admin" and the working "project_id" - :param final_content: data once modified + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param final_content: data once modified. This methdo may change it. :param edit_content: incremental data that contains the modifications to apply :param _id: internal _id - :param force: boolean. With force it is more tolerant :return: None or raises EngineException """ - pass + if not self.multiproject: + return + # Change public status + if session["public"] is not None: + if session["public"] and "ANY" not in final_content["_admin"]["projects_read"]: + final_content["_admin"]["projects_read"].append("ANY") + final_content["_admin"]["projects_write"].clear() + if not session["public"] and "ANY" in final_content["_admin"]["projects_read"]: + final_content["_admin"]["projects_read"].remove("ANY") + + # Change project status + if session.get("set_project"): + for p in session["set_project"]: + if p not in final_content["_admin"]["projects_read"]: + final_content["_admin"]["projects_read"].append(p) def check_unique_name(self, session, name, _id=None): """ Check that the name is unique for this project - :param session: contains "username", if user is "admin" and the working "project_id" + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param name: name to be checked :param _id: If not None, ignore this entry that are going to change :return: None or raises EngineException """ - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) _filter["name"] = name if _id: _filter["_id.neq"] = _id @@ -168,7 +203,7 @@ class BaseTopic: """ Modifies content descriptor to include _admin :param content: descriptor to be modified - :param project_id: if included, it add project read/write permissions + :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 """ @@ -180,13 +215,13 @@ class BaseTopic: content["_admin"]["modified"] = now if not content.get("_id"): content["_id"] = str(uuid4()) - if project_id: + if project_id is not None: if not content["_admin"].get("projects_read"): - content["_admin"]["projects_read"] = [project_id] + content["_admin"]["projects_read"] = list(project_id) if make_public: content["_admin"]["projects_read"].append("ANY") if not content["_admin"].get("projects_write"): - content["_admin"]["projects_write"] = [project_id] + content["_admin"]["projects_write"] = list(project_id) @staticmethod def format_on_edit(final_content, edit_content): @@ -199,12 +234,11 @@ class BaseTopic: content.pop("_admin", None) self.msg.write(self.topic_msg, action, content) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check if deletion can be done because of dependencies if it is not force. To override - :param session: contains "username", if user is "admin" and the working "project_id" - :param _id: itnernal _id - :param force: Avoid this checking + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param _id: internal _id :return: None if ok or raises EngineException with the conflict """ pass @@ -248,11 +282,11 @@ class BaseTopic: def show(self, session, _id): """ Get complete information on an topic - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id :return: dictionary, raise exception if not found. """ - filter_db = self._get_project_filter(session, write=False, show_all=True) + filter_db = self._get_project_filter(session) # To allow project&user addressing by name AS WELL AS _id filter_db[BaseTopic.id_field(self.topic, _id)] = _id return self.db.get_one(self.topic, filter_db) @@ -262,7 +296,7 @@ class BaseTopic: def get_file(self, session, _id, path=None, accept_header=None): """ Only implemented for descriptor topics. Return the file content of a descriptor - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: Identity of the item to get content :param path: artifact path or "$DESCRIPTOR" or None :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain @@ -280,22 +314,20 @@ class BaseTopic: if not filter_q: filter_q = {} - filter_q.update(self._get_project_filter(session, write=False, show_all=True)) + filter_q.update(self._get_project_filter(session)) # TODO transform data for SOL005 URL requests. Transform filtering # TODO implement "field-type" query string SOL005 return self.db.get_list(self.topic, filter_q) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + 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 the used login username and working project + :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 - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: _id: identity of the inserted data. """ try: @@ -303,9 +335,9 @@ class BaseTopic: # Override descriptor with query string kwargs self._update_input_with_kwargs(content, kwargs) - content = self._validate_input_new(content, force=force) - self.check_conflict_on_new(session, content, force=force) - self.format_on_new(content, project_id=session["project_id"], make_public=make_public) + 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"]) _id = self.db.create(self.topic, content) rollback.append({"topic": self.topic, "_id": _id}) self._send_msg("create", content) @@ -313,16 +345,15 @@ class BaseTopic: except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def upload_content(self, session, _id, indata, kwargs, headers, force=False): + def upload_content(self, session, _id, indata, kwargs, headers): """ Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract) - :param session: session + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id : the database id of entry to be updated :param indata: http body request :param kwargs: user query string to override parameters. NOT USED :param headers: http request headers - :param force: to be more tolerant with validation :return: True package has is completely uploaded or False if partial content has been uplodaed. Raise exception on error """ @@ -331,51 +362,82 @@ class BaseTopic: def delete_list(self, session, filter_q=None): """ Delete a several entries of a topic. This is for internal usage and test only, not exposed to NBI API - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param filter_q: filter of data to be applied :return: The deleted list, it can be empty if no one match the filter. """ # TODO add admin to filter, validate rights if not filter_q: filter_q = {} - filter_q.update(self._get_project_filter(session, write=True, show_all=True)) + filter_q.update(self._get_project_filter(session)) return self.db.del_list(self.topic, filter_q) - def delete(self, session, _id, force=False, dry_run=False): + def delete_extra(self, session, _id): + """ + Delete other things apart from database entry of a item _id. + e.g.: other associated elements at database and other file system storage + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param _id: server internal id + """ + pass + + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :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 # data = self.get_item(topic, _id) - self.check_conflict_on_del(session, _id, force) - filter_q = self._get_project_filter(session, write=True, show_all=True) + self.check_conflict_on_del(session, _id) + filter_q = self._get_project_filter(session) # To allow project addressing by name AS WELL AS _id filter_q[BaseTopic.id_field(self.topic, _id)] = _id - if not dry_run: + if dry_run: + return None + if self.multiproject and session["project_id"]: + # remove reference from project_read. If not last delete + 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 + else: v = self.db.del_one(self.topic, filter_q) - self._send_msg("deleted", {"_id": _id}) - return v - return None + self.delete_extra(session, _id) + self._send_msg("deleted", {"_id": _id}) + return v - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): + """ + Change the content of an item + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param _id: server internal id + :param indata: contains the changes to apply + :param kwargs: modifies indata + :param content: original content of the item + :return: + """ indata = self._remove_envelop(indata) # Override descriptor with query string kwargs if kwargs: self._update_input_with_kwargs(indata, kwargs) try: - indata = self._validate_input_edit(indata, force=force) + if indata and session.get("set_project"): + raise EngineException("Cannot edit content and set to project (query string SET_PROJECT) at same time", + HTTPStatus.UNPROCESSABLE_ENTITY) + indata = self._validate_input_edit(indata, force=session["force"]) # TODO self._check_edition(session, indata, _id, force) if not content: content = self.show(session, _id) deep_update_rfc7396(content, indata) - self.check_conflict_on_edit(session, content, indata, _id=_id, force=force) + 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) @@ -385,6 +447,6 @@ class BaseTopic: indata.pop("_admin", None) indata["_id"] = _id self._send_msg("edit", indata) - return id + return _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 a1c3c15..c533054 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -36,7 +36,8 @@ class DescriptorTopic(BaseTopic): def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): + def check_conflict_on_edit(self, session, final_content, edit_content, _id): + super().check_conflict_on_edit(session, final_content, edit_content, _id) # 1. validate again with pyangbind # 1.1. remove internal keys internal_keys = {} @@ -44,7 +45,7 @@ class DescriptorTopic(BaseTopic): if k in final_content: internal_keys[k] = final_content.pop(k) storage_params = internal_keys["_admin"].get("storage") - serialized = self._validate_input_new(final_content, storage_params, force) + serialized = self._validate_input_new(final_content, storage_params, session["force"]) # 1.2. modify final_content with a serialized version final_content.clear() final_content.update(serialized) @@ -52,11 +53,11 @@ class DescriptorTopic(BaseTopic): for k, v in internal_keys.items(): final_content[k] = v - if force: + if session["force"]: return # 2. check that this id is not present if "id" in edit_content: - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) _filter["id"] = final_content["id"] _filter["_id.neq"] = _id if self.db.get_one(self.topic, _filter, fail_on_empty=False): @@ -71,29 +72,14 @@ class DescriptorTopic(BaseTopic): content["_admin"]["operationalState"] = "DISABLED" content["_admin"]["usageState"] = "NOT_IN_USE" - def delete(self, session, _id, force=False, dry_run=False): - """ - Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights - :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict - :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 - v = BaseTopic.delete(self, session, _id, force, dry_run=True) - if dry_run: - return - v = self.db.del_one(self.topic, {"_id": _id}) + def delete_extra(self, session, _id): self.fs.file_delete(_id, ignore_non_exist=True) self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder - self._send_msg("delete", {"_id": _id}) - return v @staticmethod def get_one_by_id(db, session, topic, id): # find owned by this project - _filter = BaseTopic._get_project_filter(session, write=False, show_all=False) + _filter = BaseTopic._get_project_filter(session) _filter["id"] = id desc_list = db.get_list(topic, _filter) if len(desc_list) == 1: @@ -103,7 +89,7 @@ class DescriptorTopic(BaseTopic): HTTPStatus.CONFLICT) # not found any: try to find public - _filter = BaseTopic._get_project_filter(session, write=False, show_all=True) + _filter = BaseTopic._get_project_filter(session) _filter["id"] = id desc_list = db.get_list(topic, _filter) if not desc_list: @@ -114,18 +100,16 @@ class DescriptorTopic(BaseTopic): raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format( topic[:-1], id), HTTPStatus.CONFLICT) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure. Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content (self.upload_content) :param rollback: list to append created items at database in case a rollback may to be done - :param session: contains the used login username and working project + :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 - :param force: If True avoid some dependence checks - :param make_public: Make the created descriptor public to all projects :return: _id: identity of the inserted data. """ @@ -139,25 +123,24 @@ class DescriptorTopic(BaseTopic): 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, force=force) + # 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=make_public) + 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 except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def upload_content(self, session, _id, indata, kwargs, headers, force=False): + def upload_content(self, session, _id, indata, kwargs, headers): """ Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract) - :param session: session + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id : the nsd,vnfd is already created, this is the id :param indata: http body request :param kwargs: user query string to override parameters. NOT USED :param headers: http request headers - :param force: to be more tolerant with validation :return: True if package is completely uploaded or False if partial content has been uploded Raise exception on error """ @@ -283,10 +266,10 @@ class DescriptorTopic(BaseTopic): if kwargs: self._update_input_with_kwargs(indata, kwargs) # it will call overrides method at VnfdTopic or NsdTopic - # indata = self._validate_input_edit(indata, force=force) + # indata = self._validate_input_edit(indata, force=session["force"]) deep_update_rfc7396(current_desc, indata) - self.check_conflict_on_edit(session, current_desc, indata, _id=_id, force=force) + self.check_conflict_on_edit(session, current_desc, indata, _id=_id) self.db.replace(self.topic, _id, current_desc) self.fs.dir_rename(temp_folder, _id) @@ -317,7 +300,7 @@ class DescriptorTopic(BaseTopic): def get_file(self, session, _id, path=None, accept_header=None): """ Return the file content of a vnfd or nsd - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: Identity of the vnfd, nsd :param path: artifact path or "$DESCRIPTOR" or None :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain @@ -426,8 +409,8 @@ class VnfdTopic(DescriptorTopic): clean_indata = clean_indata['vnfd:vnfd'][0] return clean_indata - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): - super().check_conflict_on_edit(session, final_content, edit_content, _id, force=force) + def check_conflict_on_edit(self, session, final_content, edit_content, _id): + super().check_conflict_on_edit(session, final_content, edit_content, _id) # set type of vnfd contains_pdu = False @@ -443,24 +426,23 @@ class VnfdTopic(DescriptorTopic): final_content["_admin"]["type"] = "vnfd" # if neither vud nor pdu do not fill type - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr that uses this vnfd - :param session: + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: vnfd inernal id - :param force: Avoid this checking :return: None or raises EngineException with the conflict """ - if force: + if session["force"]: return descriptor = self.db.get_one("vnfds", {"_id": _id}) descriptor_id = descriptor.get("id") if not descriptor_id: # empty vnfd not uploaded return - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) # check vnfrs using this vnfd _filter["vnfd-id"] = _id if self.db.get_list("vnfrs", _filter): @@ -469,7 +451,7 @@ class VnfdTopic(DescriptorTopic): # check NSD using this VNFD _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id if self.db.get_list("nsds", _filter): - raise EngineException("There is soame NSD that depends on this VNFD", http_code=HTTPStatus.CONFLICT) + raise EngineException("There is at least a NSD that depends on this VNFD", http_code=HTTPStatus.CONFLICT) def _validate_input_new(self, indata, storage_params, force=False): indata = self.pyangbind_validation("vnfds", indata, force) @@ -713,22 +695,21 @@ class NsdTopic(DescriptorTopic): # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit return indata - def _check_descriptor_dependencies(self, session, descriptor, force=False): + def _check_descriptor_dependencies(self, session, descriptor): """ Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd connection points are ok - :param session: client session information + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param descriptor: descriptor to be inserted or edit - :param force: if true skip dependencies checking :return: None or raises exception """ - if force: + if session["force"]: return member_vnfd_index = {} - if descriptor.get("constituent-vnfd") and not force: + 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, write=False, show_all=True) + filter_q = self._get_project_filter(session) filter_q["id"] = vnfd_id vnf_list = self.db.get_list("vnfds", filter_q) if not vnf_list: @@ -760,23 +741,22 @@ class NsdTopic(DescriptorTopic): 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, force=False): - super().check_conflict_on_edit(session, final_content, edit_content, _id, force=force) + def check_conflict_on_edit(self, session, final_content, edit_content, _id): + super().check_conflict_on_edit(session, final_content, edit_content, _id) - self._check_descriptor_dependencies(session, final_content, force) + self._check_descriptor_dependencies(session, final_content) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note that NSD can be public and be used by other projects. - :param session: + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: vnfd inernal id - :param force: Avoid this checking :return: None or raises EngineException with the conflict """ - if force: + if session["force"]: return - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) _filter["nsdId"] = _id if self.db.get_list("nsrs", _filter): raise EngineException("There is some NSR that depends on this NSD", http_code=HTTPStatus.CONFLICT) @@ -816,7 +796,7 @@ class NstTopic(DescriptorTopic): def _check_descriptor_dependencies(self, session, descriptor): """ Check that the dependent descriptors exist on a new descriptor or edition - :param session: client session information + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param descriptor: descriptor to be inserted or edit :return: None or raises exception """ @@ -824,36 +804,35 @@ class NstTopic(DescriptorTopic): return for nsd in descriptor["netslice-subnet"]: nsd_id = nsd["nsd-ref"] - filter_q = self._get_project_filter(session, write=False, show_all=True) + filter_q = self._get_project_filter(session) filter_q["id"] = nsd_id if not self.db.get_list("nsds", filter_q): raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non " "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT) - def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): - super().check_conflict_on_edit(session, final_content, edit_content, _id, force=force) + def check_conflict_on_edit(self, session, final_content, edit_content, _id): + super().check_conflict_on_edit(session, final_content, edit_content, _id) self._check_descriptor_dependencies(session, final_content) - def check_conflict_on_del(self, session, _id, force=False): + def check_conflict_on_del(self, session, _id): """ Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note that NST can be public and be used by other projects. - :param session: + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: nst internal id - :param force: Avoid this checking :return: None or raises EngineException with the conflict """ # TODO: Check this method - if force: + if session["force"]: return # Get Network Slice Template from Database - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) _filter["_id"] = _id nst = self.db.get_one("nsts", _filter) # Search NSIs using NST via nst-ref - _filter = self._get_project_filter(session, write=False, show_all=False) + _filter = self._get_project_filter(session) _filter["nst-ref"] = nst["id"] nsis_list = self.db.get_list("nsis", _filter) for nsi_item in nsis_list: @@ -877,8 +856,8 @@ class PduTopic(BaseTopic): content["_admin"]["operationalState"] = "ENABLED" content["_admin"]["usageState"] = "NOT_IN_USE" - def check_conflict_on_del(self, session, _id, force=False): - if force: + def check_conflict_on_del(self, session, _id): + if session["force"]: return # TODO Is it needed to check descriptors _admin.project_read/project_write?? _filter = {"vdur.pdu-id": _id} diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 5e42858..5137735 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -166,25 +166,24 @@ class Engine(object): except (DbException, FsException, MsgException) as e: raise EngineException(str(e), http_code=e.http_code) - def new_item(self, rollback, session, topic, indata=None, kwargs=None, headers=None, force=False): + def new_item(self, rollback, session, topic, indata=None, kwargs=None, headers=None): """ Creates a new entry into database. For nsds and vnfds it creates an almost empty DISABLED entry, that must be completed with a call to method upload_content :param rollback: list to append created items at database in case a rollback must to be done - :param session: contains the used login username and working project + :param session: contains the used login username and working project, force to avoid checkins, public :param topic: it can be: users, projects, vim_accounts, sdns, nsrs, nsds, vnfds :param indata: data to be inserted :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks :return: _id: identity of the inserted data. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) with self.write_lock: - return self.map_topic[topic].new(rollback, session, indata, kwargs, headers, force) + return self.map_topic[topic].new(rollback, session, indata, kwargs, headers) - def upload_content(self, session, topic, _id, indata, kwargs, headers, force=False): + def upload_content(self, session, topic, _id, indata, kwargs, headers): """ Upload content for an already created entry (_id) :param session: contains the used login username and working project @@ -193,13 +192,12 @@ class Engine(object): :param indata: data to be inserted :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks :return: _id: identity of the inserted data. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) with self.write_lock: - return self.map_topic[topic].upload_content(session, _id, indata, kwargs, headers, force) + return self.map_topic[topic].upload_content(session, _id, indata, kwargs, headers) def get_item_list(self, session, topic, filter_q=None): """ @@ -252,21 +250,20 @@ class Engine(object): with self.write_lock: return self.map_topic[topic].delete_list(session, _filter) - def del_item(self, session, topic, _id, force=False): + def del_item(self, session, topic, _id): """ Delete item by its internal id :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 - :param force: indicates if deletion must be forced in case of conflict :return: dictionary with deleted item _id. It raises exception if not found. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) with self.write_lock: - return self.map_topic[topic].delete(session, _id, force) + return self.map_topic[topic].delete(session, _id) - def edit_item(self, session, topic, _id, indata=None, kwargs=None, force=False): + def edit_item(self, session, topic, _id, indata=None, kwargs=None): """ Update an existing entry at database :param session: contains the used login username and working project @@ -274,13 +271,12 @@ class Engine(object): :param _id: identifier to be updated :param indata: data to be inserted :param kwargs: used to override the indata descriptor - :param force: If True avoid some dependence checks :return: dictionary, raise exception if not found. """ if topic not in self.map_topic: raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) with self.write_lock: - return self.map_topic[topic].edit(session, _id, indata, kwargs, force) + return self.map_topic[topic].edit(session, _id, indata, kwargs) def create_admin_project(self): """ @@ -292,9 +288,9 @@ class Engine(object): if projects: return None project_desc = {"name": "admin"} - fake_session = {"project_id": "admin", "username": "admin", "admin": True} + fake_session = {"project_id": "admin", "username": "admin", "admin": True, "force": True, "public": None} rollback_list = [] - _id = self.map_topic["projects"].new(rollback_list, fake_session, project_desc, force=True) + _id = self.map_topic["projects"].new(rollback_list, fake_session, project_desc) return _id def create_admin_user(self): @@ -307,9 +303,9 @@ class Engine(object): return None # raise EngineException("Unauthorized. Database users is not empty", HTTPStatus.UNAUTHORIZED) user_desc = {"username": "admin", "password": "admin", "projects": ["admin"]} - fake_session = {"project_id": "admin", "username": "admin", "admin": True} + fake_session = {"project_id": "admin", "username": "admin", "admin": True, "force": True, "public": None} roolback_list = [] - _id = self.map_topic["users"].new(roolback_list, fake_session, user_desc, force=True) + _id = self.map_topic["users"].new(roolback_list, fake_session, user_desc) return _id def create_admin(self): diff --git a/osm_nbi/instance_topics.py b/osm_nbi/instance_topics.py index 55845d6..b7803b3 100644 --- a/osm_nbi/instance_topics.py +++ b/osm_nbi/instance_topics.py @@ -54,8 +54,8 @@ class NsrTopic(BaseTopic): BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) content["_admin"]["nsState"] = "NOT_INSTANTIATED" - def check_conflict_on_del(self, session, _id, force=False): - if force: + def check_conflict_on_del(self, session, _id): + if session["force"]: return nsr = self.db.get_one("nsrs", {"_id": _id}) if nsr["_admin"].get("nsState") == "INSTANTIATED": @@ -63,29 +63,13 @@ class NsrTopic(BaseTopic): "Launch 'terminate' operation first; or force deletion".format(_id), http_code=HTTPStatus.CONFLICT) - def delete(self, session, _id, force=False, dry_run=False): - """ - Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights - :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict - :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 - BaseTopic.delete(self, session, _id, force, dry_run=True) - if dry_run: - return - + def delete_extra(self, session, _id): self.fs.file_delete(_id, ignore_non_exist=True) - v = self.db.del_one("nsrs", {"_id": _id}) self.db.del_list("nslcmops", {"nsInstanceId": _id}) self.db.del_list("vnfrs", {"nsr-id-ref": _id}) # set all used pdus as free self.db.set_list("pdus", {"_admin.usage.nsr_id": _id}, {"_admin.usageState": "NOT_IN_USE", "_admin.usage": None}) - self._send_msg("deleted", {"_id": _id}) - return v @staticmethod def _format_ns_request(ns_request): @@ -148,16 +132,14 @@ class NsrTopic(BaseTopic): return additional_params - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Creates a new nsr into database. It also creates needed vnfrs :param rollback: list to append the created items at database in case a rollback must be done - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: params to be used for the nsr :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: the _id of nsr descriptor created at database """ @@ -165,13 +147,13 @@ class NsrTopic(BaseTopic): ns_request = self._remove_envelop(indata) # Override descriptor with query string kwargs self._update_input_with_kwargs(ns_request, kwargs) - self._validate_input_new(ns_request, force) + self._validate_input_new(ns_request, session["force"]) step = "" # look for nsr step = "getting nsd id='{}' from database".format(ns_request.get("nsdId")) _filter = {"_id": ns_request["nsdId"]} - _filter.update(BaseTopic._get_project_filter(session, write=False, show_all=True)) + _filter.update(BaseTopic._get_project_filter(session)) nsd = self.db.get_one("nsds", _filter) nsr_id = str(uuid4()) @@ -196,7 +178,7 @@ class NsrTopic(BaseTopic): "orchestration-progress": {}, # {"networks": {"active": 0, "total": 0}, "vms": {"active": 0, "total": 0}}, - "crete-time": now, + "create-time": now, "nsd-name-ref": nsd["name"], "operational-events": [], # "id", "timestamp", "description", "event", "nsd-ref": nsd["id"], @@ -338,13 +320,13 @@ 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=make_public) + BaseTopic.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) step = "creating nsr at database" - self.format_on_new(nsr_descriptor, session["project_id"], make_public=make_public) + self.format_on_new(nsr_descriptor, session["project_id"], make_public=session["public"]) self.db.create("nsrs", nsr_descriptor) rollback.append({"topic": "nsrs", "_id": nsr_id}) @@ -358,7 +340,7 @@ class NsrTopic(BaseTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) @@ -369,13 +351,13 @@ class VnfrTopic(BaseTopic): def __init__(self, db, fs, msg): BaseTopic.__init__(self, db, fs, msg) - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): raise EngineException("Method delete called directly", HTTPStatus.INTERNAL_SERVER_ERROR) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): # Not used because vnfrs are created and deleted by NsrTopic class directly raise EngineException("Method new called directly", HTTPStatus.INTERNAL_SERVER_ERROR) @@ -396,7 +378,7 @@ class NsLcmOpTopic(BaseTopic): def _check_ns_operation(self, session, nsr, operation, indata): """ Check that user has enter right parameters for the operation - :param session: + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param operation: it can be: instantiate, terminate, action, TODO: update, heal :param indata: descriptor with the parameters of the operation :return: None @@ -469,7 +451,7 @@ class NsLcmOpTopic(BaseTopic): if vim_account in vim_accounts: return try: - db_filter = self._get_project_filter(session, write=False, show_all=True) + db_filter = self._get_project_filter(session) db_filter["_id"] = vim_account self.db.get_one("vim_accounts", db_filter) except Exception: @@ -551,7 +533,7 @@ class NsLcmOpTopic(BaseTopic): (ip_address, ...) information. Modifies PDU _admin.usageState to 'IN_USE' - :param session: client session information + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param rollback: list with the database modifications to rollback if needed :param vnfr: vnfr to be updated. It is modified with pdu interface info if pdu is found :param vim_account: vim_account where this vnfr should be deployed @@ -572,7 +554,7 @@ class NsLcmOpTopic(BaseTopic): if not vdur.get("pdu-type"): continue pdu_type = vdur.get("pdu-type") - pdu_filter = self._get_project_filter(session, write=True, show_all=True) + pdu_filter = self._get_project_filter(session) pdu_filter["vim_accounts"] = vim_account pdu_filter["type"] = pdu_type pdu_filter["_admin.operationalState"] = "ENABLED" @@ -727,19 +709,16 @@ class NsLcmOpTopic(BaseTopic): } return nslcmop - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False, - slice_object=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None, slice_object=False): """ Performs a new operation over a ns :param rollback: list to append created items at database in case a rollback must to be done - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: descriptor with the parameters of the operation. It must contains among others nsInstanceId: _id of the nsr to perform the operation operation: it can be: instantiate, terminate, action, TODO: update, heal :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: id of the nslcmops """ try: @@ -750,7 +729,7 @@ class NsLcmOpTopic(BaseTopic): validate_input(indata, self.operation_schema[operation]) # get ns from nsr_id - _filter = BaseTopic._get_project_filter(session, write=True, show_all=False) + _filter = BaseTopic._get_project_filter(session) _filter["_id"] = nsInstanceId nsr = self.db.get_one("nsrs", _filter) @@ -763,7 +742,7 @@ class NsLcmOpTopic(BaseTopic): raise EngineException("ns_instance '{}' cannot be '{}' because it is not instantiated".format( nsInstanceId, operation), HTTPStatus.CONFLICT) else: - if operation == "instantiate" and not indata.get("force"): + if operation == "instantiate" and not session["force"]: raise EngineException("ns_instance '{}' cannot be '{}' because it is already instantiated".format( nsInstanceId, operation), HTTPStatus.CONFLICT) self._check_ns_operation(session, nsr, operation, indata) @@ -772,7 +751,7 @@ class NsLcmOpTopic(BaseTopic): self._update_vnfrs(session, rollback, nsr, indata) nslcmop_desc = self._create_nslcmop(nsInstanceId, operation, indata) - self.format_on_new(nslcmop_desc, session["project_id"], make_public=make_public) + self.format_on_new(nslcmop_desc, session["project_id"], make_public=session["public"]) _id = self.db.create("nslcmops", nslcmop_desc) rollback.append({"topic": "nslcmops", "_id": _id}) if not slice_object: @@ -783,10 +762,10 @@ class NsLcmOpTopic(BaseTopic): # except DbException as e: # raise EngineException("Cannot get ns_instance '{}': {}".format(e), HTTPStatus.NOT_FOUND) - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): raise EngineException("Method delete called directly", HTTPStatus.INTERNAL_SERVER_ERROR) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) @@ -827,7 +806,7 @@ class NsiTopic(BaseTopic): def _check_descriptor_dependencies(self, session, descriptor): """ Check that the dependent descriptors exist on a new descriptor or edition - :param session: client session information + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param descriptor: descriptor to be inserted or edit :return: None or raises exception """ @@ -838,12 +817,8 @@ class NsiTopic(BaseTopic): raise EngineException("Descriptor error at nst-ref='{}' references a non exist nstd".format(nstd_id), http_code=HTTPStatus.CONFLICT) - @staticmethod - def format_on_new(content, project_id=None, make_public=False): - BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) - - def check_conflict_on_del(self, session, _id, force=False): - if force: + def check_conflict_on_del(self, session, _id, ): + if session["force"]: return nsi = self.db.get_one("nsis", {"_id": _id}) if nsi["_admin"].get("nsiState") == "INSTANTIATED": @@ -851,17 +826,16 @@ class NsiTopic(BaseTopic): "Launch 'terminate' operation first; or force deletion".format(_id), http_code=HTTPStatus.CONFLICT) - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): """ Delete item by its internal _id - :param session: contains the used login username, working project, and admin rights + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id - :param force: indicates if deletion must be forced in case of conflict :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 - BaseTopic.delete(self, session, _id, force, dry_run=True) + BaseTopic.delete(self, session, _id, dry_run=True) if dry_run: return @@ -877,7 +851,7 @@ class NsiTopic(BaseTopic): if nsi: # last one using nsr continue try: - self.nsrTopic.delete(session, nsr_id, force=force, dry_run=False) + self.nsrTopic.delete(session, nsr_id, dry_run=False) except (DbException, EngineException) as e: if e.http_code == HTTPStatus.NOT_FOUND: pass @@ -900,16 +874,14 @@ class NsiTopic(BaseTopic): self.db.set_one("nsts", {"_id": nsir_admin["nst-id"]}, {"_admin.usageState": "NOT_IN_USE"}) return v - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Creates a new netslice instance record into database. It also creates needed nsrs and vnfrs :param rollback: list to append the created items at database in case a rollback must be done - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: params to be used for the nsir :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: the _id of nsi descriptor created at database """ @@ -917,13 +889,13 @@ class NsiTopic(BaseTopic): slice_request = self._remove_envelop(indata) # Override descriptor with query string kwargs self._update_input_with_kwargs(slice_request, kwargs) - self._validate_input_new(slice_request, force) + self._validate_input_new(slice_request, session["force"]) step = "" # look for nstd step = "getting nstd id='{}' from database".format(slice_request.get("nstId")) _filter = {"_id": slice_request["nstId"]} - _filter.update(BaseTopic._get_project_filter(session, write=False, show_all=True)) + _filter.update(BaseTopic._get_project_filter(session)) nstd = self.db.get_one("nsts", _filter) nstd.pop("_admin", None) nstd_id = nstd.pop("_id", None) @@ -946,7 +918,7 @@ class NsiTopic(BaseTopic): } step = "creating nsi at database" - self.format_on_new(nsi_descriptor, session["project_id"], make_public=make_public) + self.format_on_new(nsi_descriptor, session["project_id"], make_public=session["public"]) nsi_descriptor["_admin"]["nsiState"] = "NOT_INSTANTIATED" nsi_descriptor["_admin"]["netslice-subnet"] = None nsi_descriptor["_admin"]["deployed"] = {} @@ -1035,7 +1007,7 @@ class NsiTopic(BaseTopic): break # Creates Nsr objects - _id_nsr = self.nsrTopic.new(rollback, session, indata_ns, kwargs, headers, force) + _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"] @@ -1059,7 +1031,7 @@ class NsiTopic(BaseTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) @@ -1078,7 +1050,7 @@ class NsiLcmOpTopic(BaseTopic): def _check_nsi_operation(self, session, nsir, operation, indata): """ Check that user has enter right parameters for the operation - :param session: + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param operation: it can be: instantiate, terminate, action, TODO: update, heal :param indata: descriptor with the parameters of the operation :return: None @@ -1137,18 +1109,16 @@ class NsiLcmOpTopic(BaseTopic): # self.db.set_one("nsis", {"_id": nsir["_id"]}, nsir) self.db.set_one("nsis", {"_id": nsir["_id"]}, {"_admin.netslice-vld": nsir["_admin"].get("netslice-vld")}) - def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ Performs a new operation over a ns :param rollback: list to append created items at database in case a rollback must to be done - :param session: contains the used login username and working project + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param indata: descriptor with the parameters of the operation. It must contains among others nsiInstanceId: _id of the nsir to perform the operation operation: it can be: instantiate, terminate, action, TODO: update, heal :param kwargs: used to override the indata descriptor :param headers: http request headers - :param force: If True avoid some dependence checks - :param make_public: Make the created item public to all projects :return: id of the nslcmops """ try: @@ -1159,7 +1129,7 @@ class NsiLcmOpTopic(BaseTopic): validate_input(indata, self.operation_schema[operation]) # get nsi from nsiInstanceId - _filter = BaseTopic._get_project_filter(session, write=True, show_all=False) + _filter = BaseTopic._get_project_filter(session) _filter["_id"] = nsiInstanceId nsir = self.db.get_one("nsis", _filter) @@ -1172,7 +1142,7 @@ class NsiLcmOpTopic(BaseTopic): raise EngineException("netslice_instance '{}' cannot be '{}' because it is not instantiated".format( nsiInstanceId, operation), HTTPStatus.CONFLICT) else: - if operation == "instantiate" and not indata.get("force"): + if operation == "instantiate" and not session["force"]: raise EngineException("netslice_instance '{}' cannot be '{}' because it is already instantiated". format(nsiInstanceId, operation), HTTPStatus.CONFLICT) @@ -1216,7 +1186,7 @@ class NsiLcmOpTopic(BaseTopic): del indata_ns["key-pair-ref"] # 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, force, + nslcmop = self.nsi_NsLcmOpTopic.new(rollback, session, indata_ns, kwargs, headers, slice_object=True) nslcmops.append(nslcmop) if operation == "terminate": @@ -1235,7 +1205,7 @@ class NsiLcmOpTopic(BaseTopic): self._check_nsi_operation(session, nsir, operation, indata) nsilcmop_desc = self._create_nsilcmop(session, nsiInstanceId, operation, indata) - self.format_on_new(nsilcmop_desc, session["project_id"], make_public=make_public) + self.format_on_new(nsilcmop_desc, session["project_id"], make_public=session["public"]) _id = self.db.create("nsilcmops", nsilcmop_desc) rollback.append({"topic": "nsilcmops", "_id": _id}) self.msg.write("nsi", operation, nsilcmop_desc) @@ -1243,8 +1213,8 @@ class NsiLcmOpTopic(BaseTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def delete(self, session, _id, force=False, dry_run=False): + def delete(self, session, _id, dry_run=False): raise EngineException("Method delete called directly", HTTPStatus.INTERNAL_SERVER_ERROR) - def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + def edit(self, session, _id, indata=None, kwargs=None, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 705979b..fc7d11f 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -158,6 +158,12 @@ query string: exclude_default and include= … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the present specification for the particular resource, but that are not part of + Additionally it admits some administrator values: + FORCE: To force operations skipping dependency checkings + ADMIN: To act as an administrator or a different project + PUBLIC: To get public descriptors or set a descriptor as public + SET_PROJECT: To make a descriptor available for other project + Header field name Reference Example Descriptions Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response. This header field shall be present if the response is expected to have a non-empty message body. @@ -697,6 +703,78 @@ class Server(object): cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id) return + @staticmethod + def _manage_admin_query(session, kwargs, method, _id): + """ + Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT + Check that users has rights to use them and returs the admin_query + :param session: session rights obtained by token + :param kwargs: query string input. + :param method: http method: GET, POSST, PUT, ... + :param _id: + :return: admin_query dictionary with keys: + public: True, False or None + force: True or False + project_id: tuple with projects used for accessing an element + set_project: tuple with projects that a created element will belong to + method: show, list, delete, write + """ + admin_query = {"force": False, "project_id": (session["project_id"], ), "username": session["username"], + "admin": session["admin"], "public": None} + if kwargs: + # FORCE + if "FORCE" in kwargs: + if kwargs["FORCE"].lower() != "false": # if None or True set force to True + admin_query["force"] = True + del kwargs["FORCE"] + # PUBLIC + if "PUBLIC" in kwargs: + if kwargs["PUBLIC"].lower() != "false": # if None or True set public to True + admin_query["public"] = True + else: + admin_query["public"] = False + del kwargs["PUBLIC"] + # ADMIN + if "ADMIN" in kwargs: + behave_as = kwargs.pop("ADMIN") + if behave_as.lower() != "false": + if not session["admin"]: + raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED) + if not behave_as or behave_as.lower() == "true": # convert True, None to empty list + admin_query["project_id"] = () + elif isinstance(behave_as, (list, tuple)): + admin_query["project_id"] = behave_as + else: # isinstance(behave_as, str) + admin_query["project_id"] = (behave_as, ) + if "SET_PROJECT" in kwargs: + set_project = kwargs.pop("SET_PROJECT") + if not set_project: + admin_query["set_project"] = list(admin_query["project_id"]) + else: + if isinstance(set_project, str): + set_project = (set_project, ) + if admin_query["project_id"]: + for p in set_project: + if p not in admin_query["project_id"]: + raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or " + "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED) + admin_query["set_project"] = set_project + + # PROJECT_READ + # if "PROJECT_READ" in kwargs: + # admin_query["project"] = kwargs.pop("project") + # if admin_query["project"] == session["project_id"]: + if method == "GET": + if _id: + admin_query["method"] = "show" + else: + admin_query["method"] = "list" + elif method == "DELETE": + admin_query["method"] = "delete" + else: + admin_query["method"] = "write" + return admin_query + @cherrypy.expose def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs): session = None @@ -720,16 +798,15 @@ class Server(object): method = kwargs.pop("METHOD") else: method = cherrypy.request.method - if kwargs and "FORCE" in kwargs: - force = kwargs.pop("FORCE") - else: - force = False + self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args) + if main_topic == "admin" and topic == "tokens": return self.token(method, _id, kwargs) # self.engine.load_dbase(cherrypy.request.app.config) session = self.authenticator.authorize() + session = self._manage_admin_query(session, kwargs, method, _id) indata = self._format_in(kwargs) engine_topic = topic if topic == "subscriptions": @@ -755,7 +832,7 @@ class Server(object): engine_topic = "nsilcmops" elif main_topic == "pdu": engine_topic = "pdus" - if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future + if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future engine_topic = "vim_accounts" if method == "GET": @@ -782,10 +859,9 @@ class Server(object): 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, session, engine_topic, {}, None, cherrypy.request.headers, - force=force) + _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers) completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs, - cherrypy.request.headers, force=force) + cherrypy.request.headers) if completed: self._set_location_header(main_topic, version, topic, _id) else: @@ -793,7 +869,7 @@ class Server(object): outdata = {"id": _id} elif topic == "ns_instances_content": # creates NSR - _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force) + _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs) # creates nslcmop indata["lcmOperationType"] = "instantiate" indata["nsInstanceId"] = _id @@ -809,7 +885,7 @@ class Server(object): cherrypy.response.status = HTTPStatus.ACCEPTED.value elif topic == "netslice_instances_content": # creates NetSlice_Instance_record (NSIR) - _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force) + _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs) self._set_location_header(main_topic, version, topic, _id) indata["lcmOperationType"] = "instantiate" indata["nsiInstanceId"] = _id @@ -825,7 +901,7 @@ class Server(object): cherrypy.response.status = HTTPStatus.ACCEPTED.value else: _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, - cherrypy.request.headers, force=force) + cherrypy.request.headers) self._set_location_header(main_topic, version, topic, _id) outdata = {"id": _id} # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages") @@ -837,7 +913,7 @@ class Server(object): cherrypy.response.status = HTTPStatus.OK.value else: # len(args) > 1 delete_in_process = False - if topic == "ns_instances_content" and not force: + if topic == "ns_instances_content" and not session["force"]: nslcmop_desc = { "lcmOperationType": "terminate", "nsInstanceId": _id, @@ -848,7 +924,7 @@ class Server(object): delete_in_process = True outdata = {"_id": opp_id} cherrypy.response.status = HTTPStatus.ACCEPTED.value - elif topic == "netslice_instances_content" and not force: + elif topic == "netslice_instances_content" and not session["force"]: nsilcmop_desc = { "lcmOperationType": "terminate", "nsiInstanceId": _id, @@ -860,23 +936,23 @@ class Server(object): outdata = {"_id": opp_id} cherrypy.response.status = HTTPStatus.ACCEPTED.value if not delete_in_process: - self.engine.del_item(session, engine_topic, _id, force) + self.engine.del_item(session, engine_topic, _id) cherrypy.response.status = HTTPStatus.NO_CONTENT.value if engine_topic in ("vim_accounts", "wim_accounts", "sdns"): cherrypy.response.status = HTTPStatus.ACCEPTED.value elif method in ("PUT", "PATCH"): outdata = None - if not indata and not kwargs: + if not indata and not kwargs and not session.get("set_project"): raise NbiException("Nothing to update. Provide payload and/or query string", HTTPStatus.BAD_REQUEST) if item in ("nsd_content", "package_content", "nst_content") and method == "PUT": completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs, - cherrypy.request.headers, force=force) + cherrypy.request.headers) if not completed: cherrypy.response.headers["Transaction-Id"] = id else: - self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force) + self.engine.edit_item(session, engine_topic, _id, indata, kwargs) cherrypy.response.status = HTTPStatus.NO_CONTENT.value else: raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED) @@ -1032,7 +1108,8 @@ def _stop_service(): TODO: Ending database connections. """ global subscription_thread - subscription_thread.terminate() + if subscription_thread: + subscription_thread.terminate() subscription_thread = None cherrypy.tree.apps['/osm'].root.engine.stop() cherrypy.log.error("Stopping osm_nbi") diff --git a/osm_nbi/subscriptions.py b/osm_nbi/subscriptions.py index 8132fe9..7df78d2 100644 --- a/osm_nbi/subscriptions.py +++ b/osm_nbi/subscriptions.py @@ -48,7 +48,7 @@ class SubscriptionThread(threading.Thread): :param engine: an instance of Engine class, used for deleting instances """ threading.Thread.__init__(self) - + self.to_terminate = False self.config = config self.db = None self.msg = None @@ -97,16 +97,17 @@ class SubscriptionThread(threading.Thread): raise SubscriptionException(str(e), http_code=e.http_code) self.logger.debug("Starting") - while True: + while not self.to_terminate: try: self.aiomain_task = asyncio.ensure_future(self.msg.aioread(("ns", "nsi"), loop=self.loop, callback=self._msg_callback), loop=self.loop) self.loop.run_until_complete(self.aiomain_task) - except asyncio.CancelledError: - break # if cancelled it should end, breaking loop + # except asyncio.CancelledError: + # break # if cancelled it should end, breaking loop except Exception as e: - self.logger.exception("Exception '{}' at messaging read loop".format(e), exc_info=True) + if not self.to_terminate: + self.logger.exception("Exception '{}' at messaging read loop".format(e), exc_info=True) self.logger.debug("Finishing") self._stop() @@ -160,4 +161,5 @@ class SubscriptionThread(threading.Thread): but not immediately. :return: None """ + self.to_terminate = True self.loop.call_soon_threadsafe(self.aiomain_task.cancel) diff --git a/osm_nbi/tests/test.py b/osm_nbi/tests/test.py index 4c6b069..6f30cfe 100755 --- a/osm_nbi/tests/test.py +++ b/osm_nbi/tests/test.py @@ -596,6 +596,134 @@ class TestUsersProjects: engine.remove_authorization() # To finish +class TestProjectsDescriptors: + description = "test descriptors visibility among projects" + + @staticmethod + def run(engine, test_osm, manual_check, test_params=None): + vnfd_ids = [] + engine.set_test_name("ProjectDescriptors") + engine.get_autorization() + 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") + engine.test("Create project P2", "POST", "/admin/v1/projects", headers_json, {"name": "P2"}, + (201, 204), {"Location": "/admin/v1/projects/", "Content-Type": "application/json"}, "json") + engine.test("Create project P3", "POST", "/admin/v1/projects", headers_json, {"name": "P3"}, + (201, 204), {"Location": "/admin/v1/projects/", "Content-Type": "application/json"}, "json") + + engine.test("Create user U1", "POST", "/admin/v1/users", headers_json, + {"username": "U1", "projects": ["Padmin", "P2", "P3"], "password": "pw1"}, 201, + {"Location": "/admin/v1/users/", "Content-Type": "application/json"}, "json") + + engine.test("Onboard VNFD id1", "POST", "/vnfpkgm/v1/vnf_packages_content?id=id1", headers_yaml, + TestDescriptors.vnfd_empty, 201, r_headers_yaml_location_vnfd, "yaml") + vnfd_ids.append(engine.last_id) + engine.test("Onboard VNFD id2 PUBLIC", "POST", "/vnfpkgm/v1/vnf_packages_content?id=id2&PUBLIC=TRUE", + headers_yaml, TestDescriptors.vnfd_empty, 201, r_headers_yaml_location_vnfd, "yaml") + vnfd_ids.append(engine.last_id) + engine.test("Onboard VNFD id3", "POST", "/vnfpkgm/v1/vnf_packages_content?id=id3&PUBLIC=FALSE", headers_yaml, + TestDescriptors.vnfd_empty, 201, r_headers_yaml_location_vnfd, "yaml") + vnfd_ids.append(engine.last_id) + + res = engine.test("Get VNFD descriptors", "GET", "/vnfpkgm/v1/vnf_packages?id=id1,id2,id3", + headers_json, None, 200, r_header_json, "json") + response = res.json() + if len(response) != 3: + logger.error("Only 3 vnfds should be present for project admin. {} listed".format(len(response))) + engine.failed_tests += 1 + + # Change to other project Padmin + res = engine.test("Change to user U1 project Padmin", "POST", "/admin/v1/tokens", headers_json, + {"username": "U1", "password": "pw1", "project_id": "Padmin"}, (200, 201), + r_header_json, "json") + if res: + response = res.json() + engine.set_header({"Authorization": "Bearer {}".format(response["id"])}) + + # list vnfds + res = engine.test("List VNFD descriptors for Padmin", "GET", "/vnfpkgm/v1/vnf_packages", + headers_json, None, 200, r_header_json, "json") + response = res.json() + if len(response) != 0: + logger.error("Only 0 vnfds should be present for project Padmin. {} listed".format(len(response))) + engine.failed_tests += 1 + + # list Public vnfds + res = engine.test("List VNFD public descriptors", "GET", "/vnfpkgm/v1/vnf_packages?PUBLIC=True", + headers_json, None, 200, r_header_json, "json") + response = res.json() + if len(response) != 1: + logger.error("Only 1 vnfds should be present for project Padmin. {} listed".format(len(response))) + 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", + headers_json, None, 200, r_header_json, "json") + response = res.json() + if len(response) != 3: + logger.error("Only 3 vnfds should be present for project Padmin. {} listed".format(len(response))) + engine.failed_tests += 1 + + # Get Public vnfds + engine.test("Get VNFD public descriptors", "GET", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[1]), + headers_json, None, 200, r_header_json, "json") + # Edit not owned vnfd + engine.test("Edit VNFD ", "PATCH", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[0]), + headers_yaml, '{name: pepe}', 404, r_header_yaml, "yaml") + + # Add to my catalog + engine.test("Add VNFD id2 to my catalog", "PATCH", "/vnfpkgm/v1/vnf_packages/{}?SET_PROJECT". + format(vnfd_ids[1]), headers_json, None, 204, None, 0) + + # Add a new vnfd + engine.test("Onboard VNFD id4", "POST", "/vnfpkgm/v1/vnf_packages_content?id=id4", headers_yaml, + TestDescriptors.vnfd_empty, 201, r_headers_yaml_location_vnfd, "yaml") + vnfd_ids.append(engine.last_id) + + # list vnfds + res = engine.test("List VNFD public descriptors", "GET", "/vnfpkgm/v1/vnf_packages", + headers_json, None, 200, r_header_json, "json") + response = res.json() + if len(response) != 2: + logger.error("Only 2 vnfds should be present for project Padmin. {} listed".format(len(response))) + engine.failed_tests += 1 + + if manual_check: + input('VNFDs have been omboarded. Perform manual check and press enter to resume') + + test_rest.test("Delete VNFD id2", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[1]), + headers_yaml, None, 204, None, 0) + + # change to admin project + engine.remove_authorization() # To force get authorization + engine.get_autorization() + test_rest.test("Delete VNFD id1", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[0]), + headers_yaml, None, 204, None, 0) + test_rest.test("Delete VNFD id2", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[1]), + headers_yaml, None, 204, None, 0) + test_rest.test("Delete VNFD id3", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[2]), + headers_yaml, None, 204, None, 0) + test_rest.test("Delete VNFD id4", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_ids[3]), + headers_yaml, None, 404, r_header_yaml, "yaml") + test_rest.test("Delete VNFD id4", "DELETE", "/vnfpkgm/v1/vnf_packages/{}?ADMIN".format(vnfd_ids[3]), + headers_yaml, None, 204, None, 0) + # Get Public vnfds + engine.test("Get VNFD deleted id1", "GET", "/vnfpkgm/v1/vnf_packages/{}?ADMIN".format(vnfd_ids[0]), + headers_json, None, 404, r_header_json, "json") + engine.test("Get VNFD deleted id2", "GET", "/vnfpkgm/v1/vnf_packages/{}?ADMIN".format(vnfd_ids[1]), + headers_json, None, 404, r_header_json, "json") + engine.test("Get VNFD deleted id3", "GET", "/vnfpkgm/v1/vnf_packages/{}?ADMIN".format(vnfd_ids[2]), + headers_json, None, 404, r_header_json, "json") + engine.test("Get VNFD deleted id4", "GET", "/vnfpkgm/v1/vnf_packages/{}?ADMIN".format(vnfd_ids[3]), + headers_json, None, 404, r_header_json, "json") + + engine.test("Delete user U1", "DELETE", "/admin/v1/users/U1", headers_json, None, 204, None, None) + engine.test("Delete project Padmin", "DELETE", "/admin/v1/projects/Padmin", headers_json, None, 204, None, None) + engine.test("Delete project P2", "DELETE", "/admin/v1/projects/P2", headers_json, None, 204, None, None) + engine.test("Delete project P3", "DELETE", "/admin/v1/projects/P3", headers_json, None, 204, None, None) + + class TestFakeVim: description = "Creates/edit/delete fake VIMs and SDN controllers" @@ -1323,7 +1451,7 @@ class TestDeployHackfest3Charmed(TestDeploy): self.users = {'1': "ubuntu", '2': "ubuntu"} self.passwords = {'1': "osm4u", '2': "osm4u"} self.descriptor_edit = { - "vnfd0": yaml.full_load( + "vnfd0": yaml.safe_load( """ vnf-configuration: terminate-config-primitive: @@ -1845,6 +1973,31 @@ class TestDeployHnfd(TestDeployHackfest3Charmed): class TestDescriptors: description = "Test VNFD, NSD, PDU descriptors CRUD and dependencies" + vnfd_empty = """vnfd:vnfd-catalog: + vnfd: + - name: prova + short-name: prova + id: prova + """ + vnfd_prova = """vnfd:vnfd-catalog: + vnfd: + - connection-point: + - name: cp_0h8m + type: VPORT + id: prova + name: prova + short-name: prova + vdu: + - id: vdu_z4bm + image: ubuntu + interface: + - external-connection-point-ref: cp_0h8m + name: eth0 + virtual-interface: + type: VIRTIO + name: vdu_z4bm + version: '1.0' + """ def __init__(self): self.vnfd_filename = "hackfest_3charmed_vnfd.tar.gz" @@ -1852,31 +2005,6 @@ class TestDescriptors: self.descriptor_url = "https://osm-download.etsi.org/ftp/osm-3.0-three/2nd-hackfest/packages/" self.vnfd_id = None self.nsd_id = None - self.vnfd_empty = """vnfd:vnfd-catalog: - vnfd: - - name: prova - short-name: prova - id: prova - """ - self.vnfd_prova = """vnfd:vnfd-catalog: - vnfd: - - connection-point: - - name: cp_0h8m - type: VPORT - id: prova - name: prova - short-name: prova - vdu: - - id: vdu_z4bm - image: ubuntu - interface: - - external-connection-point-ref: cp_0h8m - name: eth0 - virtual-interface: - type: VIRTIO - name: vdu_z4bm - version: '1.0' - """ def run(self, engine, test_osm, manual_check, test_params=None): engine.set_test_name("Descriptors") @@ -2295,7 +2423,8 @@ if __name__ == "__main__": test_classes = { "NonAuthorized": TestNonAuthorized, "FakeVIM": TestFakeVim, - "TestUsersProjects": TestUsersProjects, + "Users-Projects": TestUsersProjects, + "Projects-Descriptors": TestProjectsDescriptors, "VIM-SDN": TestVIMSDN, "Deploy-Custom": TestDeploy, "Deploy-Hackfest-Cirros": TestDeployHackfestCirros, @@ -2305,15 +2434,15 @@ if __name__ == "__main__": "Deploy-Hackfest-3Charmed3": TestDeployHackfest3Charmed3, "Deploy-Hackfest-4": TestDeployHackfest4, "Deploy-CirrosMacIp": TestDeployIpMac, - "TestDescriptors": TestDescriptors, - "TestDeployHackfest1": TestDeployHackfest1, + "Descriptors": TestDescriptors, + "Deploy-Hackfest1": TestDeployHackfest1, # "Deploy-MultiVIM": TestDeployMultiVIM, - "DeploySingleVdu": TestDeploySingleVdu, - "DeployHnfd": TestDeployHnfd, + "Deploy-SingleVdu": TestDeploySingleVdu, + "Deploy-Hnfd": TestDeployHnfd, "Upload-Slice-Template": TestNetSliceTemplates, "Deploy-Slice-Instance": TestNetSliceInstances, - "TestDeploySimpleCharm": TestDeploySimpleCharm, - "TestDeploySimpleCharm2": TestDeploySimpleCharm2, + "Deploy-SimpleCharm": TestDeploySimpleCharm, + "Deploy-SimpleCharm2": TestDeploySimpleCharm2, } test_to_do = [] test_params = {} -- 2.17.1