[SEBA-494] Validating core version matched version requested by synchronizer

Change-Id: Id7018394c70559eae0e424e1e231d8e70b0496de
diff --git a/VERSION b/VERSION
index fdacbdb..ccbccc3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.1.57
+2.2.0
diff --git a/containers/chameleon/Dockerfile.chameleon b/containers/chameleon/Dockerfile.chameleon
index 9079966..6e326dc 100644
--- a/containers/chameleon/Dockerfile.chameleon
+++ b/containers/chameleon/Dockerfile.chameleon
@@ -13,7 +13,8 @@
 # limitations under the License.
 
 # xosproject/chameleon
-FROM xosproject/xos-base:2.1.57
+
+FROM xosproject/xos-base:2.2.0
 
 # xos-base already has protoc and dependencies installed
 
diff --git a/containers/xos/Dockerfile.client b/containers/xos/Dockerfile.client
index e69843c..083d01a 100644
--- a/containers/xos/Dockerfile.client
+++ b/containers/xos/Dockerfile.client
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 # xosproject/xos-client
-FROM xosproject/xos-libraries:2.1.57
 
+FROM xosproject/xos-libraries:2.2.0
 
 # Install XOS client
 COPY lib/xos-api /tmp/xos-api
diff --git a/containers/xos/Dockerfile.libraries b/containers/xos/Dockerfile.libraries
index 5432413..4171077 100644
--- a/containers/xos/Dockerfile.libraries
+++ b/containers/xos/Dockerfile.libraries
@@ -13,7 +13,8 @@
 # limitations under the License.
 
 # xosproject/xos-libraries
-FROM xosproject/xos-base:2.1.57
+
+FROM xosproject/xos-base:2.2.0
 
 # Add libraries
 COPY lib /opt/xos/lib
diff --git a/containers/xos/Dockerfile.synchronizer-base b/containers/xos/Dockerfile.synchronizer-base
index e5c6aca..0110aca 100644
--- a/containers/xos/Dockerfile.synchronizer-base
+++ b/containers/xos/Dockerfile.synchronizer-base
@@ -13,7 +13,8 @@
 # limitations under the License.
 
 # xosproject/xos-synchronizer-base
-FROM xosproject/xos-client:2.1.57
+
+FROM xosproject/xos-client:2.2.0
 
 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 c3ef7c7..6d024b9 100644
--- a/containers/xos/Dockerfile.xos-core
+++ b/containers/xos/Dockerfile.xos-core
@@ -13,7 +13,8 @@
 # limitations under the License.
 
 # xosproject/xos-core
-FROM xosproject/xos-libraries:2.1.57
+
+FROM xosproject/xos-libraries:2.2.0
 
 # Install XOS
 ADD xos /opt/xos
diff --git a/containers/xos/Makefile b/containers/xos/Makefile
index 9b893b7..b17f92e 100644
--- a/containers/xos/Makefile
+++ b/containers/xos/Makefile
@@ -39,15 +39,15 @@
 	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-base:${TAG} -f Dockerfile.base .
 
 xos-libraries:
-	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-libraries:${TAG} -f Dockerfile.libraries ../..
+	docker build --no-cache $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-libraries:${TAG} -f Dockerfile.libraries ../..
 
 xos-client:
 	rm -rf tmp.chameleon
 	cp -R ../../../../component/chameleon tmp.chameleon
-	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-client:${TAG} -f Dockerfile.client ../..
+	docker build --no-cache $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-client:${TAG} -f Dockerfile.client ../..
 
 xos-core:
-	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-core:${TAG} -f Dockerfile.xos-core ../..
+	docker build --no-cache $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-core:${TAG} -f Dockerfile.xos-core ../..
 
 xos-synchronizer-base:
-	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-synchronizer-base:${TAG} -f Dockerfile.synchronizer-base ../..
+	docker build --no-cache $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}xos-synchronizer-base:${TAG} -f Dockerfile.synchronizer-base ../..
diff --git a/containers/xos/pip_requested.txt b/containers/xos/pip_requested.txt
index 4e17746..c3a3098 100644
--- a/containers/xos/pip_requested.txt
+++ b/containers/xos/pip_requested.txt
@@ -49,3 +49,4 @@
 six==1.11.0
 structlog==18.1.0
 tosca-parser==0.9.0
