X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Fvnf.py;h=1c1208f430a3fd8ca6d4a6c93b32d5017dda3219;hp=9f0440538377d8c0cf0d918d3e1af6c19b13e09b;hb=refs%2Ftags%2Fv5.0.0;hpb=1ddca81e7e1863b0a7d45d1b9b9d5cabccd4e628 diff --git a/n2vc/vnf.py b/n2vc/vnf.py index 9f04405..1c1208f 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -19,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: @@ -176,7 +176,6 @@ class N2VC: } self.models = {} - self.default_model = None # Model Observers self.monitors = {} @@ -235,24 +234,99 @@ class N2VC: return True # Public methods - async def CreateNetworkService(self, nsd): - """Create a new model to encapsulate this network service. + async def Relate(self, model_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. - Create a new model in the Juju controller to encapsulate the - charms associated with a network service. + vdu: + ... + relation: + - provides: dataVM:db + requires: mgmtVM:app - You can pass either the nsd record or the id of the network - service, but this method will fail without one of them. + 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. """ - if not self.authenticated: - await self.login() - # Ideally, we will create a unique model per network service. - # This change will require all components, i.e., LCM and SO, to use - # N2VC for 100% compatibility. If we adopt unique models for the LCM, - # services deployed via LCM would't be manageable via SO and vice versa + # 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( + model_name, + vnf_name, + str(vnf_member_index), + ) + return application_name + else: + vnf_member_index += 1 + + return None - return self.default_model + # 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( + model_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={}, @@ -261,7 +335,7 @@ class N2VC: Deploy the charm(s) referenced in a VNF Descriptor. - :param str model_name: The name of the network service. + :param str model_name: The name or unique id 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 @@ -308,9 +382,6 @@ class N2VC: ########################################## # Get the model for this network service # ########################################## - # TODO: In a point release, we will use a model per deployed network - # service. In the meantime, we will always use the 'default' model. - model_name = 'default' model = await self.get_model(model_name) ######################################## @@ -335,23 +406,14 @@ class N2VC: ######################################################## to = "" if machine_spec.keys(): - 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. + 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'], + self.GetPrivateKeyPath(), + )) to = machine.id - pass ####################################### # Get the initial charm configuration # @@ -369,7 +431,8 @@ class N2VC: {'': rw_mgmt_ip} ) - self.log.debug("JujuApi: Deploying charm ({}) from {}".format( + self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format( + model_name, application_name, charm_path, to=to, @@ -393,14 +456,19 @@ class N2VC: to=to, ) + # Map the vdu id<->app name, + # + await self.Relate(model_name, vnfd) + # ####################################### # # Execute initial config primitive(s) # # ####################################### - await self.ExecuteInitialPrimitives( + uuids = await self.ExecuteInitialPrimitives( model_name, application_name, params, ) + return uuids # primitives = {} # @@ -453,9 +521,6 @@ class N2VC: 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) results = await model.get_action_status(uuid) @@ -481,9 +546,6 @@ class N2VC: 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) results = await model.get_action_output(uuid, 60) except Exception as e: @@ -621,7 +683,7 @@ class N2VC: Execute a primitive defined in the VNF descriptor. - :param str model_name: The name of the network service. + :param str model_name: The name or unique id 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. @@ -642,9 +704,6 @@ class N2VC: 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) if primitive == 'config': @@ -697,6 +756,8 @@ class N2VC: ) await app.remove() + await self.disconnect_model(self.monitors[model_name]) + # Notify the callback that this charm has been removed. self.notify_callback( model_name, @@ -717,7 +778,7 @@ class N2VC: async def GetMetrics(self, model_name, application_name): """Get the metrics collected by the VCA. - :param model_name The name of the model + :param model_name The name or unique id of the network service :param application_name The name of the application """ metrics = {} @@ -736,23 +797,31 @@ class N2VC: 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: The name or unique id of the network service + :param str relation1: '[:]' + :param str relation2: '[:]' """ + 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.""" @@ -787,11 +856,15 @@ class N2VC: params = {} for parameter in parameters: param = str(parameter['name']) + value = None + + # If there's no value, use the default-value (if set) + if parameter['value'] is None and 'default-value' in parameter: + value = parameter['default-value'] # Typecast parameter value, if present if 'data-type' in parameter: paramtype = str(parameter['data-type']).lower() - value = None if paramtype == "integer": value = int(parameter['value']) @@ -799,6 +872,9 @@ class N2VC: value = bool(parameter['value']) else: value = str(parameter['value']) + else: + # If there's no data-type, assume the value is a string + value = str(parameter['value']) if parameter['value'] == "": params[param] = str(values[parameter['value']]) @@ -820,7 +896,6 @@ class N2VC: return config - @staticmethod def FormatApplicationName(self, *args): """ Generate a Juju-compatible Application name @@ -836,7 +911,6 @@ class N2VC: FormatApplicationName("ping_pong_ns", "ping_vnf", "a") """ - appname = "" for c in "-".join(list(args)): if c.isdigit(): @@ -884,7 +958,7 @@ class N2VC: return app - async def get_model(self, model_name='default'): + async def get_model(self, model_name): """Get a model from the Juju Controller. Note: Model objects returned must call disconnected() before it goes @@ -893,9 +967,18 @@ class N2VC: await self.login() if model_name not in self.models: - self.models[model_name] = await self.controller.get_model( - model_name, - ) + # Get the models in the controller + models = await self.controller.list_models() + + if model_name not in models: + self.models[model_name] = await self.controller.add_model( + model_name + ) + else: + self.models[model_name] = await self.controller.get_model( + model_name + ) + self.refcount['model'] += 1 # Create an observer for this model @@ -953,18 +1036,8 @@ class N2VC: return try: - if self.default_model: - self.log.debug("Disconnecting model {}".format( - 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() - self.refcount['model'] -= 1 - self.models[model] = None + await self.disconnect_model(model) if self.controller: self.log.debug("Disconnecting controller {}".format( @@ -984,6 +1057,19 @@ class N2VC: ) raise e + async def disconnect_model(self, model): + self.log.debug("Disconnecting model {}".format(model)) + if model in self.models: + print(self.models[model].applications) + if len(self.models[model].applications) == 0: + print("Destroying empty model") + await self.controller.destroy_models(model) + + print("Disconnecting model") + await self.models[model].disconnect() + self.refcount['model'] -= 1 + self.models[model] = None + # async def remove_application(self, name): # """Remove the application.""" # if not self.authenticated: @@ -1013,12 +1099,14 @@ class N2VC: finally: await m.disconnect() - async def resolve_error(self, application=None): + async def resolve_error(self, model_name, application=None): """Resolve units in error state.""" if not self.authenticated: await self.login() - app = await self.get_application(self.default_model, application) + model = await self.get_model(model_name) + + app = await self.get_application(model, application) if app: self.log.debug( "JujuApi: Resolving errors for application {}".format( @@ -1029,7 +1117,7 @@ class N2VC: for unit in app.units: app.resolved(retry=True) - async def run_action(self, application, action_name, **params): + async def run_action(self, model_name, application, action_name, **params): """Execute an action and return an Action object.""" if not self.authenticated: await self.login() @@ -1040,7 +1128,10 @@ class N2VC: 'results': None, } } - app = await self.get_application(self.default_model, application) + + model = await self.get_model(model_name) + + app = await self.get_application(model, application) if app: # We currently only have one unit per application # so use the first unit available. @@ -1103,14 +1194,10 @@ class N2VC: if not self.authenticated: await self.login() - # TODO: In a point release, we will use a model per deployed network - # service. In the meantime, we will always use the 'default' model. - model_name = 'default' model = await self.get_model(model_name) app = await self.get_application(model, application_name) self.log.debug("Application: {}".format(app)) - # app = await self.get_application(model_name, application_name) if app: self.log.debug( "JujuApi: Waiting {} seconds for Application {}".format(