[SEBA-497]

Change to using alpine-grpc-base
Fix issue with nested exceptions causing an error in structlog
Reformat and python3 fixes, v3.5 mock support
Record execution times in the loader

Change-Id: I6d7923818d57012fca32ce44668820de422206d6
diff --git a/src/grpc_client/KEYS.reference.py b/src/grpc_client/KEYS.reference.py
index fd8c776..6453117 100644
--- a/src/grpc_client/KEYS.reference.py
+++ b/src/grpc_client/KEYS.reference.py
@@ -19,57 +19,57 @@
 #########################################################################
 
 TOSCA_KEYS = {
-    'XOSBase': [],
-    'User': ['email'],
-    'Privilege': [],
-    'AddressPool': ['name'],
-    'Controller': ['name'],
-    'ControllerImages': [],
-    'ControllerNetwork': [],
-    'ControllerRole': [],
-    'ControllerSite': [],
-    'ControllerPrivilege': [],
-    'ControllerSitePrivilege': [],
-    'ControllerSlice': [],
-    'ControllerSlicePrivilege': [],
-    'ControllerUser': [],
-    'Deployment': ['name'],
-    'DeploymentPrivilege': [],
-    'DeploymentRole': [],
-    'Diag': ['name'],
-    'Flavor': ['name'],
-    'Image': ['name'],
-    'ImageDeployments': [],
-    'Instance': ['name'],
-    'Network': ['name'],
-    'NetworkParameter': [],
-    'NetworkParameterType': ['name'],
-    'NetworkSlice': ['network', 'slice'],
-    'NetworkTemplate': ['name'],
-    'Node': ['name'],
-    'NodeLabel': ['name'],
-    'Port': [],
-    'Role': [],
-    'Service': ['name'],
-    'ServiceAttribute': ['name'],
-    'ServiceDependency': ['provider_service'],
-    'ServiceMonitoringAgentInfo': ['name'],
-    'ServicePrivilege': [],
-    'ServiceRole': [],
-    'Site': ['name'],
-    'SiteDeployment': ['site', 'deployment'],
-    'SitePrivilege': ['site', 'role'],
-    'SiteRole': [],
-    'Slice': ['name'],
-    'SlicePrivilege': [],
-    'SliceRole': [],
-    'Tag': ['name'],
-    'InterfaceType': ['name'],
-    'ServiceInterface': ['service', 'interface_type'],
-    'ServiceInstance': ['name'],
-    'ServiceInstanceLink': ['provider_service_instance'],
-    'ServiceInstanceAttribute': ['name'],
-    'TenantWithContainer': ['name'],
-    'XOS': ['name'],
-    'XOSGuiExtension': ['name'],
-}
\ No newline at end of file
+    "XOSBase": [],
+    "User": ["email"],
+    "Privilege": [],
+    "AddressPool": ["name"],
+    "Controller": ["name"],
+    "ControllerImages": [],
+    "ControllerNetwork": [],
+    "ControllerRole": [],
+    "ControllerSite": [],
+    "ControllerPrivilege": [],
+    "ControllerSitePrivilege": [],
+    "ControllerSlice": [],
+    "ControllerSlicePrivilege": [],
+    "ControllerUser": [],
+    "Deployment": ["name"],
+    "DeploymentPrivilege": [],
+    "DeploymentRole": [],
+    "Diag": ["name"],
+    "Flavor": ["name"],
+    "Image": ["name"],
+    "ImageDeployments": [],
+    "Instance": ["name"],
+    "Network": ["name"],
+    "NetworkParameter": [],
+    "NetworkParameterType": ["name"],
+    "NetworkSlice": ["network", "slice"],
+    "NetworkTemplate": ["name"],
+    "Node": ["name"],
+    "NodeLabel": ["name"],
+    "Port": [],
+    "Role": [],
+    "Service": ["name"],
+    "ServiceAttribute": ["name"],
+    "ServiceDependency": ["provider_service"],
+    "ServiceMonitoringAgentInfo": ["name"],
+    "ServicePrivilege": [],
+    "ServiceRole": [],
+    "Site": ["name"],
+    "SiteDeployment": ["site", "deployment"],
+    "SitePrivilege": ["site", "role"],
+    "SiteRole": [],
+    "Slice": ["name"],
+    "SlicePrivilege": [],
+    "SliceRole": [],
+    "Tag": ["name"],
+    "InterfaceType": ["name"],
+    "ServiceInterface": ["service", "interface_type"],
+    "ServiceInstance": ["name"],
+    "ServiceInstanceLink": ["provider_service_instance"],
+    "ServiceInstanceAttribute": ["name"],
+    "TenantWithContainer": ["name"],
+    "XOS": ["name"],
+    "XOSGuiExtension": ["name"],
+}
diff --git a/src/grpc_client/__init__.py b/src/grpc_client/__init__.py
index d4e8062..c7879fb 100644
--- a/src/grpc_client/__init__.py
+++ b/src/grpc_client/__init__.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,4 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
 
