Fix bug 760
[osm/N2VC.git] / n2vc / vnf.py
index 1bdfe2f..c8ee2ef 100644 (file)
@@ -47,6 +47,9 @@ class NetworkServiceDoesNotExist(Exception):
     """The Network Service being acted against does not exist."""
 
 
+class PrimitiveDoesNotExist(Exception):
+    """The Primitive being executed does not exist."""
+
 # Quiet the debug logging
 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
@@ -57,13 +60,12 @@ logging.getLogger('juju.machine').setLevel(logging.WARN)
 class VCAMonitor(ModelObserver):
     """Monitor state changes within the Juju Model."""
     log = None
-    ns_name = None
-    applications = {}
 
     def __init__(self, ns_name):
         self.log = logging.getLogger(__name__)
 
         self.ns_name = ns_name
+        self.applications = {}
 
     def AddApplication(self, application_name, callback, *callback_args):
         if application_name not in self.applications:
@@ -148,22 +150,34 @@ class N2VC:
                  secret=None,
                  artifacts=None,
                  loop=None,
+                 juju_public_key=None,
+                 ca_cert=None,
                  ):
         """Initialize N2VC
-
-        :param vcaconfig dict A dictionary containing the VCA configuration
-
-        :param artifacts str The directory where charms required by a vnfd are
+        :param log obj: The logging object to log to
+        :param server str: The IP Address or Hostname of the Juju controller
+        :param port int: The port of the Juju Controller
+        :param user str: The Juju username to authenticate with
+        :param secret str: The Juju password to authenticate with
+        :param artifacts str: The directory where charms required by a vnfd are
             stored.
+        :param loop obj: The loop to use.
+        :param juju_public_key str: The contents of the Juju public SSH key
+        :param ca_cert str: The CA certificate to use to authenticate
+
 
         :Example:
-        n2vc = N2VC(vcaconfig={
-            'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
-            'user': 'admin',
-            'ip-address': '10.44.127.137',
-            'port': 17070,
-            'artifacts': '/path/to/charms'
-        })
+        client = n2vc.vnf.N2VC(
+            log=log,
+            server='10.1.1.28',
+            port=17070,
+            user='admin',
+            secret='admin',
+            artifacts='/app/storage/myvnf/charms',
+            loop=loop,
+            juju_public_key='<contents of the juju public key>',
+            ca_cert='<contents of CA certificate>',
+        )
         """
 
         # Initialize instance-level variables
@@ -190,6 +204,12 @@ class N2VC:
         self.username = ""
         self.secret = ""
 
+        self.juju_public_key = juju_public_key
+        if juju_public_key:
+            self._create_juju_public_key(juju_public_key)
+
+        self.ca_cert = ca_cert
+
         if log:
             self.log = log
         else:
@@ -222,6 +242,31 @@ class N2VC:
         """Close any open connections."""
         yield self.logout()
 
+    def _create_juju_public_key(self, public_key):
+        """Recreate the Juju public key on disk.
+
+        Certain libjuju commands expect to be run from the same machine as Juju
+         is bootstrapped to. This method will write the public key to disk in
+         that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
+        """
+        # Make sure that we have a public key before writing to disk
+        if public_key is None or len(public_key) == 0:
+            if 'OSM_VCA_PUBKEY' in os.environ:
+                public_key = os.getenv('OSM_VCA_PUBKEY', '')
+                if len(public_key == 0):
+                    return
+            else:
+                return
+
+        path = "{}/.local/share/juju/ssh".format(
+            os.path.expanduser('~'),
+        )
+        if not os.path.exists(path):
+            os.makedirs(path)
+
+            with open('{}/juju_id_rsa.pub'.format(path), 'w') as f:
+                f.write(public_key)
+
     def notify_callback(self, model_name, application_name, status, message,
                         callback=None, *callback_args):
         try:
@@ -245,9 +290,10 @@ class N2VC:
 
         vdu:
             ...
-            relation:
-            -   provides: dataVM:db
-                requires: mgmtVM:app
+            vca-relationships:
+                relation:
+                -   provides: dataVM:db
+                    requires: mgmtVM:app
 
         This tells N2VC that the charm referred to by the dataVM vdu offers a relation named 'db', and the mgmtVM vdu has an 'app' endpoint that should be connected to a database.
 
@@ -297,8 +343,9 @@ class N2VC:
         # Loop through relations
         for cfg in configs:
             if 'juju' in cfg:
-                if 'relation' in juju:
-                    for rel in juju['relation']:
+                juju = cfg['juju']
+                if 'vca-relationships' in juju and 'relation' in juju['vca-relationships']:
+                    for rel in juju['vca-relationships']['relation']:
                         try:
 
                             # get the application name for the provides
@@ -401,11 +448,10 @@ class N2VC:
         # Register this application with the model-level event monitor #
         ################################################################
         if callback:
-            self.monitors[model_name].AddApplication(
+            self.log.debug("JujuApi: Registering callback for {}".format(
                 application_name,
-                callback,
-                *callback_args
-            )
+            ))
+            await self.Subscribe(model_name, application_name, callback, *callback_args)
 
         ########################################################
         # Check for specific machine placement (native charms) #
@@ -415,8 +461,8 @@ class N2VC:
             if all(k in machine_spec for k in ['host', 'user']):
                 # Enlist an existing machine as a Juju unit
                 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
-                    machine_spec['user'],
-                    machine_spec['host'],
+                    machine_spec['username'],
+                    machine_spec['hostname'],
                     self.GetPrivateKeyPath(),
                 ))
                 to = machine.id
@@ -461,10 +507,19 @@ class N2VC:
             # Where to deploy the charm to.
             to=to,
         )
-
-        # Map the vdu id<->app name,
-        #
-        await self.Relate(model_name, vnfd)
+        #############################
+        # Map the vdu id<->app name #
+        #############################
+        try:
+            await self.Relate(model_name, vnfd)
+        except KeyError as ex:
+            # We don't currently support relations between NS and VNF/VDU charms
+            self.log.warn("[N2VC] Relations not supported: {}".format(ex))
+        except Exception as ex:
+            # This may happen if not all of the charms needed by the relation
+            # are ready. We can safely ignore this, because Relate will be
+            # retried when the endpoint of the relation is deployed.
+            self.log.warn("[N2VC] Relations not ready")
 
         # #######################################
         # # Execute initial config primitive(s) #
@@ -671,16 +726,25 @@ class N2VC:
                     }
 
                     for primitive in sorted(primitives):
-                        uuids.append(
-                            await self.ExecutePrimitive(
-                                model_name,
-                                application_name,
-                                primitives[primitive]['name'],
-                                callback,
-                                callback_args,
-                                **primitives[primitive]['parameters'],
+                        try:
+                            # self.log.debug("Queuing action {}".format(primitives[primitive]['name']))
+                            uuids.append(
+                                await self.ExecutePrimitive(
+                                    model_name,
+                                    application_name,
+                                    primitives[primitive]['name'],
+                                    callback,
+                                    callback_args,
+                                    **primitives[primitive]['parameters'],
+                                )
                             )
-                        )
+                        except PrimitiveDoesNotExist as e:
+                            self.log.debug("Ignoring exception PrimitiveDoesNotExist: {}".format(e))
+                            pass
+                        except Exception as e:
+                            self.log.debug("XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}".format(e))
+                            raise e
+
             except N2VCPrimitiveExecutionFailed as e:
                 self.log.debug(
                     "[N2VC] Exception executing primitive: {}".format(e)
@@ -727,12 +791,22 @@ class N2VC:
             else:
                 app = await self.get_application(model, application_name)
                 if app:
+                    # Does this primitive exist?
+                    actions = await app.get_actions()
+
+                    if primitive not in actions.keys():
+                        raise PrimitiveDoesNotExist("Primitive {} does not exist".format(primitive))
+
                     # Run against the first (and probably only) unit in the app
                     unit = app.units[0]
                     if unit:
                         action = await unit.run_action(primitive, **params)
                         uuid = action.id
+        except PrimitiveDoesNotExist as e:
+            # Catch and raise this exception if it's thrown from the inner block
+            raise e
         except Exception as e:
+            # An unexpected exception was caught
             self.log.debug(
                 "Caught exception while executing primitive: {}".format(e)
             )
@@ -759,7 +833,7 @@ class N2VC:
             app = await self.get_application(model, application_name)
             if app:
                 # Remove this application from event monitoring
-                self.monitors[model_name].RemoveApplication(application_name)
+                await self.Unsubscribe(model_name, application_name)
 
                 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
                 self.log.debug(
@@ -864,6 +938,33 @@ class N2VC:
             return True
         return False
 
+    async def Subscribe(self, ns_name, application_name, callback, *callback_args):
+        """Subscribe to callbacks for an application.
+
+        :param ns_name str: The name of the Network Service
+        :param application_name str: The name of the application
+        :param callback obj: The callback method
+        :param callback_args list: The list of arguments to append to calls to
+            the callback method
+        """
+        self.monitors[ns_name].AddApplication(
+            application_name,
+            callback,
+            *callback_args
+        )
+
+    async def Unsubscribe(self, ns_name, application_name):
+        """Unsubscribe to callbacks for an application.
+
+        Unsubscribes the caller from notifications from a deployed application.
+
+        :param ns_name str: The name of the Network Service
+        :param application_name str: The name of the application
+        """
+        self.monitors[ns_name].RemoveApplication(
+            application_name,
+        )
+
     # Non-public methods
     async def add_relation(self, model_name, relation1, relation2):
         """
@@ -1090,7 +1191,6 @@ class N2VC:
 
         self.log.debug("JujuApi: Logging into controller")
 
-        cacert = None
         self.controller = Controller(loop=self.loop)
 
         if self.secret:
@@ -1106,7 +1206,7 @@ class N2VC:
                 endpoint=self.endpoint,
                 username=self.user,
                 password=self.secret,
-                cacert=cacert,
+                cacert=self.ca_cert,
             )
             self.refcount['controller'] += 1
         else: