From: Adam Israel Date: Mon, 5 Feb 2018 12:52:15 +0000 (+0100) Subject: Add juju support to lcm X-Git-Tag: v3.0.3~1 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=c887cc2fdd02c898ecd1f08d6927043027fd07eb;p=osm%2FRO.git Add juju support to lcm This adds basic Juju support to the lcm. - Add a ModelObserver to watch model changes and update nsr_lcm when a charm is deployed and either ready or blocked. - Refactor DeployApplication to take ns and vnf record as parameters and extract necessary information to deploy and configure charm - Add basic checks for proxy/non-proxy charm - Rebase against latest lcm changes - Automatically remove a charm when deleting the associated network service - Revert lcm.cfg - Address changes per discussion this morning TODO: - Finish adding non-proxy charm support (need to pick appropriate example to highlight this) Signed-off-by: Adam Israel Change-Id: Iffd24293722a050630bcb5812d3f15217067b238 --- diff --git a/lcm/osm_lcm/juju_api.py b/lcm/osm_lcm/juju_api.py new file mode 100644 index 00000000..9b8fa5ad --- /dev/null +++ b/lcm/osm_lcm/juju_api.py @@ -0,0 +1,855 @@ +############################################################################ +# Copyright 2016 RIFT.io Inc # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ + +import argparse +import asyncio +import logging +import os +import ssl + +import juju.loop +from juju.controller import Controller +from juju.model import Model, ModelObserver + +try: + ssl._create_default_https_context = ssl._create_unverified_context +except AttributeError: + # Legacy Python doesn't verify by default (see pep-0476) + # https://www.python.org/dev/peps/pep-0476/ + pass + + +class JujuVersionError(Exception): + pass + + +class JujuApiError(Exception): + pass + + +class JujuEnvError(JujuApiError): + pass + + +class JujuModelError(JujuApiError): + pass + + +class JujuStatusError(JujuApiError): + pass + + +class JujuUnitsError(JujuApiError): + pass + + +class JujuWaitUnitsError(JujuApiError): + pass + + +class JujuSrvNotDeployedError(JujuApiError): + pass + + +class JujuAddCharmError(JujuApiError): + pass + + +class JujuDeployError(JujuApiError): + pass + + +class JujuDestroyError(JujuApiError): + pass + + +class JujuResolveError(JujuApiError): + pass + + +class JujuActionError(JujuApiError): + pass + + +class JujuActionApiError(JujuActionError): + pass + + +class JujuActionInfoError(JujuActionError): + pass + + +class JujuActionExecError(JujuActionError): + pass + + +class JujuAuthenticationError(Exception): + pass + + +class JujuMonitor(ModelObserver): + """Monitor state changes within the Juju Model.""" + # async def on_change(self, delta, old, new, model): + # """React to changes in the Juju model.""" + # + # # TODO: Setup the hook to update the UI if the status of a unit changes + # # to be used when deploying a charm and waiting for it to be "ready" + # if delta.entity in ['application', 'unit'] and delta.type == "change": + # pass + # + # # TODO: Add a hook when an action is complete + + pass + + +class JujuApi(object): + """JujuApi wrapper on jujuclient library. + + There should be one instance of JujuApi for each VNF manged by Juju. + + Assumption: + Currently we use one unit per service/VNF. So once a service + is deployed, we store the unit name and reuse it + """ + + log = None + controller = None + models = {} + model = None + model_name = None + model_uuid = None + authenticated = False + + def __init__(self, + log=None, + loop=None, + server='127.0.0.1', + port=17070, + user='admin', + secret=None, + version=None, + model_name='default', + ): + """Initialize with the Juju credentials.""" + + if log: + self.log = log + else: + self.log = logging.getLogger(__name__) + + # Quiet websocket traffic + logging.getLogger('websockets.protocol').setLevel(logging.INFO) + + self.log.debug('JujuApi: instantiated') + + self.server = server + self.port = port + + self.secret = secret + if user.startswith('user-'): + self.user = user + else: + self.user = 'user-{}'.format(user) + + self.endpoint = '%s:%d' % (server, int(port)) + + self.model_name = model_name + + if loop: + self.loop = loop + + def __del__(self): + """Close any open connections.""" + yield self.logout() + + async def add_relation(self, a, b, via=None): + """ + 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) + """ + if not self.authenticated: + await self.login() + + m = await self.get_model() + try: + m.add_relation(a, b, via) + finally: + await m.disconnect() + + async def apply_config(self, config, application): + """Apply a configuration to the application.""" + self.log.debug("JujuApi: Applying configuration to {}.".format( + application + )) + return await self.set_config(application=application, config=config) + + async def deploy_application(self, charm, name="", path="", specs={}): + """ + Deploy an application. + + Deploy an application to a container or a machine already provisioned + by the OSM Resource Orchestrator (requires the Juju public ssh key + installed on the new machine via cloud-init). + + :param str charm: The name of the charm + :param str name: The name of the application, if different than the charm + :param str path: The path to the charm + :param dict machine: A dictionary identifying the machine to manage via Juju + Examples:: + + deploy_application(..., specs={'host': '10.0.0.4', 'user': 'ubuntu'}) + """ + if not self.authenticated: + await self.login() + + # Check that the charm is valid and exists. + if charm is None: + return None + + app = await self.get_application(name) + if app is None: + + # Check for specific machine placement + to = None + if all(k in specs for k in ['hostname', 'username']): + machine = await self.model.add_machine(spec='ssh:%@%'.format( + specs['host'], + specs['user'], + )) + to = machine.id + + # TODO: Handle the error if the charm isn't found. + self.log.debug("JujuApi: Deploying charm {} ({}) from {}".format( + charm, + name, + path, + to=to, + )) + app = await self.model.deploy( + path, + application_name=name, + series='xenial', + ) + return app + deploy_service = deploy_application + + async def get_action_status(self, uuid): + """Get the status of an action.""" + if not self.authenticated: + await self.login() + + self.log.debug("JujuApi: Waiting for status of action uuid {}".format(uuid)) + action = await self.model.wait_for_action(uuid) + return action.status + + async def get_application(self, application): + """Get the deployed application.""" + if not self.authenticated: + await self.login() + + self.log.debug("JujuApi: Getting application {}".format(application)) + app = None + if application and self.model: + if self.model.applications: + if application in self.model.applications: + app = self.model.applications[application] + return app + + async def get_application_status(self, application): + """Get the status of an application.""" + if not self.authenticated: + await self.login() + + status = None + app = await self.get_application(application) + if app: + status = app.status + self.log.debug("JujuApi: Status of application {} is {}".format( + application, + str(status), + )) + return status + get_service_status = get_application_status + + async def get_config(self, application): + """Get the configuration of an application.""" + if not self.authenticated: + await self.login() + + config = None + app = await self.get_application(application) + if app: + config = await app.get_config() + + self.log.debug("JujuApi: Config of application {} is {}".format( + application, + str(config), + )) + + return config + + async def get_model(self, name='default'): + """Get a model from the Juju Controller. + + Note: Model objects returned must call disconnected() before it goes + out of scope.""" + if not self.authenticated: + await self.login() + + model = Model() + + uuid = await self.get_model_uuid(name) + + self.log.debug("JujuApi: Connecting to model {} ({})".format( + model, + uuid, + )) + + await model.connect( + self.endpoint, + uuid, + self.user, + self.secret, + None, + ) + + return model + + async def get_model_uuid(self, name='default'): + """Get the UUID of a model. + + Iterate through all models in a controller and find the matching model. + """ + if not self.authenticated: + await self.login() + + uuid = None + + models = await self.controller.get_models() + + self.log.debug("JujuApi: Looking through {} models for model {}".format( + len(models.user_models), + name, + )) + for model in models.user_models: + if model.model.name == name: + uuid = model.model.uuid + break + + return uuid + + async def get_status(self): + """Get the model status.""" + if not self.authenticated: + await self.login() + + if not self.model: + self.model = self.get_model(self.model_name) + + class model_state: + applications = {} + machines = {} + relations = {} + + self.log.debug("JujuApi: Getting model status") + status = model_state() + status.applications = self.model.applications + status.machines = self.model.machines + + return status + + async def is_application_active(self, application): + """Check if the application is in an active state.""" + if not self.authenticated: + await self.login() + + state = False + status = await self.get_application_status(application) + if status and status in ['active']: + state = True + + self.log.debug("JujuApi: Application {} is {} active".format( + application, + "" if status else "not", + )) + + return state + is_service_active = is_application_active + + async def is_application_blocked(self, application): + """Check if the application is in a blocked state.""" + if not self.authenticated: + await self.login() + + state = False + status = await self.get_application_status(application) + if status and status in ['blocked']: + state = True + + self.log.debug("JujuApi: Application {} is {} blocked".format( + application, + "" if status else "not", + )) + + return state + is_service_blocked = is_application_blocked + + async def is_application_deployed(self, application): + """Check if the application is in a deployed state.""" + if not self.authenticated: + await self.login() + + state = False + status = await self.get_application_status(application) + if status and status in ['active']: + state = True + self.log.debug("JujuApi: Application {} is {} deployed".format( + application, + "" if status else "not", + )) + + return state + is_service_deployed = is_application_deployed + + async def is_application_error(self, application): + """Check if the application is in an error state.""" + if not self.authenticated: + await self.login() + + state = False + status = await self.get_application_status(application) + if status and status in ['error']: + state = True + self.log.debug("JujuApi: Application {} is {} errored".format( + application, + "" if status else "not", + )) + + return state + is_service_error = is_application_error + + async def is_application_maint(self, application): + """Check if the application is in a maintenance state.""" + if not self.authenticated: + await self.login() + + state = False + status = await self.get_application_status(application) + if status and status in ['maintenance']: + state = True + self.log.debug("JujuApi: Application {} is {} in maintenence".format( + application, + "" if status else "not", + )) + + return state + is_service_maint = is_application_maint + + async def is_application_up(self, application=None): + """Check if the application is up.""" + if not self.authenticated: + await self.login() + state = False + + status = await self.get_application_status(application) + if status and status in ['active', 'blocked']: + state = True + self.log.debug("JujuApi: Application {} is {} up".format( + application, + "" if status else "not", + )) + return state + is_service_up = is_application_up + + async def login(self): + """Login to the Juju controller.""" + if self.authenticated: + return + cacert = None + self.controller = Controller() + + self.log.debug("JujuApi: Logging into controller") + + if self.secret: + await self.controller.connect( + self.endpoint, + self.user, + self.secret, + cacert, + ) + else: + await self.controller.connect_current() + + self.authenticated = True + self.model = await self.get_model(self.model_name) + + async def logout(self): + """Logout of the Juju controller.""" + if not self.authenticated: + return + + if self.model: + await self.model.disconnect() + self.model = None + if self.controller: + await self.controller.disconnect() + self.controller = None + + self.authenticated = False + + async def remove_application(self, name): + """Remove the application.""" + if not self.authenticated: + await self.login() + + app = await self.get_application(name) + if app: + self.log.debug("JujuApi: Destroying application {}".format( + name, + )) + + await app.destroy() + + async def remove_relation(self, a, b): + """ + Remove a relation between two application endpoints + + :param a An application endpoint + :param b An application endpoint + """ + if not self.authenticated: + await self.login() + + m = await self.get_model() + try: + m.remove_relation(a, b) + finally: + await m.disconnect() + + async def resolve_error(self, application=None): + """Resolve units in error state.""" + if not self.authenticated: + await self.login() + + app = await self.get_application(application) + if app: + self.log.debug("JujuApi: Resolving errors for application {}".format( + application, + )) + + for unit in app.units: + app.resolved(retry=True) + + async def run_action(self, application, action_name, **params): + """Execute an action and return an Action object.""" + if not self.authenticated: + await self.login() + result = { + 'status': '', + 'action': { + 'tag': None, + 'results': None, + } + } + app = await self.get_application(application) + if app: + # We currently only have one unit per application + # so use the first unit available. + unit = app.units[0] + + self.log.debug("JujuApi: Running Action {} against Application {}".format( + action_name, + application, + )) + + action = await unit.run_action(action_name, **params) + + # Wait for the action to complete + await action.wait() + + result['status'] = action.status + result['action']['tag'] = action.data['id'] + result['action']['results'] = action.results + + return result + execute_action = run_action + + async def set_config(self, application, config): + """Apply a configuration to the application.""" + if not self.authenticated: + await self.login() + + app = await self.get_application(application) + if app: + self.log.debug("JujuApi: Setting config for Application {}".format( + application, + )) + await app.set_config(config) + + # Verify the config is set + newconf = await app.get_config() + for key in config: + if config[key] != newconf[key]: + self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key])) + + + async def set_parameter(self, parameter, value, application=None): + """Set a config parameter for a service.""" + if not self.authenticated: + await self.login() + + self.log.debug("JujuApi: Setting {}={} for Application {}".format( + parameter, + value, + application, + )) + return await self.apply_config( + {parameter: value}, + application=application, + ) + + async def wait_for_application(self, name, timeout=300): + """Wait for an application to become active.""" + if not self.authenticated: + await self.login() + + app = await self.get_application(name) + if app: + self.log.debug("JujuApi: Waiting {} seconds for Application {}".format( + timeout, + name, + )) + + await self.model.block_until( + lambda: all( + unit.agent_status == 'idle' + and unit.workload_status + in ['active', 'unknown'] for unit in app.units + ), + timeout=timeout, + ) + + +def get_argparser(): + parser = argparse.ArgumentParser(description='Test Driver for Juju API') + + ################### + # Authentication # + ################### + parser.add_argument( + "-s", "--server", + default='10.0.202.49', + help="Juju controller", + ) + parser.add_argument( + "-u", "--user", + default='admin', + help="User, default user-admin" + ) + parser.add_argument( + "-p", "--password", + default='', + help="Password for the user" + ) + parser.add_argument( + "-P", "--port", + default=17070, + help="Port number, default 17070" + ) + parser.add_argument( + "-m", "--model", + default='default', + help="The model to connect to." + ) + + ########## + # Charm # + ########## + parser.add_argument( + "-d", "--directory", + help="Local directory for the charm" + ) + parser.add_argument( + "--application", + help="Charm name" + ) + + ############# + # Placement # + ############# + + """ + To deploy to a non-Juju machine, provide the host and + credentials for Juju to manually provision (host, username, (password or key?)) + + """ + parser.add_argument( + "--proxy", + action='store_true', + help="Deploy as a proxy charm.", + ) + parser.add_argument( + "--no-proxy", + action='store_false', + dest='proxy', + help="Deploy as a full charm.", + ) + parser.set_defaults(proxy=True) + + # Test options? + # unit test? + ####### + # VNF # + ####### + + return parser.parse_args() + + +async def deploy_charm_and_wait(): + args = get_argparser() + + # Set logging level to debug so we can see verbose output from the + # juju library. + logging.basicConfig(level=logging.DEBUG) + + # Quiet logging from the websocket library. If you want to see + # everything sent over the wire, set this to DEBUG. + ws_logger = logging.getLogger('websockets.protocol') + ws_logger.setLevel(logging.INFO) + + """Here's an example of a coroutine that will deploy a charm and wait until + it's ready to be used.""" + api = JujuApi(server=args.server, + port=args.port, + user=args.user, + secret=args.password, + # loop=loop, + log=ws_logger, + model_name=args.model + ) + print("Logging in...") + await api.login() + + if api.authenticated: + status = await api.get_status() + print('Applications:', list(status.applications.keys())) + print('Machines:', list(status.machines.keys())) + + if args.directory and args.application: + + + + # Deploy the charm + charm = os.path.basename( + os.path.expanduser( + os.path.dirname(args.directory) + ) + ) + await api.deploy_application(charm, + name=args.application, + path=args.directory, + ) + + # Wait for the application to fully deploy. This will block until the + # agent is in an idle state, and the charm's workload is either + # 'active' or 'unknown', meaning it's ready but the author did not + # explicitly set a workload state. + print("Waiting for application '{}' to deploy...".format(charm)) + while (True): + # Deploy the charm and wait, periodically checking its status + await api.wait_for_application(charm, 30) + + error = await api.is_application_error(charm) + if error: + print("This application is in an error state.") + break + + blocked = await api.is_application_blocked(charm) + if blocked: + print("This application is blocked.") + break + + # An extra check to see if the charm is ready + up = await api.is_application_up(charm) + print("Application is {}".format("up" if up else "down")) + + print("Service {} is deployed".format(args.application)) + + ################################### + # Execute config on a proxy charm # + ################################### + config = await api.get_config(args.application) + hostname = config['ssh-username']['value'] + rhostname = hostname[::-1] + + # Apply the configuration + await api.apply_config( + {'ssh-username': rhostname}, application=args.application + ) + + # Get the configuration + config = await api.get_config(args.application) + + # Verify the configuration has been updated + assert(config['ssh-username']['value'] == rhostname) + + #################################### + # Get the status of an application # + #################################### + status = await api.get_application_status(charm) + print("Application Status: {}".format(status)) + + ########################### + # Execute a simple action # + ########################### + result = await api.run_action(charm, 'get-ssh-public-key') + print("Action {} status is {} and returned {}".format( + result['status'], + result['action']['tag'], + result['action']['results'] + )) + + ##################################### + # Execute an action with parameters # + ##################################### + result = await api.run_action(charm, 'run', command='hostname') + + print("Action {} status is {} and returned {}".format( + result['status'], + result['action']['tag'], + result['action']['results'] + )) + + print("Logging out...") + await api.logout() + api = None + +# get public key in juju controller? that can be pulled without need of a charm deployed and installed to vm via cloud-init + +if __name__ == "__main__": + # Create a single event loop for running code asyncronously. + loop = asyncio.get_event_loop() + + # An initial set of tasks to run + tasks = [ + deploy_charm_and_wait(), + ] + + # TODO: optionally run forever and use a Watcher to monitor what's happening + loop.run_until_complete(asyncio.wait(tasks)) diff --git a/lcm/osm_lcm/lcm.py b/lcm/osm_lcm/lcm.py index 06393e5f..9bff8c19 100644 --- a/lcm/osm_lcm/lcm.py +++ b/lcm/osm_lcm/lcm.py @@ -14,6 +14,7 @@ from fsbase import FsException from msgbase import MsgException from os import environ import logging +from vca import DeployApplication, RemoveApplication #streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s" streamformat = "%(name)s %(levelname)s: %(message)s" @@ -39,7 +40,6 @@ class Lcm: # load configuration config = self.read_config_file(config_file) self.config = config - self.config = config self.ro_url = "http://{}:{}/openmano".format(config["RO"]["host"], config["RO"]["port"]) self.ro_tenant = config["RO"]["tenant"] self.vca = config["VCA"] # TODO VCA @@ -185,14 +185,27 @@ class Lcm: if vnfd.get("vnf-configuration") and vnfd["vnf-configuration"].get("juju"): db_nsr["config-status"] = "configuring" proxy_charm = vnfd["vnf-configuration"]["juju"]["charm"] - config_primitive = vnfd["vnf-configuration"].get("config-primitive") - # get parameters for juju charm + + # Note: The charm needs to exist on disk at the location + # specified by charm_path. base_folder = vnfd["_admin"]["storage"] - path = "{}{}/{}/charms".format(base_folder["path"], base_folder["folder"], base_folder["file"], - proxy_charm) - mgmt_ip = nsr_lcm['nsr_ip'][vnfd_index] - # TODO launch VCA charm - # task = asyncio.ensure_future(DeployCharm(self.loop, path, mgmt_ip, config_primitive)) + charm_path = "{}{}/{}/charms/{}".format( + base_folder["path"], + base_folder["folder"], + base_folder["file"], + proxy_charm + ) + task = asyncio.ensure_future( + DeployApplication( + self.config['VCA'], + self.db, + db_nsr, + vnfd, + vnfd_index, + charm_path, + ) + ) + db_nsr["detailed-status"] = "Done" db_nsr["operational-status"] = "running" self.db.replace("nsrs", nsr_id, db_nsr) @@ -217,9 +230,28 @@ class Lcm: db_nsr["config-status"] = "terminate" db_nsr["detailed-status"] = "Deleting charms" self.db.replace("nsrs", nsr_id, db_nsr) - # TODO destroy VCA charm + try: + self.logger.debug("Deleting charms") + nsd = db_nsr["nsd"] + for c_vnf in nsd["constituent-vnfd"]: + vnfd_id = c_vnf["vnfd-id-ref"] + vnfd_index = str(c_vnf["member-vnf-index"]) + vnfd = self.db.get_one("vnfds", {"id": vnfd_id}) + if vnfd.get("vnf-configuration") and vnfd["vnf-configuration"].get("juju"): + asyncio.ensure_future( + RemoveApplication( + self.config['VCA'], + self.db, + db_nsr, + vnfd, + vnfd_index, + ) + ) + except Exception as e: + self.logger.debug("Failed while deleting charms: {}".format(e)) # remove from RO + RO = ROclient.ROClient(self.loop, endpoint_url=self.ro_url, tenant=self.ro_tenant, datacenter=db_nsr["datacenter"]) # Delete ns @@ -283,7 +315,6 @@ class Lcm: nsr_id, RO_vnfd_id, e)) self.db.replace("nsrs", nsr_id, db_nsr) - # TODO delete from database or mark as deleted??? db_nsr["operational-status"] = "terminated" self.db.del_one("nsrs", {"_id": nsr_id}) @@ -389,7 +420,6 @@ class Lcm: self.logger.critical("At config file '{}': {}".format(config_file, e)) - if __name__ == '__main__': config_file = "lcm.cfg" @@ -433,6 +463,3 @@ if __name__ == '__main__': # lcm.db.create("ns_request", ns_request) lcm.start() - - - diff --git a/lcm/osm_lcm/vca.py b/lcm/osm_lcm/vca.py new file mode 100644 index 00000000..b2b26633 --- /dev/null +++ b/lcm/osm_lcm/vca.py @@ -0,0 +1,233 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +from juju_api import JujuApi +from juju.model import ModelObserver +import logging +import os +import os.path +import re + + +class VCAMonitor(ModelObserver): + """Monitor state changes within the Juju Model.""" + context = None + + async def on_change(self, delta, old, new, model): + """React to changes in the Juju model.""" + status = None + db_nsr = self.context['db_nsr'] + vnf_id = self.context['vnf_id'] + + nsr_lcm = db_nsr["_admin"]["deploy"] + nsr_id = nsr_lcm["id"] + application = self.context['application'] + + if delta.entity == "unit": + # We only care about changes to a unit + if delta.type == "add" and old is None: + if new and new.application == application: + status = "BUILD" + elif delta.type == "change": + if new and new.application == application: + if new.agent_status == "idle": + if new.workload_status in ("active", "blocked"): + status = "ACTIVE" + + elif delta.type == "remove" and new is None: + if new and new.application == application: + status = "DELETING" + + if status: + nsr_lcm["VCA"][vnf_id]['operational-status'] = status + + # TODO: Clean this up, and make it work with deletes (if we need + # TODO: to update the database post-delete) + # Figure out if we're finished configuring + count = len(nsr_lcm["VCA"]) + active = 0 + for vnf_id in nsr_lcm["VCA"]: + if nsr_lcm["VCA"][vnf_id]['operational-status'] == "ACTIVE": + active += 1 + if active == count: + db_nsr["config-status"] = "done" + else: + db_nsr["config-status"] = "configuring {}/{}".format(active, count) + + try: + self.context['db'].replace( + "nsrs", + nsr_id, + db_nsr + ) + + # self.context['db'].replace( + # "nsr_lcm", + # {"id": self.context['nsr_lcm']['id']}, + # self.context['nsr_lcm'] + # ) + except Exception as e: + # I've seen this happen when we handle a delete, because the + # db record is gone by the time we've finished deleting + # the charms. + print("Error updating database: ", e) + + pass + + +def GetJujuApi(config): + # Quiet logging from the websocket library. If you want to see + # everything sent over the wire, set this to DEBUG. + logging.basicConfig(level=logging.DEBUG) + + ws_logger = logging.getLogger('websockets.protocol') + ws_logger.setLevel(logging.INFO) + + api = JujuApi(server=config['host'], + port=config['port'], + user=config['user'], + secret=config['secret'], + log=ws_logger, + model_name='default' + ) + return api + + +def get_vnf_unique_name(nsr_name, vnfr_name, member_vnf_index): + """Get the unique VNF name. + Charm names accepts only a to z and non-consecutive - characters.""" + name = "{}-{}-{}".format(nsr_name, vnfr_name, member_vnf_index) + new_name = '' + for c in name: + if c.isdigit(): + c = chr(97 + int(c)) + elif not c.isalpha(): + c = "-" + new_name += c + return re.sub('\-+', '-', new_name.lower()) + + +def get_initial_config(initial_config_primitive, mgmt_ip): + config = {} + for primitive in initial_config_primitive: + if primitive['name'] == 'config': + for parameter in primitive['parameter']: + param = parameter['name'] + if parameter['value'] == "": + config[param] = mgmt_ip + else: + config[param] = parameter['value'] + return config + + +async def DeployApplication(vcaconfig, db, db_nsr, vnfd, + vnfd_index, charm_path): + """ + Deploy a charm. + + Deploy a VNF configuration charm from a local directory. + :param dict vcaconfig: The VCA portion of the LCM Configuration + :param object vnfd: The VNF descriptor + ... + :param int vnfd_index: The index of the vnf. + + :Example: + + DeployApplication(...) + """ + nsr_lcm = db_nsr["_admin"]["deploy"] + nsr_id = nsr_lcm["id"] + vnf_id = vnfd['id'] + + if "proxy" in vnfd["vnf-configuration"]["juju"]: + use_proxy = vnfd["vnf-configuration"]["juju"]["proxy"] + else: + # TBD: We need this to handle a full charm + use_proxy = True + + application = get_vnf_unique_name( + db_nsr["name"].lower().strip(), + vnfd['id'], + vnfd_index, + ) + + api = GetJujuApi(vcaconfig) + + await api.login() + if api.authenticated: + charm = os.path.basename(charm_path) + + # Set the INIT state; further operational status updates + # will be made by the VCAMonitor + nsr_lcm["VCA"][vnf_id] = {} + nsr_lcm["VCA"][vnf_id]['operational-status'] = 'INIT' + nsr_lcm["VCA"][vnf_id]['application'] = application + + db.replace("nsrs", nsr_id, db_nsr) + + model = await api.get_model() + context = { + 'application': application, + 'vnf_id': vnf_id, + 'db_nsr': db_nsr, + 'db': db, + } + mon = VCAMonitor() + mon.context = context + model.add_observer(mon) + + await api.deploy_application(charm, + name=application, + path=charm_path, + ) + + # Get and apply the initial config primitive + cfg = get_initial_config( + vnfd["vnf-configuration"].get( + "initial-config-primitive" + ), + nsr_lcm['nsr_ip'][vnfd_index] + ) + + await api.apply_config(cfg, application) + + await api.logout() + + +async def RemoveApplication(vcaconfig, db, db_nsr, vnfd, vnfd_index): + """ + Remove an application from the Juju Controller + + Removed the named application and it's charm from the Juju controller. + + :param object loop: The event loop. + :param str application_name: The unique name of the application. + + :Example: + + RemoveApplication(loop, "ping_vnf") + RemoveApplication(loop, "pong_vnf") + """ + nsr_lcm = db_nsr["_admin"]["deploy"] + vnf_id = vnfd['id'] + application = nsr_lcm["VCA"][vnf_id]['application'] + + api = GetJujuApi(vcaconfig) + + await api.login() + if api.authenticated: + model = await api.get_model() + context = { + 'application': application, + 'vnf_id': vnf_id, + 'db_nsr': db_nsr, + 'db': db, + } + + mon = VCAMonitor() + mon.context = context + model.add_observer(mon) + + print("VCA: Removing application {}".format(application)) + await api.remove_application(application) + await api.logout()