X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Fvnf.py;h=a60f36cdf72ca51a05b0211b1ca16a242abbe080;hp=df3ec00f06bb4fb86427f0b90e6b4acefc7f9431;hb=95e2d7d8fd3798c521909164656bf568bf3bff85;hpb=5e08a0e8fa4fd9d0156d28f8f4e53e5b176c704a diff --git a/n2vc/vnf.py b/n2vc/vnf.py index df3ec00..a60f36c 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -3,7 +3,9 @@ import logging import os import os.path import re +import shlex import ssl +import subprocess import sys # import time @@ -17,7 +19,7 @@ if path not in sys.path: from juju.controller import Controller from juju.model import ModelObserver - +from juju.errors import JujuAPIError # We might need this to connect to the websocket securely, but test and verify. try: @@ -87,20 +89,14 @@ class VCAMonitor(ModelObserver): 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: - """The workload status may fluctuate around certain - events, so wait until the status has stabilized before - triggering the callback.""" - if callback: - callback( - self.ns_name, - delta.data['application'], - new_status, - new.workload_status_message, - *callback_args) + # Fire off a callback with the application state + if callback: + callback( + self.ns_name, + delta.data['application'], + new.workload_status, + new.workload_status_message, + *callback_args) if old and not new: # This is a charm being removed @@ -173,6 +169,12 @@ class N2VC: self.connecting = False self.authenticated = False + # For debugging + self.refcount = { + 'controller': 0, + 'model': 0, + } + self.models = {} self.default_model = None @@ -252,6 +254,100 @@ class N2VC: return self.default_model + async def Relate(self, ns_name, vnfd): + """Create a relation between the charm-enabled VDUs in a VNF. + + The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint. + + vdu: + ... + 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. + + :param str ns_name: The name of the network service. + :param dict vnfd: The parsed yaml VNF descriptor. + """ + + # Currently, the call to Relate() is made automatically after the + # deployment of each charm; if the relation depends on a charm that + # hasn't been deployed yet, the call will fail silently. This will + # prevent an API breakage, with the intent of making this an explicitly + # required call in a more object-oriented refactor of the N2VC API. + + configs = [] + vnf_config = vnfd.get("vnf-configuration") + if vnf_config: + juju = vnf_config['juju'] + if juju: + configs.append(vnf_config) + + for vdu in vnfd['vdu']: + vdu_config = vdu.get('vdu-configuration') + if vdu_config: + juju = vdu_config['juju'] + if juju: + configs.append(vdu_config) + + def _get_application_name(name): + """Get the application name that's mapped to a vnf/vdu.""" + vnf_member_index = 0 + vnf_name = vnfd['name'] + + for vdu in vnfd.get('vdu'): + # Compare the named portion of the relation to the vdu's id + if vdu['id'] == name: + application_name = self.FormatApplicationName( + ns_name, + vnf_name, + str(vnf_member_index), + ) + return application_name + else: + vnf_member_index += 1 + + return None + + # Loop through relations + for cfg in configs: + if 'juju' in cfg: + if 'relation' in juju: + for rel in juju['relation']: + try: + + # get the application name for the provides + (name, endpoint) = rel['provides'].split(':') + application_name = _get_application_name(name) + + provides = "{}:{}".format( + application_name, + endpoint + ) + + # get the application name for thr requires + (name, endpoint) = rel['requires'].split(':') + application_name = _get_application_name(name) + + requires = "{}:{}".format( + application_name, + endpoint + ) + self.log.debug("Relation: {} <-> {}".format( + provides, + requires + )) + await self.add_relation( + ns_name, + provides, + requires, + ) + except Exception as e: + self.log.debug("Exception: {}".format(e)) + + return + async def DeployCharms(self, model_name, application_name, vnfd, charm_path, params={}, machine_spec={}, callback=None, *callback_args): @@ -333,14 +429,22 @@ class N2VC: ######################################################## to = "" if machine_spec.keys(): - # TODO: This needs to be tested. - # if all(k in machine_spec for k in ['hostname', 'username']): - # # Enlist the existing machine in Juju - # machine = await self.model.add_machine(spec='ssh:%@%'.format( - # specs['host'], - # specs['user'], - # )) - # to = machine.id + if all(k in machine_spec for k in ['hostname', 'username']): + # Get the path to the previously generated ssh private key. + # Machines we're manually provisioned must have N2VC's public + # key injected, so if we don't have a keypair, raise an error. + private_key_path = "" + + # Enlist the existing machine in Juju + machine = await self.model.add_machine( + spec='ssh:{}@{}:{}'.format( + specs['host'], + specs['user'], + private_key_path, + ) + ) + # Set the machine id that the deploy below will use. + to = machine.id pass ####################################### @@ -351,9 +455,6 @@ class N2VC: if 'rw_mgmt_ip' in params: rw_mgmt_ip = params['rw_mgmt_ip'] - # initial_config = {} - # self.log.debug(type(params)) - # self.log.debug("Params: {}".format(params)) if 'initial-config-primitive' not in params: params['initial-config-primitive'] = {} @@ -382,10 +483,14 @@ class N2VC: series='xenial', # Apply the initial 'config' primitive during deployment config=initial_config, - # TBD: Where to deploy the charm to. - to=None, + # Where to deploy the charm to. + to=to, ) + # Map the vdu id<->app name, + # + await self.Relate(model_name, vnfd) + # ####################################### # # Execute initial config primitive(s) # # ####################################### @@ -487,6 +592,77 @@ class N2VC: return results + # async def ProvisionMachine(self, model_name, hostname, username): + # """Provision machine for usage with Juju. + # + # Provisions a previously instantiated machine for use with Juju. + # """ + # try: + # if not self.authenticated: + # await self.login() + # + # # FIXME: This is hard-coded until model-per-ns is added + # model_name = 'default' + # + # model = await self.get_model(model_name) + # model.add_machine(spec={}) + # + # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format( + # "ubuntu", + # host['address'], + # private_key_path, + # )) + # return machine.id + # + # except Exception as e: + # self.log.debug( + # "Caught exception while getting primitive status: {}".format(e) + # ) + # raise N2VCPrimitiveExecutionFailed(e) + + def GetPrivateKeyPath(self): + homedir = os.environ['HOME'] + sshdir = "{}/.ssh".format(homedir) + private_key_path = "{}/id_n2vc_rsa".format(sshdir) + return private_key_path + + async def GetPublicKey(self): + """Get the N2VC SSH public key.abs + + Returns the SSH public key, to be injected into virtual machines to + be managed by the VCA. + + The first time this is run, a ssh keypair will be created. The public + key is injected into a VM so that we can provision the machine with + Juju, after which Juju will communicate with the VM directly via the + juju agent. + """ + public_key = "" + + # Find the path to where we expect our key to live. + homedir = os.environ['HOME'] + sshdir = "{}/.ssh".format(homedir) + if not os.path.exists(sshdir): + os.mkdir(sshdir) + + private_key_path = "{}/id_n2vc_rsa".format(sshdir) + public_key_path = "{}.pub".format(private_key_path) + + # If we don't have a key generated, generate it. + if not os.path.exists(private_key_path): + cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( + "rsa", + "4096", + private_key_path + ) + subprocess.check_output(shlex.split(cmd)) + + # Read the public key + with open(public_key_path, "r") as f: + public_key = f.readline() + + return public_key + async def ExecuteInitialPrimitives(self, model_name, application_name, params, callback=None, *callback_args): """Execute multiple primitives. @@ -650,24 +826,39 @@ class N2VC: return metrics + async def HasApplication(self, model_name, application_name): + model = await self.get_model(model_name) + app = await self.get_application(model, application_name) + if app: + return True + return False + # Non-public methods - async def add_relation(self, a, b, via=None): + async def add_relation(self, model_name, relation1, relation2): """ Add a relation between two application endpoints. - :param a An application endpoint - :param b An application endpoint - :param via The egress subnet(s) for outbound traffic, e.g., - (192.168.0.0/16,10.0.0.0/8) + :param str model_name Name of the network service. + :param str relation1 '[:]' + :param str relation12 '[:]' """ + if not self.authenticated: await self.login() - m = await self.get_model() + m = await self.get_model(model_name) try: - m.add_relation(a, b, via) - finally: - await m.disconnect() + await m.add_relation(relation1, relation2) + except JujuAPIError as e: + # If one of the applications in the relationship doesn't exist, + # or the relation has already been added, let the operation fail + # silently. + if 'not found' in e.message: + return + if 'already exists' in e.message: + return + + raise e # async def apply_config(self, config, application): # """Apply a configuration to the application.""" @@ -811,6 +1002,7 @@ class N2VC: self.models[model_name] = await self.controller.get_model( model_name, ) + self.refcount['model'] += 1 # Create an observer for this model self.monitors[model_name] = VCAMonitor(model_name) @@ -846,6 +1038,7 @@ class N2VC: password=self.secret, cacert=cacert, ) + self.refcount['controller'] += 1 else: # current_controller no longer exists # self.log.debug("Connecting to current controller...") @@ -860,8 +1053,6 @@ class N2VC: self.authenticated = True self.log.debug("JujuApi: Logged into controller") - # self.default_model = await self.controller.get_model("default") - async def logout(self): """Logout of the Juju controller.""" if not self.authenticated: @@ -873,20 +1064,26 @@ class N2VC: self.default_model )) await self.default_model.disconnect() + self.refcount['model'] -= 1 self.default_model = None for model in self.models: await self.models[model].disconnect() - model = None + self.refcount['model'] -= 1 + self.models[model] = None if self.controller: self.log.debug("Disconnecting controller {}".format( self.controller )) await self.controller.disconnect() + self.refcount['controller'] -= 1 self.controller = None self.authenticated = False + + self.log.debug(self.refcount) + except Exception as e: self.log.fatal( "Fatal error logging out of Juju Controller: {}".format(e)