PATCH support for enabling and disabling of NSDs and VNFDs.
[osm/NBI.git] / osm_nbi / base_topic.py
index 52e02af..216c9df 100644 (file)
@@ -19,6 +19,7 @@ from http import HTTPStatus
 from time import time
 from osm_common.dbbase import deep_update_rfc7396
 from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
 from time import time
 from osm_common.dbbase import deep_update_rfc7396
 from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
+from yaml import safe_load, YAMLError
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
@@ -68,6 +69,7 @@ class BaseTopic:
     # static variables for all instance classes
     topic = None        # to_override
     topic_msg = None    # to_override
     # static variables for all instance classes
     topic = None        # to_override
     topic_msg = None    # to_override
+    quota_name = None   # to_override. If not provided topic will be used for quota_name
     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
     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
@@ -106,24 +108,26 @@ class BaseTopic:
         """
         Check whether topic quota is exceeded by the given project
         Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed
         """
         Check whether topic quota is exceeded by the given project
         Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed
-        :param projects: projects (tuple) for which quota should be checked
-        :param override: boolean. If true, don't raise ValidationError even though quota be exceeded
+        :param session[project_id]: projects (tuple) for which quota should be checked
+        :param session[force]: boolean. If true, skip quota checking
         :return: None
         :raise:
             DbException if project not found
         :return: None
         :raise:
             DbException if project not found
-            ValidationError if quota exceeded and not overridden
+            ValidationError if quota exceeded in one of the projects
         """
         """
-        if session["force"] or session["admin"]:
+        if session["force"]:
             return
         projects = session["project_id"]
         for project in projects:
             proj = self.auth.get_project(project)
             pid = proj["_id"]
             return
         projects = session["project_id"]
         for project in projects:
             proj = self.auth.get_project(project)
             pid = proj["_id"]
-            quota = proj.get("quotas", {}).get(self.topic, self.default_quota)
+            quota_name = self.quota_name or self.topic
+            quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
             count = self.db.count(self.topic, {"_admin.projects_read": pid})
             if count >= quota:
                 name = proj["name"]
             count = self.db.count(self.topic, {"_admin.projects_read": pid})
             if count >= quota:
                 name = proj["name"]
-                raise ValidationError("{} quota ({}) exceeded for project {} ({})".format(self.topic, quota, name, pid))
+                raise ValidationError("quota ({}={}) exceeded for project {} ({})".format(quota_name, quota, name, pid),
+                                      http_code=HTTPStatus.UNAUTHORIZED)
 
     def _validate_input_new(self, input, force=False):
         """
 
     def _validate_input_new(self, input, force=False):
         """
@@ -136,7 +140,7 @@ class BaseTopic:
             validate_input(input, self.schema_new)
         return input
 
             validate_input(input, self.schema_new)
         return input
 
-    def _validate_input_edit(self, input, force=False):
+    def _validate_input_edit(self, input, content, force=False):
         """
         Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
         :param input: user input content for the new topic
         """
         Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
         :param input: user input content for the new topic
