CORD-762 add set_fk support to orm

Change-Id: If86d5c041f3ea293aa2b109d881454cae95dd29a
diff --git a/xos/xos_client/tests/orm_listall.py b/xos/xos_client/tests/orm_listall.py
new file mode 100644
index 0000000..d76be05
--- /dev/null
+++ b/xos/xos_client/tests/orm_listall.py
@@ -0,0 +1,27 @@
+import sys
+sys.path.append("..")
+
+from xosapi import xos_grpc_client
+
+def test_callback():
+    print "TEST: orm_listall_crud"
+
+    c = xos_grpc_client.coreclient
+
+    for model_name in c.xos_orm.all_model_names:
+        model_class = getattr(c.xos_orm, model_name)
+
+        try:
+            print "   list all %s ..." % model_name,
+
+            objs = model_class.objects.all()
+
+            print "[%d] okay" % len(objs)
+        except:
+            print "   fail!"
+            traceback.print_exc()
+
+    print "    done"
+
+xos_grpc_client.start_api_parseargs(test_callback)
+
diff --git a/xos/xos_client/tests/orm_user_crud.py b/xos/xos_client/tests/orm_user_crud.py
new file mode 100644
index 0000000..45bda8f
--- /dev/null
+++ b/xos/xos_client/tests/orm_user_crud.py
@@ -0,0 +1,63 @@
+import sys
+sys.path.append("..")
+
+from xosapi import xos_grpc_client
+
+def test_callback():
+    print "TEST: orm_user_crud"
+
+    c = xos_grpc_client.coreclient
+
+    # create a new user and save it
+    u=c.xos_orm.User.objects.new()
+    assert(u.id==0)
+    import random, string
+    u.email=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
+    u.site=c.xos_orm.Site.objects.all()[0]
+    u.save()
+
+    # when we created the user, he should be assigned an id
+    orig_id = u.id
+    assert(orig_id!=0)
+
+    # invalidate u.site so it's reloaded from the server
+    u.invalidate_cache("site")
+
+    # site object should be populated
+    assert(u.site is not None)
+
+    # site object should have a backpointer to user
+    u_all = u.site.users.all()
+    u_all = [x for x in u_all if x.email == u.email]
+    assert(len(u_all)==1)
+
+    # update the user
+    u.password="foobar"
+    u.save()
+
+    # update should not have changed it
+    assert(u.id==orig_id)
+
+    # check a listall and make sure the user is listed
+    u_all = c.xos_orm.User.objects.all()
+    u_all = [x for x in u_all if x.email == u.email]
+    assert(len(u_all)==1)
+    u2 = u_all[0]
+    assert(u2.id == u.id)
+
+    # get and make sure the password was updated
+    u3 = c.xos_orm.User.objects.get(id=orig_id)
+    assert(u3.password=="foobar")
+
+    # delete the user
+    u3.delete()
+
+    # make sure it is deleted
+    u_all = c.xos_orm.User.objects.all()
+    u_all = [x for x in u_all if x.email == u.email]
+    assert(len(u_all)==0)
+
+    print "    okay"
+
+xos_grpc_client.start_api_parseargs(test_callback)
+
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index f0ef355..690fcf9 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -33,6 +33,7 @@
         super(ORMWrapper, self).__setattr__("stub", stub)
         super(ORMWrapper, self).__setattr__("cache", {})
         super(ORMWrapper, self).__setattr__("reverse_cache", {})
+        super(ORMWrapper, self).__setattr__("poisoned", {})
         super(ORMWrapper, self).__setattr__("is_new", is_new)
         fkmap=self.gen_fkmap()
         super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
@@ -82,10 +83,29 @@
 
         return self.cache[name]
 
+    def fk_set(self, name, model):
+        fk_entry = self._fkmap[name]
+        id = model.id
+        setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
+
+        # XXX setting the cache here is a problematic, since the cached object's
+        # reverse foreign key pointers will not include the reference back
+        # to this object. Instead of setting the cache, let's poison the name
+        # and throw an exception if someone tries to get it.
+
+        # To work around this, explicitly call reset_cache(fieldName) and
+        # the ORM will reload the object.
+
+        self.poisoned[name] = True
+
     def __getattr__(self, name, *args, **kwargs):
         # note: getattr is only called for attributes that do not exist in
         #       self.__dict__
 
+        if name in self.poisoned.keys():
+            # see explanation in fk_set()
+            raise Exception("foreign key was poisoned")
+
         if name in self._fkmap.keys():
             return self.fk_resolve(name)
 
@@ -95,7 +115,9 @@
         return getattr(self._wrapped_class, name, *args, **kwargs)
 
     def __setattr__(self, name, value):
-        if name in self.__dict__:
+        if name in self._fkmap.keys():
+            self.fk_set(name, value)
+        elif name in self.__dict__:
             super(ORMWrapper,self).__setattr__(name, value)
         else:
             setattr(self._wrapped_class, name, value)
@@ -103,6 +125,19 @@
     def __repr__(self):
         return self._wrapped_class.__repr__()
 
+    def invalidate_cache(self, name=None):
+        if name:
+            if name in self.cache:
+                del self.cache[name]
+            if name in self.reverse_cache:
+                del self.reverse_cache[name]
+            if name in self.poisoned:
+                del self.poisoned[name]
+        else:
+            self.cache.clear()
+            self.reverse_cache.clear()
+            self.poisoned.clear()
+
     def save(self):
         if self.is_new:
            new_class = self.stub.invoke("Create%s" % self._wrapped_class.__class__.__name__, self._wrapped_class)
diff --git a/xos/xos_client/xosapi/xos_grpc_client.py b/xos/xos_client/xosapi/xos_grpc_client.py
index 2b32914..f06aad5 100644
--- a/xos/xos_client/xosapi/xos_grpc_client.py
+++ b/xos/xos_client/xosapi/xos_grpc_client.py
@@ -1,3 +1,4 @@
+import argparse
 import base64
 import functools
 import grpc
@@ -94,6 +95,116 @@
         self.reconnect_callback2 = reconnect_callback
 
 # -----------------------------------------------------------------------------
+# Wrappers for easy setup for test cases, etc
+# -----------------------------------------------------------------------------
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+
+    defs = {"grpc_insecure_endpoint": "xos-core.cord.lab:50055",
+            "grpc_secure_endpoint": "xos-core.cord.lab:50051",
+            "consul": None}
+
+    _help = '<hostname>:<port> to consul agent (default: %s)' % defs['consul']
+    parser.add_argument(
+        '-C', '--consul', dest='consul', action='store',
+        default=defs['consul'],
+        help=_help)
+
+    _help = ('gRPC insecure end-point to connect to. It can either be a direct'
+             'definition in the form of <hostname>:<port>, or it can be an'
+             'indirect definition in the form of @<service-name> where'
+             '<service-name> is the name of the grpc service as registered'
+             'in consul (example: @voltha-grpc). (default: %s'
+             % defs['grpc_insecure_endpoint'])
+    parser.add_argument('-G', '--grpc-insecure-endpoint',
+                        dest='grpc_insecure_endpoint',
+                        action='store',
+                        default=defs["grpc_insecure_endpoint"],
+                        help=_help)
+
+    _help = ('gRPC secure end-point to connect to. It can either be a direct'
+             'definition in the form of <hostname>:<port>, or it can be an'
+             'indirect definition in the form of @<service-name> where'
+             '<service-name> is the name of the grpc service as registered'
+             'in consul (example: @voltha-grpc). (default: %s'
+             % defs["grpc_secure_endpoint"])
+    parser.add_argument('-S', '--grpc-secure-endpoint',
+                        dest='grpc_secure_endpoint',
+                        action='store',
+                        default=defs["grpc_secure_endpoint"],
+                        help=_help)
+
+    parser.add_argument('-u', '--username',
+                        dest='username',
+                        action='store',
+                        default=None,
+                        help=_help)
+
+    parser.add_argument('-p', '--password',
+                        dest='password',
+                        action='store',
+                        default=None,
+                        help=_help)
+
+    _help = 'omit startup banner log lines'
+    parser.add_argument('-n', '--no-banner',
+                        dest='no_banner',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    _help = "suppress debug and info logs"
+    parser.add_argument('-q', '--quiet',
+                        dest='quiet',
+                        action='count',
+                        help=_help)
+
+    _help = 'enable verbose logging'
+    parser.add_argument('-v', '--verbose',
+                        dest='verbose',
+                        action='count',
+                        help=_help)
+
+    args = parser.parse_args()
+
+    return args
+
+def coreclient_reconnect(client, reconnect_callback, *args, **kwargs):
+    global coreapi
+
+    coreapi = coreclient.xos_orm
+
+    if reconnect_callback:
+        reconnect_callback(*args, **kwargs)
+
+    reactor.stop()
+
+def start_api(reconnect_callback, *args, **kwargs):
+    global coreclient
+
+    if kwargs.get("username", None):
+        coreclient = SecureClient(*args, **kwargs)
+    else:
+        coreclient = InsecureClient(*args, **kwargs)
+
+    coreclient.set_reconnect_callback(functools.partial(coreclient_reconnect, coreclient, reconnect_callback))
+    coreclient.start()
+
+    reactor.run()
+
+def start_api_parseargs(reconnect_callback):
+    args = parse_args()
+
+    if args.username:
+        start_api(reconnect_callback, endpoint=args.grpc_secure_endpoint, username=args.username, password=args.password)
+    else:
+        start_api(reconnect_callback, endpoint=args.grpc_insecure_endpoint)
+
+
+
+
+# -----------------------------------------------------------------------------
 # Self test
 # -----------------------------------------------------------------------------
 
diff --git a/xos/xos_client/xossh b/xos/xos_client/xossh
old mode 100644
new mode 100755