Register operations for VIM, WIM, SDNC 34/7734/4
authortierno <alfonso.tiernosepulveda@telefonica.com>
Mon, 1 Jul 2019 15:36:49 +0000 (15:36 +0000)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Tue, 2 Jul 2019 12:58:19 +0000 (12:58 +0000)
provide http ACCEPTED 202 on asynchronous operations

Change-Id: I448dd90e74eca736af5b9e1f3f65bc45992228f0
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
osm_nbi/admin_topics.py
osm_nbi/base_topic.py
osm_nbi/descriptor_topics.py
osm_nbi/engine.py
osm_nbi/instance_topics.py
osm_nbi/nbi.py
osm_nbi/tests/test.py
osm_nbi/validation.py

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