SEBA-382 deleted_objects support in orm

Change-Id: Id012c627d59f18e95bd9acb5b7c7d55e74686694
diff --git a/VERSION b/VERSION
index 5473cf8..0789fdb 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.1.26-dev
+2.1.26
diff --git a/containers/chameleon/Dockerfile.chameleon b/containers/chameleon/Dockerfile.chameleon
index 8f27a20..4d8b8c2 100644
--- a/containers/chameleon/Dockerfile.chameleon
+++ b/containers/chameleon/Dockerfile.chameleon
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/chameleon
-FROM xosproject/xos-base:2.1.25
+FROM xosproject/xos-base:2.1.26
 
 # xos-base already has protoc and dependencies installed
 
diff --git a/containers/xos/Dockerfile.client b/containers/xos/Dockerfile.client
index 9b446f3..55bbbbb 100644
--- a/containers/xos/Dockerfile.client
+++ b/containers/xos/Dockerfile.client
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-client
-FROM xosproject/xos-libraries:2.1.25
+FROM xosproject/xos-libraries:2.1.26
 
 # Install XOS client
 COPY xos/xos_client /tmp/xos_client
diff --git a/containers/xos/Dockerfile.libraries b/containers/xos/Dockerfile.libraries
index 766cca7..e04dc3d 100644
--- a/containers/xos/Dockerfile.libraries
+++ b/containers/xos/Dockerfile.libraries
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-libraries
-FROM xosproject/xos-base:2.1.25
+FROM xosproject/xos-base:2.1.26
 
 # Add libraries
 COPY lib /opt/xos/lib
diff --git a/containers/xos/Dockerfile.synchronizer-base b/containers/xos/Dockerfile.synchronizer-base
index e00018f..dd42166 100644
--- a/containers/xos/Dockerfile.synchronizer-base
+++ b/containers/xos/Dockerfile.synchronizer-base
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-synchronizer-base
-FROM xosproject/xos-client:2.1.25
+FROM xosproject/xos-client:2.1.26
 
 COPY xos/synchronizers/new_base /opt/xos/synchronizers/new_base
 COPY xos/xos/logger.py /opt/xos/xos/logger.py
diff --git a/containers/xos/Dockerfile.xos-core b/containers/xos/Dockerfile.xos-core
index bb1a7e7..2ccb16f 100644
--- a/containers/xos/Dockerfile.xos-core
+++ b/containers/xos/Dockerfile.xos-core
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-core
-FROM xosproject/xos-libraries:2.1.25
+FROM xosproject/xos-libraries:2.1.26
 
 # Install XOS
 ADD xos /opt/xos
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index fb47e5d..2f21dbc 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -589,28 +589,42 @@
             log.exception("Exception in apihelper.list")
             raise
 
+    def build_filter(self, request, query=None):
+        """ Given a filter request, turn it into a django query.
+
+            If argument query is not None, then the new query will be appended to the existing query.
+        """
+        for element in request.elements:
+            if query:
+                query = query & self.query_element_to_q(element)
+            else:
+                query = self.query_element_to_q(element)
+        return query
+
     def filter(self, djangoClass, user, request):
         try:
-            query = None
             if request.kind == request.DEFAULT:
-                for element in request.elements:
-                    if query:
-                        query = query & self.query_element_to_q(element)
-                    else:
-                        query = self.query_element_to_q(element)
+                query = self.build_filter(request, None)
                 queryset = djangoClass.objects.filter(query)
             elif request.kind == request.SYNCHRONIZER_DIRTY_OBJECTS:
                 query = (Q(enacted=None) | Q(enacted__lt=F('updated')) | Q(enacted__lt=F('changed_by_policy'))) \
                         & Q(lazy_blocked=False) & Q(no_sync=False)
+                query = self.build_filter(request, query)
                 queryset = djangoClass.objects.filter(query)
             elif request.kind == request.SYNCHRONIZER_DELETED_OBJECTS:
-                queryset = djangoClass.deleted_objects.all()
+                query = self.build_filter(request, None)
+                if query:
+                    queryset = djangoClass.deleted_objects.filter(query)
+                else:
+                    queryset = djangoClass.deleted_objects.all()
             elif request.kind == request.SYNCHRONIZER_DIRTY_POLICIES:
                 query = (Q(policed=None) | Q(policed__lt=F('updated')) | Q(policed__lt=F('changed_by_step'))) \
                         & Q(no_policy=False)
+                query = self.build_filter(request, query)
                 queryset = djangoClass.objects.filter(query)
             elif request.kind == request.SYNCHRONIZER_DELETED_POLICIES:
                 query = Q(policed__lt=F('updated')) | Q(policed=None)
+                query = self.build_filter(request, query)
                 queryset = djangoClass.deleted_objects.filter(query)
             elif request.kind == request.ALL:
                 queryset = djangoClass.objects.all()
diff --git a/xos/xos_client/xosapi/fake_stub.py b/xos/xos_client/xosapi/fake_stub.py
index 8cc60f7..13d684d 100644
--- a/xos/xos_client/xosapi/fake_stub.py
+++ b/xos/xos_client/xosapi/fake_stub.py
@@ -353,7 +353,12 @@
         return len(self.items)
 
 class FakeQuery(object):
-    DEFAULT="default"
+    DEFAULT=0
+    ALL = 1
+    SYNCHRONIZER_DIRTY_OBJECTS = 2
+    SYNCHRONIZER_DELETED_OBJECTS = 3
+    SYNCHRONIZER_DIRTY_POLICIES = 4
+    SYNCHRONIZER_DELETED_POLICIES = 5
 
     def __init__(self):
         self.elements = FakeElements()
@@ -362,6 +367,7 @@
     def __init__(self):
         self.id_counter = 1
         self.objs = {}
+        self.deleted_objs = {}
         for name in ["Controller", "Deployment", "Slice", "Site", "Tag", "Service", "ServiceInstance", "ONOSService",
                      "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice",
                      "TestModel"]:
@@ -390,17 +396,25 @@
 
     def filter(self, classname, query, metadata=None):
         items = []
-        for (k,v) in self.objs.items():
+
+        if query.kind == FakeQuery.SYNCHRONIZER_DELETED_OBJECTS:
+            objs = self.deleted_objs.items()
+        else:
+            objs = self.objs.items()
+
+        for (k,v) in objs:
             (this_classname, id) = k.split(":")
             if this_classname != classname:
                 continue
+            match = True
             for q in query.elements.items:
                 iValue = getattr(q, "iValue", None)
                 if (iValue is not None) and getattr(v,q.name)!=iValue:
-                    continue
+                    match = False
                 sValue = getattr(q, "sValue", None)
                 if (sValue is not None) and getattr(v, q.name) != sValue:
-                    continue
+                    match = False
+            if match:
                 items.append(v)
         return FakeItemList(items)
 
@@ -426,7 +440,9 @@
 
     def delete(self, classname, id, metadata=None):
         k = self.make_key(classname, id)
+        obj = self.objs[k]
         del self.objs[k]
+        self.deleted_objs[k] = obj
 
 class FakeCommonProtos(object):
     def __init__(self):
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index 0ea714b..c4097f1 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -486,15 +486,18 @@
     """ Manages a remote list of objects """
 
     # constants better agree with common.proto
+    DEFAULT = 0
+    ALL = 1
     SYNCHRONIZER_DIRTY_OBJECTS = 2
     SYNCHRONIZER_DELETED_OBJECTS = 3
     SYNCHRONIZER_DIRTY_POLICIES = 4
     SYNCHRONIZER_DELETED_POLICIES = 5
 
-    def __init__(self, stub, modelName, packageName):
+    def __init__(self, stub, modelName, packageName, kind=0):
         self._stub = stub
         self._modelName = modelName
         self._packageName = packageName
+        self._kind = kind
 
     def wrap_single(self, obj):
         return make_ORMWrapper(obj, self._stub)
@@ -506,17 +509,20 @@
         return ORMQuerySet(result)
 
     def all(self):
-        return self.wrap_list(self._stub.invoke("List%s" % self._modelName, self._stub.make_empty()))
+        if (self._kind == self.DEFAULT):
+            return self.wrap_list(self._stub.invoke("List%s" % self._modelName, self._stub.make_empty()))
+        else:
+            return self.filter()
 
     def first(self):
-        objs=self.wrap_list(self._stub.invoke("List%s" % self._modelName, self._stub.make_empty()))
+        objs = self.all()
         if not objs:
             return None
         return objs[0]
 
     def filter(self, **kwargs):
         q = self._stub.make_Query()
-        q.kind = q.DEFAULT
+        q.kind = self._kind
 
         for (name, val) in kwargs.items():
             el = q.elements.add()
@@ -562,6 +568,9 @@
             return objs[0]
 
     def new(self, **kwargs):
+        if (self._kind != ORMObjectManager.DEFAULT):
+            raise Exception("Creating objects is only supported by the DEFAULT object manager")
+
         cls = self._stub.all_grpc_classes[self._modelName]
         o = make_ORMWrapper(cls(), self._stub, is_new=True)
         for (k,v) in  kwargs.items():
@@ -574,6 +583,7 @@
         self.model_name = model_name
         self._stub = stub
         self.objects = ORMObjectManager(stub, model_name, package_name)
+        self.deleted_objects = ORMObjectManager(stub, model_name, package_name, ORMObjectManager.SYNCHRONIZER_DELETED_OBJECTS)
 
     @property
     def __name__(self):
diff --git a/xos/xos_client/xosapi/test_orm.py b/xos/xos_client/xosapi/test_orm.py
index 4c3cd32..00dfeba 100644
--- a/xos/xos_client/xosapi/test_orm.py
+++ b/xos/xos_client/xosapi/test_orm.py
@@ -537,6 +537,43 @@
 
         self.assertEqual(site.diff, {})
 
+    def test_deleted_objects_all(self):
+        orm = self.make_coreapi()
+        orig_len_sites = len(orm.Site.objects.all())
+        orig_len_deleted_sites = len(orm.Site.deleted_objects.all())
+        site = orm.Site(name="mysite")
+        site.save()
+        site.delete()
+        sites = orm.Site.objects.all()
+        self.assertEqual(len(sites), orig_len_sites)
+        deleted_sites = orm.Site.deleted_objects.all()
+        self.assertEqual(len(deleted_sites), orig_len_deleted_sites+1)
+
+    def test_deleted_objects_filter(self):
+        orm = self.make_coreapi()
+        with patch.object(orm.grpc_stub, "FilterTestModel", wraps=orm.grpc_stub.FilterTestModel) as filter:
+            foo = orm.TestModel(name="foo")
+            foo.save()
+            foo.delete()
+
+            # There should be no live objects
+            objs = orm.TestModel.objects.filter(name = "foo")
+            self.assertEqual(len(objs), 0)
+
+            # There should be one deleted object
+            deleted_objs = orm.TestModel.deleted_objects.filter(name = "foo")
+            self.assertEqual(len(deleted_objs), 1)
+
+            # Two calls, one for when we checked live objects, the other for when we checked deleted objects
+            self.assertEqual(filter.call_count, 2)
+            q = filter.call_args[0][0]
+
+            # Now spy on the query that was generated, to make sure it looks like we expect
+            self.assertEqual(q.kind, q.SYNCHRONIZER_DELETED_OBJECTS)
+            self.assertEqual(len(q.elements), 1)
+            self.assertEqual(q.elements[0].operator, q.elements[0].EQUAL)
+            self.assertEqual(q.elements[0].sValue, "foo")
+
 
 def main():
     global USE_FAKE_STUB