Additional fix for bug 733
[osm/N2VC.git] / n2vc / vnf.py
index 9cdbb33..bb7b871 100644 (file)
@@ -57,13 +57,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 +147,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 +201,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 +239,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 +287,10 @@ class N2VC:
 
         vdu:
             ...
-            relation:
-            -   provides: dataVM:db
-                requires: mgmtVM:app
+            vca-relations:
+                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 +340,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-relations' in juju and 'relations' in juju['vca-relations']:
+                    for rel in juju['vca-relations']['relation']:
                         try:
 
                             # get the application name for the provides
@@ -401,11 +445,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 +458,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 +504,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) #
@@ -759,7 +811,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(
@@ -769,11 +821,11 @@ class N2VC:
 
                 await self.disconnect_model(self.monitors[model_name])
 
-                # Notify the callback that this charm has been removed.
                 self.notify_callback(
                     model_name,
                     application_name,
                     "removed",
+                    "Removing charm {}".format(application_name),
                     callback,
                     *callback_args,
                 )
@@ -823,7 +875,7 @@ class N2VC:
 
         # Do not delete the default model. The default model was used by all
         # Network Services, prior to the implementation of a model per NS.
-        if ns_uuid.lower() is "default":
+        if ns_uuid.lower() == "default":
             return False
 
         if not self.authenticated:
@@ -836,7 +888,7 @@ class N2VC:
 
         try:
             await self.controller.destroy_models(ns_uuid)
-        except JujuError as e:
+        except JujuError:
             raise NetworkServiceDoesNotExist(
                 "The Network Service '{}' does not exist".format(ns_uuid)
             )
@@ -864,6 +916,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 +1169,6 @@ class N2VC:
 
         self.log.debug("JujuApi: Logging into controller")
 
-        cacert = None
         self.controller = Controller(loop=self.loop)
 
         if self.secret:
@@ -1106,7 +1184,7 @@ class N2VC:
                 endpoint=self.endpoint,
                 username=self.user,
                 password=self.secret,
-                cacert=cacert,
+                cacert=self.ca_cert,
             )
             self.refcount['controller'] += 1
         else: