X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Fvnf.py;h=c8ee2ef64d7f3cf5cc833223f63a24d3a184ba81;hp=a486f2709b24e85350e4b913fbb6737eaf884f95;hb=9b1fde45c38da39b576c8cf1dbdfa8f4c3408844;hpb=d420a8b6f1fecde3983369b131da1f042c7c8a14 diff --git a/n2vc/vnf.py b/n2vc/vnf.py index a486f27..c8ee2ef 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -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) @@ -147,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='', + ca_cert='', + ) """ # Initialize instance-level variables @@ -189,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: @@ -221,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: @@ -244,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. @@ -296,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 @@ -400,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) # @@ -414,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 @@ -460,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) # @@ -670,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) @@ -726,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) ) @@ -758,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( @@ -863,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): """ @@ -1089,7 +1191,6 @@ class N2VC: self.log.debug("JujuApi: Logging into controller") - cacert = None self.controller = Controller(loop=self.loop) if self.secret: @@ -1105,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: