SEBA-462 Service unload can be initiated by synchronizer

Change-Id: Idb8b924a6b048b16cdb6b04e91058026932d827d
diff --git a/lib/xos-synchronizer/xossynchronizer/modelaccessor.py b/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
index 1418cc6..c2d5114 100644
--- a/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
+++ b/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
@@ -28,7 +28,7 @@
 import os
 import signal
 import sys
-import time
+from threading import Timer
 from loadmodels import ModelLoadClient
 
 from xosconfig import Config
@@ -37,6 +37,7 @@
 
 log = create_logger(Config().get("logging"))
 
+after_reactor_exit_code = None
 orig_sigint = None
 model_accessor = None
 
@@ -161,6 +162,43 @@
 
     reactor.callLater(1, functools.partial(keep_trying, client, reactor))
 
+def unload_models(client, reactor, version):
+    # This function is called by a timer until it succeeds.
+    log.info("unload_models initiated by timer")
+
+    try:
+        result = ModelLoadClient(client).unload_models(
+            Config.get("name"),
+            version=version,
+            cleanup_behavior=ModelLoadClient.AUTOMATICALLY_CLEAN)
+
+        log.debug("Unload response", result=result)
+
+        if result.status in [result.SUCCESS, result.SUCCESS_NOTHING_CHANGED]:
+            log.info("Models successfully unloaded. Exiting with status", code=0)
+            sys.exit(0)
+
+        if result.status == result.TRYAGAIN:
+            log.info("TRYAGAIN received. Expect to try again in 30 seconds.")
+
+    except Exception as e:
+        # If the synchronizer is operational, then assume the ORM's restart_on_disconnect will deal with the
+        # connection being lost.
+        log.exception("Error while unloading. Expect to try again in 30 seconds.")
+
+    Timer(30, functools.partial(unload_models, client, reactor, version)).start()
+
+def exit_while_inside_reactor(reactor, code):
+    """ Calling sys.exit() while inside reactor ends up trapped by reactor.
+
+        So what we'll do is set a flag indicating we want to exit, then stop reactor, then return
+    """
+    global after_reactor_exit_code
+
+    reactor.stop()
+    signal.signal(signal.SIGINT, orig_sigint)
+    after_reactor_exit_code = code
+
 
 def grpcapi_reconnect(client, reactor):
     global model_accessor
@@ -172,9 +210,29 @@
         version = autodiscover_version_of_main(max_parent_depth=0) or "unknown"
         log.info("Service version is %s" % version)
         try:
-            ModelLoadClient(client).upload_models(
-                Config.get("name"), Config.get("models_dir"), version=version
-            )
+            if Config.get("desired_state") == "load":
+                ModelLoadClient(client).upload_models(
+                    Config.get("name"), Config.get("models_dir"), version=version
+                )
+            elif Config.get("desired_state") == "unload":
+                # Try for an easy unload. If there's no dirty models, then unload will succeed without
+                # requiring us to setup the synchronizer.
+                log.info("Trying for an easy unload_models")
+                result = ModelLoadClient(client).unload_models(
+                    Config.get("name"),
+                    version=version,
+                    cleanup_behavior=1)  # FIXME: hardcoded value for automatic delete
+                if result.status in [result.SUCCESS, result.SUCCESS_NOTHING_CHANGED]:
+                    log.info("Models successfully unloaded. Synchronizer exiting")
+                    exit_while_inside_reactor(reactor, 0)
+                    return
+
+                # We couldn't unload the easy way, so we'll have to do it the hard way. Fall through and
+                # setup the synchronizer.
+            else:
+                log.error("Misconfigured", desired_state=Config.get("desired_state"))
+                exit_while_inside_reactor(reactor, -1)
+                return
         except Exception as e:  # TODO: narrow exception scope
             if (
                 hasattr(e, "code")
@@ -242,6 +300,10 @@
     # Restore the sigint handler
     signal.signal(signal.SIGINT, orig_sigint)
 
+    # Check to see if we still want to unload
+    if Config.get("desired_state") == "unload":
+        Timer(30, functools.partial(unload_models, client, reactor, version)).start()
+
 
 def config_accessor_grpcapi():
     global orig_sigint
@@ -282,6 +344,11 @@
 
     reactor.run()
 
+    # Catch if we wanted to stop while inside of a reactor callback
+    if after_reactor_exit_code is not None:
+        log.info("exiting with status", code=after_reactor_exit_code)
+        sys.exit(after_reactor_exit_code)
+
 
 def config_accessor_mock():
     global model_accessor