Final A1 V1.0.0 Release (but 1.1.0 to come)
[ric-plt/a1.git] / a1 / data.py
index d701373..d320046 100644 (file)
@@ -24,7 +24,7 @@ For now, the database is in memory.
 We use dict data structures (KV) with the expectation of having to move this into Redis
 """
 import json
-from a1.exceptions import PolicyTypeNotFound, PolicyInstanceNotFound, PolicyTypeAlreadyExists
+from a1.exceptions import PolicyTypeNotFound, PolicyInstanceNotFound, PolicyTypeAlreadyExists, CantDeleteNonEmptyType
 from a1 import get_module_logger
 from a1 import a1rmr
 
@@ -38,9 +38,89 @@ H = "handlers"
 D = "data"
 
 
+# Internal helpers
+
+
+def _get_statuses(policy_type_id, policy_instance_id):
+    """
+    shared helper to get statuses for an instance
+    """
+    instance_is_valid(policy_type_id, policy_instance_id)
+    return [v for _, v in POLICY_DATA[policy_type_id][I][policy_instance_id][H].items()]
+
+
+def _get_instance_list(policy_type_id):
+    """
+    shared helper to get instance list for a type
+    """
+    type_is_valid(policy_type_id)
+    return list(POLICY_DATA[policy_type_id][I].keys())
+
+
+def _clean_up_type(policy_type_id):
+    """
+    pop through a1s mailbox, updating a1s db of all policy statuses
+    for all instances of type, see if it can be deleted
+    """
+    type_is_valid(policy_type_id)
+    for msg in a1rmr.dequeue_all_waiting_messages([21024]):
+        # try to parse the messages as responses. Drop those that are malformed
+        pay = json.loads(msg["payload"])
+        if "policy_type_id" in pay and "policy_instance_id" in pay and "handler_id" in pay and "status" in pay:
+            """
+            NOTE: can't raise an exception here e.g.:
+                instance_is_valid(pti, pii)
+            because this is called on many functions; just drop bad status messages.
+            We def don't want bad messages that happen to hit a1s mailbox to blow up anything
+
+            NOTE2: we don't use the parameters "policy_type_id, policy_instance" from above here,
+            # because we are popping the whole mailbox, which might include other statuses
+            """
+            pti = pay["policy_type_id"]
+            pii = pay["policy_instance_id"]
+            if pti in POLICY_DATA and pii in POLICY_DATA[pti][I]:  # manual check per comment above
+                POLICY_DATA[pti][I][pii][H][pay["handler_id"]] = pay["status"]
+        else:
+            logger.debug("Dropping message")
+            logger.debug(pay)
+
+    for policy_instance_id in _get_instance_list(policy_type_id):
+        # see if we can delete
+        vector = _get_statuses(policy_type_id, policy_instance_id)
+
+        """
+        TODO: not being able to delete if the list is [] is prolematic.
+        There are cases, such as a bad routing file, where this type will never be able to be deleted because it never went to any xapps
+        However, A1 cannot distinguish between the case where [] was never going to work, and the case where it hasn't worked *yet*
+
+        However, removing this constraint also leads to problems.
+        Deleting the instance when the vector is empty, for example doing so “shortly after” the PUT, can lead to a worse race condition where the xapps get the policy after that, implement it, but because the DELETE triggered “too soon”, you can never get the status or do the delete on it again, so the xapps are all implementing the instance roguely.
+
+        This requires some thought to address.
+        For now we stick with the "less bad problem".
+        """
+        if vector != []:
+            all_deleted = True
+            for i in vector:
+                if i != "DELETED":
+                    all_deleted = False
+                    break  # have at least one not DELETED, do nothing
+
+            # blow away from a1 db
+            if all_deleted:
+                del POLICY_DATA[policy_type_id][I][policy_instance_id]
+
+
 # Types
 
 
+def get_type_list():
+    """
+    retrieve all type ids
+    """
+    return list(POLICY_DATA.keys())
+
+
 def type_is_valid(policy_type_id):
     """
     check that a type is valid
@@ -62,19 +142,23 @@ def store_policy_type(policy_type_id, body):
     POLICY_DATA[policy_type_id][I] = {}
 
 
-def get_policy_type(policy_type_id):
+def delete_policy_type(policy_type_id):
     """
-    retrieve a type
+    delete a policy type; can only be done if there are no instances (business logic)
     """
-    type_is_valid(policy_type_id)
-    return POLICY_DATA[policy_type_id][D]
+    pil = get_instance_list(policy_type_id)
+    if pil == []:  # empty, can delete
+        del POLICY_DATA[policy_type_id]
+    else:
+        raise CantDeleteNonEmptyType()
 
 
-def get_type_list():
+def get_policy_type(policy_type_id):
     """
-    retrieve all type ids
+    retrieve a type
     """
-    return list(POLICY_DATA.keys())
+    type_is_valid(policy_type_id)
+    return POLICY_DATA[policy_type_id][D]
 
 
 # Instances
@@ -102,45 +186,11 @@ def store_policy_instance(policy_type_id, policy_instance_id, instance):
     POLICY_DATA[policy_type_id][I][policy_instance_id][H] = {}
 
 
-def delete_policy_instance_if_applicable(policy_type_id, policy_instance_id):
-    """
-    delete a policy instance if all known statuses are DELETED
-
-    pops a1s waiting mailbox
-    """
-    # pop through a1s mailbox, updating a1s db of all policy statuses
-    for msg in a1rmr.dequeue_all_waiting_messages([21024]):
-        # try to parse the messages as responses. Drop those that are malformed
-        # NOTE: we don't use the parameters "policy_type_id, policy_instance" from above here,
-        # because we are popping the whole mailbox, which might include other statuses
-        pay = json.loads(msg["payload"])
-        if "policy_type_id" in pay and "policy_instance_id" in pay and "handler_id" in pay and "status" in pay:
-            set_policy_instance_status(pay["policy_type_id"], pay["policy_instance_id"], pay["handler_id"], pay["status"])
-        else:
-            logger.debug("Dropping message")
-            logger.debug(pay)
-
-    # raise if not valid
-    instance_is_valid(policy_type_id, policy_instance_id)
-
-    # see if we can delete
-    vector = get_policy_instance_statuses(policy_type_id, policy_instance_id)
-    if vector != []:
-        all_deleted = True
-        for i in vector:
-            if i != "DELETED":
-                all_deleted = False
-                break  # have at least one not DELETED, do nothing
-
-        # blow away from a1 db
-        if all_deleted:
-            del POLICY_DATA[policy_type_id][I][policy_instance_id]
-
-
 def get_policy_instance(policy_type_id, policy_instance_id):
     """
     Retrieve a policy instance
     """
+    _clean_up_type(policy_type_id)
     instance_is_valid(policy_type_id, policy_instance_id)
     return POLICY_DATA[policy_type_id][I][policy_instance_id][D]
 
@@ -149,23 +199,13 @@ def get_policy_instance_statuses(policy_type_id, policy_instance_id):
     """
     Retrieve the status vector for a policy instance
     """
-    instance_is_valid(policy_type_id, policy_instance_id)
-
-    return [v for _, v in POLICY_DATA[policy_type_id][I][policy_instance_id][H].items()]
-
-
-def set_policy_instance_status(policy_type_id, policy_instance_id, handler_id, status):
-    """
-    Update the status of a handler id of a policy instance
-    """
-    instance_is_valid(policy_type_id, policy_instance_id)
-
-    POLICY_DATA[policy_type_id][I][policy_instance_id][H][handler_id] = status
+    _clean_up_type(policy_type_id)
+    return _get_statuses(policy_type_id, policy_instance_id)
 
 
 def get_instance_list(policy_type_id):
     """
     retrieve all instance ids for a type
     """
-    type_is_valid(policy_type_id)
-    return list(POLICY_DATA[policy_type_id][I].keys())
+    _clean_up_type(policy_type_id)
+    return _get_instance_list(policy_type_id)