Final A1 V1.0.0 Release (but 1.1.0 to come) 79/1079/6 1.0.0
authorTommy Carpenter <tc677g@att.com>
Mon, 7 Oct 2019 15:15:45 +0000 (11:15 -0400)
committerTommy Carpenter <tc677g@att.com>
Mon, 7 Oct 2019 19:13:10 +0000 (15:13 -0400)
- Represents v1.0.0 of the A1 API for O-RAN-SC Release A

Change-Id: I6bd25244ad122fc290e2a2db226bcdf679ff6e46
Signed-off-by: Tommy Carpenter <tc677g@att.com>
a1/controller.py
a1/data.py
a1/exceptions.py
container-tag.yaml
docs/developer-guide.rst
docs/release-notes.rst
integration_tests/a1mediator/Chart.yaml
integration_tests/test_a1.tavern.yaml
setup.py
tests/test_controller.py

index 7db18a0..b9db142 100644 (file)
@@ -35,7 +35,7 @@ def _try_func_return(func):
     """
     try:
         return func()
-    except (ValidationError, exceptions.PolicyTypeAlreadyExists) as exc:
+    except (ValidationError, exceptions.PolicyTypeAlreadyExists, exceptions.CantDeleteNonEmptyType) as exc:
         logger.exception(exc)
         return "", 400
     except (exceptions.PolicyTypeNotFound, exceptions.PolicyInstanceNotFound) as exc:
@@ -104,8 +104,12 @@ def delete_policy_type(policy_type_id):
     """
     Handles DELETE /a1-p/policytypes/policy_type_id
     """
-    logger.error(policy_type_id)
-    return "", 501
+
+    def delete_policy_type_handler():
+        data.delete_policy_type(policy_type_id)
+        return "", 204
+
+    return _try_func_return(delete_policy_type_handler)
 
 
 # Policy instances
@@ -115,31 +119,14 @@ def get_all_instances_for_type(policy_type_id):
     """
     Handles GET /a1-p/policytypes/policy_type_id/policies
     """
-
-    def get_all_instance_handler():
-        # try to clean up instances for this type
-        for policy_instance_id in data.get_instance_list(policy_type_id):
-            data.delete_policy_instance_if_applicable(policy_type_id, policy_instance_id)
-
-        # re-fetch this list as it may have changed
-        return data.get_instance_list(policy_type_id), 200
-
-    return _try_func_return(get_all_instance_handler)
+    return _try_func_return(lambda: data.get_instance_list(policy_type_id))
 
 
 def get_policy_instance(policy_type_id, policy_instance_id):
     """
     Handles GET /a1-p/policytypes/polidyid/policies/policy_instance_id
     """
-
-    def get_instance_handler():
-        # delete if applicable (will raise if not applicable to begin with)
-        data.delete_policy_instance_if_applicable(policy_type_id, policy_instance_id)
-
-        # raise 404 now that we may have deleted, or get the instance otherwise
-        return data.get_policy_instance(policy_type_id, policy_instance_id), 200
-
-    return _try_func_return(get_instance_handler)
+    return _try_func_return(lambda: data.get_policy_instance(policy_type_id, policy_instance_id))
 
 
 def get_policy_instance_status(policy_type_id, policy_instance_id):
@@ -153,9 +140,6 @@ def get_policy_instance_status(policy_type_id, policy_instance_id):
     """
 
     def get_status_handler():
-        # delete if applicable (will raise if not applicable to begin with)
-        data.delete_policy_instance_if_applicable(policy_type_id, policy_instance_id)
-
         vector = data.get_policy_instance_statuses(policy_type_id, policy_instance_id)
         for i in vector:
             if i == "OK":
@@ -202,6 +186,8 @@ def delete_policy_instance(policy_type_id, policy_instance_id):
         """
         here we send out the DELETEs but we don't delete the instance until a GET is called where we check the statuses
         """
+        data.instance_is_valid(policy_type_id, policy_instance_id)
+
         # send rmr (best effort)
         body = _gen_body_to_handler("DELETE", policy_type_id, policy_instance_id)
         a1rmr.send(json.dumps(body), message_type=policy_type_id)
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)
index be113ba..c76e9f2 100644 (file)
@@ -19,6 +19,10 @@ Custom Exceptions
 """
 
 
+class CantDeleteNonEmptyType(BaseException):
+    """tried to delete a type that isn't empty"""
+
+
 class PolicyInstanceNotFound(BaseException):
     """a policy instance cannot be found"""
 
index 3bf180b..163ffaa 100644 (file)
@@ -1,4 +1,4 @@
 # The Jenkins job uses this string for the tag in the image name
 # for example nexus3.o-ran-sc.org:10004/my-image-name:my-tag
 ---
-tag: 0.14.1-NOT_FOR_USE_YET
+tag: 1.0.0
index 9ce6e51..84662a5 100644 (file)
@@ -39,7 +39,7 @@ This project follows semver. When changes are made, the versions are in:
 
 4) ``integration_tests/a1mediator/Chart.yaml``
 