@@ -151,7 +155,7 @@ class BaseTopic:
     def _get_project_filter(session):
         """
         Generates a filter dictionary for querying database, so that only allowed items for this project can be
     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
+        addressed. Only proprietary 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:
             project_id: project list this session has rights to access. Can be empty, one or several
         not present or contains ANY mean public.
         :param session: contains:
             project_id: project list this session has rights to access. Can be empty, one or several
@@ -301,11 +305,12 @@ class BaseTopic:
         pass
 
     @staticmethod
         pass
 
     @staticmethod
-    def _update_input_with_kwargs(desc, kwargs):
+    def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
         """
         Update descriptor with the kwargs. It contains dot separated keys
         :param desc: dictionary to be updated
         :param kwargs: plain dictionary to be used for updating.
         """
         Update descriptor with the kwargs. It contains dot separated keys
         :param desc: dictionary to be updated
         :param kwargs: plain dictionary to be used for updating.
+        :param yaml_format: get kwargs values as yaml format.
         :return: None, 'desc' is modified. It raises EngineException.
         """
         if not kwargs:
         :return: None, 'desc' is modified. It raises EngineException.
         """
         if not kwargs:
@@ -320,12 +325,23 @@ class BaseTopic:
                         update_content = update_content[kitem_old]
                     if isinstance(update_content, dict):
                         kitem_old = kitem
                         update_content = update_content[kitem_old]
                     if isinstance(update_content, dict):
                         kitem_old = kitem
+                        if not isinstance(update_content.get(kitem_old), (dict, list)):
+                            update_content[kitem_old] = {}
                     elif isinstance(update_content, list):
                     elif isinstance(update_content, list):
+                        # key must be an index of the list, must be integer
                         kitem_old = int(kitem)
                         kitem_old = int(kitem)
+                        # if index greater than list, extend the list
+                        if kitem_old >= len(update_content):
+                            update_content += [None] * (kitem_old - len(update_content) + 1)
+                        if not isinstance(update_content[kitem_old], (dict, list)):
+                            update_content[kitem_old] = {}
                     else:
                         raise EngineException(
                             "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem))
                     else:
                         raise EngineException(
                             "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem))
-                update_content[kitem_old] = v
+                if v is None:
+                    del update_content[kitem_old]
+                else:
+                    update_content[kitem_old] = v if not yaml_format else safe_load(v)
         except KeyError:
             raise EngineException(
                 "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old))
         except KeyError:
             raise EngineException(
                 "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old))
@@ -335,6 +351,8 @@ class BaseTopic:
         except IndexError:
             raise EngineException(
                 "Invalid query string '{}'. Index '{}' out of  range".format(k, kitem_old))
         except IndexError:
             raise EngineException(
                 "Invalid query string '{}'. Index '{}' out of  range".format(k, kitem_old))
+        except YAMLError:
+            raise EngineException("Invalid query string '{}' yaml format".format(k))
 
     def show(self, session, _id):
         """
 
     def show(self, session, _id):
         """
@@ -464,30 +482,39 @@ class BaseTopic:
         """
 
         # To allow addressing projects and users by name AS WELL AS by _id
         """
 
         # To allow addressing projects and users by name AS WELL AS by _id
-        filter_q = {BaseTopic.id_field(self.topic, _id): _id}
+        if not self.multiproject:
+            filter_q = {}
+        else:
+            filter_q = self._get_project_filter(session)
+        filter_q[self.id_field(self.topic, _id)] = _id
         item_content = self.db.get_one(self.topic, filter_q)
 
         item_content = self.db.get_one(self.topic, filter_q)
 
-        # TODO add admin to filter, validate rights
-        # data = self.get_item(topic, _id)
         self.check_conflict_on_del(session, _id, item_content)
         if dry_run:
             return None
         
         self.check_conflict_on_del(session, _id, item_content)
         if dry_run:
             return None
         
-        if self.multiproject:
-            filter_q.update(self._get_project_filter(session))
         if self.multiproject and session["project_id"]:
         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"]:
+            # remove reference from project_read if there are more projects referencing it. If it last one,
+            # do not remove reference, but delete
+            other_projects_referencing = next((p for p in item_content["_admin"]["projects_read"]
+                                               if p not in session["project_id"] and p != "ANY"), None)
+
+            # check if there are projects referencing it (apart from ANY, that means, public)....
+            if other_projects_referencing:
+                # remove references but not delete
+                update_dict_pull = {"_admin.projects_read": session["project_id"],
+                                    "_admin.projects_write": session["project_id"]}
+                self.db.set_one(self.topic, filter_q, update_dict=None, pull_list=update_dict_pull)
                 return None
                 return None
-        else:
-            self.db.del_one(self.topic, filter_q)
+            else:
+                can_write = next((p for p in item_content["_admin"]["projects_write"] if p == "ANY" or
+                                  p in session["project_id"]), None)
+                if not can_write:
+                    raise EngineException("You have not write permission to delete it",
+                                          http_code=HTTPStatus.UNAUTHORIZED)
+
+        # delete
+        self.db.del_one(self.topic, filter_q)
         self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
         self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
         return None
         self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
         self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
         return None
@@ -511,11 +538,13 @@ class BaseTopic:
             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)
             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)
             # TODO self._check_edition(session, indata, _id, force)
             if not content:
                 content = self.show(session, _id)
+            
+            indata = self._validate_input_edit(indata, content, force=session["force"])
+            
             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
             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