Merge "Bug 502 improvements"
[osm/N2VC.git] / n2vc / vnf.py
index 7244b22..1b9efa8 100644 (file)
@@ -36,6 +36,7 @@ class JujuCharmNotFound(Exception):
 class JujuApplicationExists(Exception):
     """The Application already exists."""
 
 class JujuApplicationExists(Exception):
     """The Application already exists."""
 
+
 class N2VCPrimitiveExecutionFailed(Exception):
     """Something failed while attempting to execute a primitive."""
 
 class N2VCPrimitiveExecutionFailed(Exception):
     """Something failed while attempting to execute a primitive."""
 
@@ -46,42 +47,69 @@ logging.getLogger('juju.client.connection').setLevel(logging.WARN)
 logging.getLogger('juju.model').setLevel(logging.WARN)
 logging.getLogger('juju.machine').setLevel(logging.WARN)
 
 logging.getLogger('juju.model').setLevel(logging.WARN)
 logging.getLogger('juju.machine').setLevel(logging.WARN)
 
+
 class VCAMonitor(ModelObserver):
     """Monitor state changes within the Juju Model."""
 class VCAMonitor(ModelObserver):
     """Monitor state changes within the Juju Model."""
-    callback = None
-    callback_args = None
     log = None
     ns_name = None
     log = None
     ns_name = None
-    application_name = None
+    applications = {}
 
 
-    def __init__(self, ns_name, application_name, callback, *args):
+    def __init__(self, ns_name):
         self.log = logging.getLogger(__name__)
 
         self.ns_name = ns_name
         self.log = logging.getLogger(__name__)
 
         self.ns_name = ns_name
-        self.application_name = application_name
-        self.callback = callback
-        self.callback_args = args
+
+    def AddApplication(self, application_name, callback, *callback_args):
+        if application_name not in self.applications:
+            self.applications[application_name] = {
+                'callback': callback,
+                'callback_args': callback_args
+            }
+
+    def RemoveApplication(self, application_name):
+        if application_name in self.applications:
+            del self.applications[application_name]
 
     async def on_change(self, delta, old, new, model):
         """React to changes in the Juju model."""
 
         if delta.entity == "unit":
 
     async def on_change(self, delta, old, new, model):
         """React to changes in the Juju model."""
 
         if delta.entity == "unit":
+            # Ignore change events from other applications
+            if delta.data['application'] not in self.applications.keys():
+                return
+
             try:
             try:
+
+                application_name = delta.data['application']
+
+                callback = self.applications[application_name]['callback']
+                callback_args = self.applications[application_name]['callback_args']
+
                 if old and new:
                     old_status = old.workload_status
                     new_status = new.workload_status
 
                     if old_status == new_status:
                 if old and new:
                     old_status = old.workload_status
                     new_status = new.workload_status
 
                     if old_status == new_status:
-
                         """The workload status may fluctuate around certain events,
                         so wait until the status has stabilized before triggering
                         the callback."""
                         """The workload status may fluctuate around certain events,
                         so wait until the status has stabilized before triggering
                         the callback."""
-                        if self.callback:
-                            self.callback(
+                        if callback:
+                            callback(
                                 self.ns_name,
                                 self.ns_name,
-                                self.application_name,
+                                delta.data['application'],
                                 new_status,
                                 new_status,
-                                *self.callback_args)
+                                new.workload_status_message,
+                                *callback_args)
+
+                if old and not new:
+                    # This is a charm being removed
+                    if callback:
+                        callback(
+                            self.ns_name,
+                            delta.data['application'],
+                            "removed",
+                            "",
+                            *callback_args)
             except Exception as e:
                 self.log.debug("[1] notify_callback exception {}".format(e))
         elif delta.entity == "action":
             except Exception as e:
                 self.log.debug("[1] notify_callback exception {}".format(e))
         elif delta.entity == "action":
@@ -108,6 +136,7 @@ class VCAMonitor(ModelObserver):
 # Create unique models per network service
 # Document all public functions
 
 # Create unique models per network service
 # Document all public functions
 
+
 class N2VC:
 
     # Juju API
 class N2VC:
 
     # Juju API
@@ -185,10 +214,10 @@ class N2VC:
         """Close any open connections."""
         yield self.logout()
 
         """Close any open connections."""
         yield self.logout()
 
-    def notify_callback(self, model_name, application_name, status, callback=None, *callback_args):
+    def notify_callback(self, model_name, application_name, status, message, callback=None, *callback_args):
         try:
             if callback:
         try:
             if callback:
-                callback(model_name, application_name, status, *callback_args)
+                callback(model_name, application_name, status, message, *callback_args)
         except Exception as e:
             self.log.error("[0] notify_callback exception {}".format(e))
             raise e
         except Exception as e:
             self.log.error("[0] notify_callback exception {}".format(e))
             raise e
@@ -219,10 +248,7 @@ class N2VC:
 
         Deploy the charm(s) referenced in a VNF Descriptor.
 
 
         Deploy the charm(s) referenced in a VNF Descriptor.
 
-        You can pass either the nsd record or the id of the network
-        service, but this method will fail without one of them.
-
-        :param str ns_name: The name of the network service
+        :param str model_name: The name of the network service.
         :param str application_name: The name of the application
         :param dict vnfd: The name of the application
         :param str charm_path: The path to the Juju charm
         :param str application_name: The name of the application
         :param dict vnfd: The name of the application
         :param str charm_path: The path to the Juju charm
@@ -271,15 +297,17 @@ class N2VC:
         ########################################
         app = await self.get_application(model, application_name)
         if app:
         ########################################
         app = await self.get_application(model, application_name)
         if app:
-            raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model))
+            raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
 
 
-        ############################################################
-        # Create a monitor to watch for application status changes #
-        ############################################################
+        ################################################################
+        # Register this application with the model-level event monitor #
+        ################################################################
         if callback:
         if callback:
-            self.log.debug("Setting monitor<->callback")
-            self.monitors[application_name] = VCAMonitor(model_name, application_name, callback, *callback_args)
-            model.add_observer(self.monitors[application_name])
+            self.monitors[model_name].AddApplication(
+                application_name,
+                callback,
+                *callback_args
+            )
 
         ########################################################
         # Check for specific machine placement (native charms) #
 
         ########################################################
         # Check for specific machine placement (native charms) #
@@ -345,14 +373,16 @@ class N2VC:
                     # This is applied when the Application is deployed
                     pass
                 else:
                     # This is applied when the Application is deployed
                     pass
                 else:
-                    # TODO: We need to sort by seq, and queue the actions in order.
-
                     seq = primitive['seq']
 
                     seq = primitive['seq']
 
+                    params = {}
+                    if 'parameter' in primitive:
+                        params = primitive['parameter']
+
                     primitives[seq] = {
                         'name': primitive['name'],
                         'parameters': self._map_primitive_parameters(
                     primitives[seq] = {
                         'name': primitive['name'],
                         'parameters': self._map_primitive_parameters(
-                            primitive['parameter'],
+                            params,
                             {'<rw_mgmt_ip>': rw_mgmt_ip}
                         ),
                     }
                             {'<rw_mgmt_ip>': rw_mgmt_ip}
                         ),
                     }
@@ -367,12 +397,30 @@ class N2VC:
                             **primitives[primitive]['parameters'],
                         )
             except N2VCPrimitiveExecutionFailed as e:
                             **primitives[primitive]['parameters'],
                         )
             except N2VCPrimitiveExecutionFailed as e:
-                self.debug.log(
+                self.log.debug(
                     "[N2VC] Exception executing primitive: {}".format(e)
                 )
                 raise
 
     async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
                     "[N2VC] Exception executing primitive: {}".format(e)
                 )
                 raise
 
     async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
+        """Execute a primitive of a charm for Day 1 or Day 2 configuration.
+
+        Execute a primitive defined in the VNF descriptor.
+
+        :param str model_name: The name of the network service.
+        :param str application_name: The name of the application
+        :param str primitive: The name of the primitive to execute.
+        :param obj callback: A callback function to receive status changes.
+        :param tuple callback_args: A list of arguments to be passed to the callback function.
+        :param dict params: A dictionary of key=value pairs representing the primitive's parameters
+          Examples::
+          {
+            'rw_mgmt_ip': '1.2.3.4',
+            # Pass the initial-config-primitives section of the vnf or vdu
+            'initial-config-primitives': {...}
+          }
+        """
+        uuid = None
         try:
             if not self.authenticated:
                 await self.login()
         try:
             if not self.authenticated:
                 await self.login()
@@ -380,11 +428,14 @@ class N2VC:
             # FIXME: This is hard-coded until model-per-ns is added
             model_name = 'default'
 
             # FIXME: This is hard-coded until model-per-ns is added
             model_name = 'default'
 
+            model = await self.controller.get_model(model_name)
+
             if primitive == 'config':
                 # config is special, and expecting params to be a dictionary
             if primitive == 'config':
                 # config is special, and expecting params to be a dictionary
-                await self.set_config(application_name, params['params'])
+                self.log.debug("Setting charm configuration for {}".format(application_name))
+                self.log.debug(params['params'])
+                await self.set_config(model, application_name, params['params'])
             else:
             else:
-                model = await self.controller.get_model(model_name)
                 app = await self.get_application(model, application_name)
                 if app:
                     # Run against the first (and probably only) unit in the app
                 app = await self.get_application(model, application_name)
                 if app:
                     # Run against the first (and probably only) unit in the app
@@ -392,13 +443,23 @@ class N2VC:
                     if unit:
                         self.log.debug("Executing primitive {}".format(primitive))
                         action = await unit.run_action(primitive, **params)
                     if unit:
                         self.log.debug("Executing primitive {}".format(primitive))
                         action = await unit.run_action(primitive, **params)
-                        # action = await action.wait()
+                        uuid = action.id
                 await model.disconnect()
         except Exception as e:
             self.log.debug("Caught exception while executing primitive: {}".format(e))
                 await model.disconnect()
         except Exception as e:
             self.log.debug("Caught exception while executing primitive: {}".format(e))
-            raise e
+            raise N2VCPrimitiveExecutionFailed(e)
+        return uuid
 
     async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
 
     async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
+        """Remove a charm from the VCA.
+
+        Remove a charm referenced in a VNF Descriptor.
+
+        :param str model_name: The name of the network service.
+        :param str application_name: The name of the application
+        :param obj callback: A callback function to receive status changes.
+        :param tuple callback_args: A list of arguments to be passed to the callback function.
+        """
         try:
             if not self.authenticated:
                 await self.login()
         try:
             if not self.authenticated:
                 await self.login()
@@ -406,9 +467,16 @@ class N2VC:
             model = await self.get_model(model_name)
             app = await self.get_application(model, application_name)
             if app:
             model = await self.get_model(model_name)
             app = await self.get_application(model, application_name)
             if app:
-                self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
+                # Remove this application from event monitoring
+                self.monitors[model_name].RemoveApplication(application_name)
+
+                # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
+                self.log.debug("Removing the application {}".format(application_name))
                 await app.remove()
                 await app.remove()
+
+                # Notify the callback that this charm has been removed.
                 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
                 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
+
         except Exception as e:
             print("Caught exception: {}".format(e))
             self.log.debug(e)
         except Exception as e:
             print("Caught exception: {}".format(e))
             self.log.debug(e)
@@ -417,9 +485,19 @@ class N2VC:
     async def DestroyNetworkService(self, nsd):
         raise NotImplementedError()
 
     async def DestroyNetworkService(self, nsd):
         raise NotImplementedError()
 
-    async def GetMetrics(self, nsd, vnfd):
-        """Get the metrics collected by the VCA."""
-        raise NotImplementedError()
+    async def GetMetrics(self, model_name, application_name):
+        """Get the metrics collected by the VCA.
+
+        :param model_name The name of the model
+        :param application_name The name of the application
+        """
+        metrics = {}
+        model = await self.get_model(model_name)
+        app = await self.get_application(model, application_name)
+        if app:
+            metrics = await app.get_metrics()
+
+        return metrics
 
     # Non-public methods
     async def add_relation(self, a, b, via=None):
 
     # Non-public methods
     async def add_relation(self, a, b, via=None):
@@ -440,12 +518,12 @@ class N2VC:
         finally:
             await m.disconnect()
 
         finally:
             await m.disconnect()
 
-    async def apply_config(self, config, application):
-        """Apply a configuration to the application."""
-        print("JujuApi: Applying configuration to {}.".format(
-            application
-        ))
-        return await self.set_config(application=application, config=config)
+    async def apply_config(self, config, application):
+        """Apply a configuration to the application."""
+        print("JujuApi: Applying configuration to {}.".format(
+            application
+        ))
+        return await self.set_config(application=application, config=config)
 
     def _get_config_from_dict(self, config_primitive, values):
         """Transform the yang config primitive to dict.
 
     def _get_config_from_dict(self, config_primitive, values):
         """Transform the yang config primitive to dict.
@@ -569,6 +647,10 @@ class N2VC:
             print("connecting to model {}".format(model_name))
             self.models[model_name] = await self.controller.get_model(model_name)
 
             print("connecting to model {}".format(model_name))
             self.models[model_name] = await self.controller.get_model(model_name)
 
+            # Create an observer for this model
+            self.monitors[model_name] = VCAMonitor(model_name)
+            self.models[model_name].add_observer(self.monitors[model_name])
+
         return self.models[model_name]
 
     async def login(self):
         return self.models[model_name]
 
     async def login(self):
@@ -709,12 +791,12 @@ class N2VC:
 
         return result
 
 
         return result
 
-    async def set_config(self, application, config):
+    async def set_config(self, model_name, application, config):
         """Apply a configuration to the application."""
         if not self.authenticated:
             await self.login()
 
         """Apply a configuration to the application."""
         if not self.authenticated:
             await self.login()
 
-        app = await self.get_application(self.default_model, application)
+        app = await self.get_application(model_name, application)
         if app:
             self.log.debug("JujuApi: Setting config for Application {}".format(
                 application,
         if app:
             self.log.debug("JujuApi: Setting config for Application {}".format(
                 application,
@@ -727,20 +809,20 @@ class N2VC:
                 if config[key] != newconf[key]['value']:
                     self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
 
                 if config[key] != newconf[key]['value']:
                     self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
 
-    async def set_parameter(self, parameter, value, application=None):
-        """Set a config parameter for a service."""
-        if not self.authenticated:
-            await self.login()
-
-        self.log.debug("JujuApi: Setting {}={} for Application {}".format(
-            parameter,
-            value,
-            application,
-        ))
-        return await self.apply_config(
-            {parameter: value},
-            application=application,
-        )
+    async def set_parameter(self, parameter, value, application=None):
+        """Set a config parameter for a service."""
+        if not self.authenticated:
+            await self.login()
+    #
+        self.log.debug("JujuApi: Setting {}={} for Application {}".format(
+            parameter,
+            value,
+            application,
+        ))
+        return await self.apply_config(
+            {parameter: value},
+            application=application,
+        )
 
     async def wait_for_application(self, name, timeout=300):
         """Wait for an application to become active."""
 
     async def wait_for_application(self, name, timeout=300):
         """Wait for an application to become active."""