+semver==2.8.1
diff --git a/containers/xos/pip_requirements.txt b/containers/xos/pip_requirements.txt
index af3f2ae..f8e4ec4 100644
--- a/containers/xos/pip_requirements.txt
+++ b/containers/xos/pip_requirements.txt
@@ -140,3 +140,4 @@
 Werkzeug==0.14.1
 wrapt==1.10.11
 zope.interface==4.5.0
+semver==2.8.1
diff --git a/lib/xos-config/xosconfig/default.py b/lib/xos-config/xosconfig/default.py
index 2e37229..4f6ec23 100644
--- a/lib/xos-config/xosconfig/default.py
+++ b/lib/xos-config/xosconfig/default.py
@@ -14,7 +14,10 @@
 
 DEFAULT_VALUES = {
     "xos_dir": "/opt/xos",
-    "desired_state": "load", # synchronizers - default to "load"
+    "desired_state": "load",  # synchronizers - default to "load"
+    # by default version in not set,
+    # we can't make it mandatory as we're reading multiple files in the synchronizers
+    "core_version": None,
     # The configuration below inherits from the standard config of the Python logging module
     # See: https://docs.python.org/2/library/logging.config.html
     # multistructlog supports this config in all of its generality
diff --git a/lib/xos-config/xosconfig/synchronizer-config-schema.yaml b/lib/xos-config/xosconfig/synchronizer-config-schema.yaml
index 357e8d9..cba845f 100644
--- a/lib/xos-config/xosconfig/synchronizer-config-schema.yaml
+++ b/lib/xos-config/xosconfig/synchronizer-config-schema.yaml
@@ -15,6 +15,8 @@
 map:
   name:
     type: str
+  core_version:
+    type: str
   desired_state:
     type: str
   xos_dir:
diff --git a/lib/xos-synchronizer/xossynchronizer/loadmodels.py b/lib/xos-synchronizer/xossynchronizer/loadmodels.py
index 7acce41..1c1f8bd 100644
--- a/lib/xos-synchronizer/xossynchronizer/loadmodels.py
+++ b/lib/xos-synchronizer/xossynchronizer/loadmodels.py
@@ -70,6 +70,13 @@
                     item.filename = fn
                     item.contents = open(os.path.join(migrations_dir, fn)).read()
 
+        # loading core requested version from synchronizer config
+        core_version = Config.get("core_version")
+        if core_version is None:
+            log.warn("Core version is not set in the config file")
+
+        request.core_version = core_version
+
         result = self.api.dynamicload.LoadModels(request)
 
         return result
diff --git a/lib/xos-synchronizer/xossynchronizer/modelaccessor.py b/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
index c2d5114..72fc3c5 100644
--- a/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
+++ b/lib/xos-synchronizer/xossynchronizer/modelaccessor.py
@@ -41,7 +41,6 @@
 orig_sigint = None
 model_accessor = None
 
-
 class ModelAccessor(object):
     def __init__(self):
         self.all_model_classes = self.get_all_model_classes()
@@ -208,7 +207,7 @@
 
     if Config.get("models_dir"):
         version = autodiscover_version_of_main(max_parent_depth=0) or "unknown"
-        log.info("Service version is %s" % version)
+        log.info("Service version is %s" % version, core_version=Config.get("core_version"))
         try:
             if Config.get("desired_state") == "load":
                 ModelLoadClient(client).upload_models(
@@ -246,6 +245,23 @@
                 client.connected = False
                 client.connect()
                 return
+
+            elif (
+                hasattr(e, "code")
+                and callable(e.code)
+                and hasattr(e.code(), "name")
+                and (e.code().name) == "INVALID_ARGUMENT"
+            ):
+                # in this case there is a version mismatch between the service and the core,
+                # shut down the process so it's clear something is wrong
+                log.error(e.details())
+
+                # kill the process so the operator is aware something is wrong
+                log.info("shutting down")
+                exit_while_inside_reactor(reactor, 1)
+                return
+
+
             log.exception("failed to onboard models")
             # If it's some other error, then we don't need to force a reconnect. Just try the LoadModels() again.
             reactor.callLater(10, functools.partial(grpcapi_reconnect, client, reactor))
diff --git a/xos/coreapi/protos/dynamicload.proto b/xos/coreapi/protos/dynamicload.proto
index 192ee42..f65021e 100644
--- a/xos/coreapi/protos/dynamicload.proto
+++ b/xos/coreapi/protos/dynamicload.proto
@@ -32,12 +32,13 @@
 
 message LoadModelsRequest {
     string name = 1;
-    string version = 2;
+    string version = 2; // this is the service version
     repeated Xproto xprotos = 3;
     repeated DeclFile decls = 4;
     repeated AtticFile attics = 5;
     repeated APIConvenienceFile convenience_methods = 6;
     repeated MigrationFile migrations = 7;
+    string core_version = 8; // this is the core version
 };
 
 message ListConvenienceMethodsReply {
diff --git a/xos/coreapi/xos_dynamicload_api.py b/xos/coreapi/xos_dynamicload_api.py
index b7fce6e..8a44581 100644
--- a/xos/coreapi/xos_dynamicload_api.py
+++ b/xos/coreapi/xos_dynamicload_api.py
@@ -19,7 +19,12 @@
 from dynamicbuild import DynamicBuilder
 from apistats import REQUEST_COUNT, track_request_time
 import grpc
+import semver
+import re
+from xosconfig import Config
+from multistructlog import create_logger
 
+log = create_logger(Config().get("logging"))
 
 class DynamicLoadService(dynamicload_pb2_grpc.dynamicloadServicer):
     def __init__(self, thread_pool, server):
@@ -54,9 +59,57 @@
                         django_models[k] = v
                 self.django_app_models[app.name] = django_models
 
+    def match_major_version(self, current, expected):
+        """
+        Returns true if the major version is the same
+        :param current: semver string for the current version
+        :param expected: semver string for the expected version
+        :return: bool
+        """
+        current_parts = semver.parse(current)
+        expected = re.sub("[><=!]", "", expected)
+        expected_parts = semver.parse(expected)
+        match = current_parts["major"] == expected_parts["major"]
+        log.debug("Verifying major version",
+                  expected_major=expected_parts["major"], current_major=current_parts["major"], match=match)
+        return match
+
     @track_request_time("DynamicLoad", "LoadModels")
     def LoadModels(self, request, context):
         try:
+
+            core_version = autodiscover_version_of_main()
+            requested_core_version = request.core_version
+            log.info("Loading service models",
+                     service=request.name,
+                     service_version=request.version,
+                     requested_core_version=requested_core_version
+                )
+
+            if not requested_core_version:
+                requested_core_version = "<3.0.0"
+
+            match_version = semver.match(core_version, requested_core_version)
+            match_major = self.match_major_version(core_version, requested_core_version)
+            if not match_version:
+                log.error("Not loading service because of mismatching versions", service=request.name,
+                          core_version=core_version, requested_core_version=requested_core_version)
+                context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
+                msg = "Service %s is requesting core version %s but actual version is %s" % (
+                request.name, requested_core_version, core_version)
+                context.set_details(msg)
+                raise Exception(msg)
+            if not match_major:
+                log.error("Not loading service because of mismatching major versions", service=request.name,
+                          core_version=core_version, requested_core_version=requested_core_version)
+                context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
+                msg = "Service %s is requesting core version %s but actual version is %s, major version is different" % (
+                    request.name, requested_core_version, core_version)
+                context.set_details(msg)
+                raise Exception(msg)
+                context.set_details(msg)
+                raise Exception(msg)
+
             builder = DynamicBuilder()
             result = builder.handle_loadmodels_request(request)