+from .main import GRPC_Client
+
+__all__ = [
+    'GRPC_Client',
+]
diff --git a/src/grpc_client/main.py b/src/grpc_client/main.py
index 619b43b..314081f 100644
--- a/src/grpc_client/main.py
+++ b/src/grpc_client/main.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,23 +13,27 @@
 # limitations under the License.
 
 
-import functools
-from xosapi.xos_grpc_client import SecureClient, InsecureClient
-from twisted.internet import defer
-from resources import RESOURCES
-from xosconfig import Config
-from twisted.internet import reactor
+from __future__ import absolute_import
 
-from xosconfig import Config
+import functools
+
+from xosapi.xos_grpc_client import InsecureClient, SecureClient
+
 from multistructlog import create_logger
-log = create_logger(Config().get('logging'))
+from twisted.internet import defer, reactor
+from xosconfig import Config
+
+from .resources import RESOURCES
+
+log = create_logger(Config().get("logging"))
+
 
 class GRPC_Client:
     def __init__(self):
         self.client = None
 
-        insecure = Config.get('gprc_endpoint')
-        secure = Config.get('gprc_endpoint')
+        insecure = Config.get("gprc_endpoint")
+        secure = Config.get("gprc_endpoint")
 
         self.grpc_secure_endpoint = secure + ":50051"
         self.grpc_insecure_endpoint = insecure + ":50055"
@@ -55,26 +58,36 @@
         self.client = InsecureClient(endpoint=self.grpc_insecure_endpoint)
         self.client.restart_on_disconnect = True
 
-        self.client.set_reconnect_callback(functools.partial(deferred.callback, self.client))
+        self.client.set_reconnect_callback(
+            functools.partial(deferred.callback, self.client)
+        )
         self.client.start()
 
         return deferred
 
     def create_secure_client(self, username, password, arg):
         """
-        This method will check if this combination of username/password already has stored orm classes in RESOURCES, otherwise create them
+        This method will check if this combination of username/password already
+        has stored orm classes in RESOURCES, otherwise create them
         """
         deferred = defer.Deferred()
         key = "%s~%s" % (username, password)
         if key in RESOURCES:
             reactor.callLater(0, deferred.callback, arg)
         else:
-            local_cert = Config.get('local_cert')
-            client = SecureClient(endpoint=self.grpc_secure_endpoint, username=username, password=password, cacert=local_cert)
+            local_cert = Config.get("local_cert")
+            client = SecureClient(
+                endpoint=self.grpc_secure_endpoint,
+                username=username,
+                password=password,
+                cacert=local_cert,
+            )
             client.restart_on_disconnect = True
             # SecureClient is preceeded by an insecure client, so treat all secure clients as previously connected
             # See CORD-3152
             client.was_connected = True
-            client.set_reconnect_callback(functools.partial(self.setup_resources, client, key, deferred, arg))
+            client.set_reconnect_callback(
+                functools.partial(self.setup_resources, client, key, deferred, arg)
+            )
             client.start()
         return deferred
diff --git a/src/grpc_client/models_accessor.py b/src/grpc_client/models_accessor.py
index 2c96c48..445918a 100644
--- a/src/grpc_client/models_accessor.py
+++ b/src/grpc_client/models_accessor.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,11 +12,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from xosconfig import Config
-from multistructlog import create_logger
-log = create_logger(Config().get('logging'))
+from __future__ import absolute_import
 
-from resources import RESOURCES
+from multistructlog import create_logger
+from xosconfig import Config
+
+from .resources import RESOURCES
+
+log = create_logger(Config().get("logging"))
+
+
+class GRPCModelsException(Exception):
+    """
+    Differentiates between exceptions created by GRPCModelsAccessor and other exceptions
+    """
+    pass
+
 
 class GRPCModelsAccessor:
     """
@@ -31,24 +41,31 @@
         """
 
         # NOTE: we need to import this later as it's generated by main.py
-        from KEYS import TOSCA_KEYS
+        from .KEYS import TOSCA_KEYS
 
         # get the key for this model
         try:
             filter_keys = TOSCA_KEYS[class_name]
-        except KeyError, e:
-            raise Exception("[XOS-TOSCA] Model %s doesn't have a tosca_key specified" % (class_name))
+        except KeyError:
+            raise GRPCModelsException(
+                "[XOS-TOSCA] Model %s doesn't have a tosca_key specified" % (class_name)
+            )
 
         if len(filter_keys) == 0:
-            raise Exception("[XOS-TOSCA] Model %s doesn't have a tosca_key specified" % (class_name))
+            raise GRPCModelsException(
+                "[XOS-TOSCA] Model %s doesn't have a tosca_key specified" % (class_name)
+            )
 
         filter = {}
         for k in filter_keys:
             if isinstance(k, str):
                 try:
                     filter[k] = data[k]
-                except KeyError, e:
-                    raise Exception("[XOS-TOSCA] Model %s doesn't have a property for the specified tosca_key (%s)" % (class_name, e))
+                except KeyError as e:
+                    raise GRPCModelsException(
+                        "[XOS-TOSCA] Model %s doesn't have a property for the specified tosca_key (%s)"
+                        % (class_name, e)
+                    )
             elif isinstance(k, list):
                 # one of they keys in this list has to be set
                 one_of_key = None
@@ -57,15 +74,23 @@
                         one_of_key = i
                         one_of_key_val = data[i]
                 if not one_of_key:
-                    raise Exception("[XOS-TOSCA] Model %s doesn't have a property for the specified tosca_key_one_of (%s)" % (class_name, k))
+                    raise GRPCModelsException(
+                        "[XOS-TOSCA] Model %s doesn't have a property for the specified tosca_key_one_of (%s)"
+                        % (class_name, k)
+                    )
                 else:
                     filter[one_of_key] = one_of_key_val
 
         key = "%s~%s" % (username, password)
-        if not key in RESOURCES:
-            raise Exception("[XOS-TOSCA] User '%s' does not have ready resources" % username)
+        if key not in RESOURCES:
+            raise GRPCModelsException(
+                "[XOS-TOSCA] User '%s' does not have ready resources" % username
+            )
         if class_name not in RESOURCES[key]:
-            raise Exception('[XOS-TOSCA] The model you are trying to create (class: %s, properties, %s) is not know by xos-core' % (class_name, str(filter)))
+            raise GRPCModelsException(
+                "[XOS-TOSCA] The model you are trying to create (class: %s, properties, %s) is not know by xos-core"
+                % (class_name, str(filter))
+            )
 
         cls = RESOURCES[key][class_name]
 
@@ -73,15 +98,27 @@
 
         if len(models) == 1:
             model = models[0]
-            log.info("[XOS-Tosca] Model of class %s and properties %s already exist, retrieving instance..." % (class_name, str(filter)), model=model)
+            log.info(
+                "[XOS-TOSCA] Model of class %s and properties %s already exist, retrieving instance..."
+                % (class_name, str(filter)),
+                model=model,
+            )
         elif len(models) == 0:
 
-            if 'must-exist' in data and data['must-exist']:
-                raise Exception("[XOS-TOSCA] Model of class %s and properties %s has property 'must-exist' but cannot be found" % (class_name, str(filter)))
+            if "must-exist" in data and data["must-exist"]:
+                raise GRPCModelsException(
+                    "[XOS-TOSCA] Model of class %s and properties %s has property 'must-exist' but cannot be found"
+                    % (class_name, str(filter))
+                )
 
             model = cls.objects.new()
-            log.info("[XOS-Tosca] Model (%s) is new, creating new instance..." % str(filter))
+            log.info(
+                "[XOS-TOSCA] Model (%s) is new, creating new instance..." % str(filter)
+            )
         else:
-            raise Exception("[XOS-Tosca] Model of class %s and properties %s has multiple instances, I can't handle it" % (class_name, str(filter)))
+            raise GRPCModelsException(
+                "[XOS-TOSCA] Model of class %s and properties %s has multiple instances, I can't handle it"
+                % (class_name, str(filter))
+            )
 
-        return model
\ No newline at end of file
+        return model
diff --git a/src/grpc_client/resources.py b/src/grpc_client/resources.py
index cfe3e2e..88ce137 100644
--- a/src/grpc_client/resources.py
+++ b/src/grpc_client/resources.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,4 +14,4 @@
 
 
 # NOTE will add all the resources in this dictionary
-RESOURCES = {}
\ No newline at end of file
+RESOURCES = {}