-6) ``a1/openapi.yml`` (this is an API version, not a software version)
+6) ``a1/openapi.yaml`` (this is an API version, not a software version; no need to bump on patch changes)
 
 7) in the it/dep repo that contains a1 helm chart, ``values.yaml``, ``Chart.yml``
 
index 19f660e..7a537d9 100644 (file)
@@ -23,11 +23,22 @@ All notable changes to this project will be documented in this file.
 The format is based on `Keep a Changelog <http://keepachangelog.com/>`__
 and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
-[1.0.0] - TBD
+[1.1.0] - TBD
 
 ::
 
-    * Release 1.0.0 will be the Release A version of A1
+    * Represents a resillent version of 1.0.0 that uses Redis for persistence
+
+
+[1.0.0] - 10/7/2019
+
+::
+
+    * Represents v1.0.0 of the A1 API for O-RAN-SC Release A
+    * Finished here:
+      - Implement type DELETE
+      - Clean up where policy instance cleanups happen
+
 
 [0.14.1] - 10/2/2019
 ::
index 2247fdd..be6cc65 100644 (file)
@@ -1,4 +1,4 @@
 apiVersion: v1
 description: A1 Helm chart for Kubernetes
 name: a1mediator
-version: 0.14.1
+version: 1.0.0
index 47662ca..55a8937 100644 (file)
@@ -115,7 +115,26 @@ stages:
     response:
       status_code: 404
 
-  # PUT the instance and make sure subsequent GETs return properly
+  - name: bad body for admission control policy
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
+      method: PUT
+      json:
+        not: "expected"
+      headers:
+        content-type: application/json
+    response:
+      status_code: 400
+
+  - name: not a json
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
+      method: PUT
+      data: "asdf"
+    response:
+      status_code: 415
+
+  # put it properly
   - name: put the admission control policy instance
     request:
       url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
@@ -130,6 +149,14 @@ stages:
     response:
       status_code: 202
 
+  - name: cant delete type with instances
+    delay_before: 3  # wait for the type acks to come back first
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000
+      method: DELETE
+    response:
+      status_code: 400
+
   - name: test the admission control policy get
     request:
       url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
@@ -191,6 +218,36 @@ stages:
     response:
       status_code: 404
 
+  - name: delete ac type
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000
+      method: DELETE
+    response:
+      status_code: 204
+
+  - name: cant delete again
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000
+      method: DELETE
+    response:
+      status_code: 404
+
+  - name: cant get
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20000
+      method: DELETE
+    response:
+      status_code: 404
+
+  - name: empty type list
+    request:
+      url: http://localhost:10000/a1-p/policytypes
+      method: GET
+    response:
+      status_code: 200
+      body: []
+
+
 ---
 
 test_name: test the delay receiver
@@ -210,7 +267,7 @@ stages:
       method: GET
     response:
       status_code: 200
-      body: [20000]
+      body: []
 
   - name: instance list 404
     request:
@@ -266,7 +323,6 @@ stages:
     response:
       status_code: 200
       body:
-       - 20000
        - 20001
 
   - name: instance list 200 but empty
@@ -291,6 +347,15 @@ stages:
     response:
       status_code: 404
 
+  - name: bad body for delaytest
+    request:
+      url: http://localhost:10000/a1-p/policytypes/20001/policies/delaytest
+      method: PUT
+      json:
+        not: "welcome"
+    response:
+      status_code: 400
+
   - name: create delay policy instance
     request:
       url: http://localhost:10000/a1-p/policytypes/20001/policies/delaytest
@@ -311,9 +376,9 @@ stages:
       body:
         test: foo
 
-  - name: test the admission control policy status get
+  - name: test the delay status get
     max_retries: 3
-    delay_before: 5  # give it a few seconds for rmr ; delay reciever sleeps for 5 seconds by default
+    delay_before: 6  # give it a few seconds for rmr ; delay reciever sleeps for 5 seconds by default
     request:
       url: http://localhost:10000/a1-p/policytypes/20001/policies/delaytest/status
       method: GET
@@ -366,7 +431,6 @@ stages:
       status_code: 202
 
   - name: should be no status
-    delay_before: 5  # give it a few seconds for rmr ; delay reciever sleeps for 5 seconds by default
     request:
       url: http://localhost:10000/a1-p/policytypes/20002/policies/brokentest/status
       method: GET
@@ -374,6 +438,7 @@ stages:
       status_code: 200
       body: []
 
+  # this one cant currently be deleted, see the comment in a1/data.py
 
 ---
 
@@ -388,10 +453,9 @@ stages:
     response:
       status_code: 404
 
-
-  - name: bad instance get
+  - name: bad instance get bad type
     request:
-      url: http://localhost:10000/a1-p/policytypes/20000/policies/darkness
+      url: http://localhost:10000/a1-p/policytypes/20666/policies/nonono
       method: GET
     response:
       status_code: 404
@@ -425,32 +489,3 @@ stages:
       status_code: 400
 
 
-
-
-  - name: bad body for admission control policy
-    request:
-      url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
-      method: PUT
-      json:
-        not: "expected"
-      headers:
-        content-type: application/json
-    response:
-      status_code: 400
-
-  - name: not a json
-    request:
-      url: http://localhost:10000/a1-p/policytypes/20000/policies/admission_control_policy
-      method: PUT
-      data: "asdf"
-    response:
-      status_code: 415
-
-  - name: bad body for delaytest
-    request:
-      url: http://localhost:10000/a1-p/policytypes/20001/policies/delaytest
-      method: PUT
-      json:
-        not: "welcome"
-    response:
-      status_code: 400
index 0aa2b3f..7ce5d53 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ from setuptools import setup, find_packages
 
 setup(
     name="a1",
-    version="0.14.1",
+    version="1.0.0",
     packages=find_packages(exclude=["tests.*", "tests"]),
     author="Tommy Carpenter",
     description="RIC A1 Mediator for policy/intent changes",
index 2866dd8..33f1447 100644 (file)
@@ -101,7 +101,7 @@ def _test_put_patch(monkeypatch):
 # Actual Tests
 
 
-def test_xapp_put_good(client, monkeypatch, adm_type_good, adm_instance_good):
+def test_workflow(client, monkeypatch, adm_type_good, adm_instance_good):
     """ test policy put good"""
 
     # no type there yet
@@ -143,6 +143,8 @@ def test_xapp_put_good(client, monkeypatch, adm_type_good, adm_instance_good):
 
     # create a good instance
     _test_put_patch(monkeypatch)
+    # assert that rmr bad states don't cause problems
+    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
     res = client.put(ADM_CTRL_INSTANCE, json=adm_instance_good)
     assert res.status_code == 202
 
@@ -170,6 +172,10 @@ def test_xapp_put_good(client, monkeypatch, adm_type_good, adm_instance_good):
     monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue)
     get_instance_good("IN EFFECT")
 
+    # cant delete type until there are no instances
+    res = client.delete(ADM_CTRL_TYPE)
+    assert res.status_code == 400
+
     # delete it
     res = client.delete(ADM_CTRL_INSTANCE)
     assert res.status_code == 202
@@ -186,42 +192,31 @@ def test_xapp_put_good(client, monkeypatch, adm_type_good, adm_instance_good):
     assert res.status_code == 404
     res = client.get(ADM_CTRL_INSTANCE)  # cant get instance
     assert res.status_code == 404
+
     # list still 200 but no instance
     res = client.get(ADM_CTRL_POLICIES)
     assert res.status_code == 200
     assert res.json == []
 
+    # delete the type
+    res = client.delete(ADM_CTRL_TYPE)
+    assert res.status_code == 204
 
-def test_xapp_put_good_bad_rmr(client, monkeypatch, adm_instance_good):
-    """
-    assert that rmr bad states don't cause problems
-    """
-    _test_put_patch(monkeypatch)
-    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
-    res = client.put(ADM_CTRL_INSTANCE, json=adm_instance_good)
-    assert res.status_code == 202
-
-    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(5))
-    res = client.put(ADM_CTRL_INSTANCE, json=adm_instance_good)
-    assert res.status_code == 202
+    # cant touch this
+    res = client.get(ADM_CTRL_TYPE)
+    assert res.status_code == 404
+    res = client.delete(ADM_CTRL_TYPE)
+    assert res.status_code == 404
 
 
 def test_bad_instances(client, monkeypatch, adm_type_good):
     """
-    Test bad send failures
+    test various failure modes
     """
+    # put the type (needed for some of the tests below)
     rmr_mocks.patch_rmr(monkeypatch)
-
-    # TODO: reenable this after delete!
-    # put the type
-    # res = client.put(ADM_CTRL_TYPE, json=adm_type_good)
-    # assert res.status_code == 201
-
-    # illegal type range
-    res = client.put("/a1-p/policytypes/19999", json=adm_type_good)
-    assert res.status_code == 400
-    res = client.put("/a1-p/policytypes/21024", json=adm_type_good)
-    assert res.status_code == 400
+    res = client.put(ADM_CTRL_TYPE, json=adm_type_good)
+    assert res.status_code == 201
 
     # bad body
     res = client.put(ADM_CTRL_INSTANCE, json={"not": "expected"})
@@ -231,6 +226,29 @@ def test_bad_instances(client, monkeypatch, adm_type_good):
     res = client.put(ADM_CTRL_INSTANCE, data="notajson")
     assert res.status_code == 415
 
+    # delete a non existent instance
+    res = client.delete(ADM_CTRL_INSTANCE + "DARKNESS")
+    assert res.status_code == 404
+
+    # get a non existent instance
+    monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue)
+    res = client.get(ADM_CTRL_INSTANCE + "DARKNESS")
+    assert res.status_code == 404
+
+    # delete the type (as cleanup)
+    res = client.delete(ADM_CTRL_TYPE)
+    assert res.status_code == 204
+
+
+def test_illegal_types(client, monkeypatch, adm_type_good):
+    """
+    Test illegal types
+    """
+    res = client.put("/a1-p/policytypes/19999", json=adm_type_good)
+    assert res.status_code == 400
+    res = client.put("/a1-p/policytypes/21024", json=adm_type_good)
+    assert res.status_code == 400
+
 
 def test_healthcheck(client):
     """