X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Fn2vc_juju_conn.py;h=4456dd9dbfd448fde5a9c3136786f778214e4786;hp=2d2fbdbdd8ebf8b6afcd4714ec068a60bb0d6adf;hb=dfaa6e8f53c55ed31697981688085820a01cb43f;hpb=9ae4d929c2b739d146e3e27388dc4825ca046e50;ds=sidebyside diff --git a/n2vc/n2vc_juju_conn.py b/n2vc/n2vc_juju_conn.py index 2d2fbdb..4456dd9 100644 --- a/n2vc/n2vc_juju_conn.py +++ b/n2vc/n2vc_juju_conn.py @@ -29,6 +29,7 @@ import binascii import re from n2vc.n2vc_conn import N2VCConnector +from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml from n2vc.exceptions \ import N2VCBadArgumentsException, N2VCException, N2VCConnectionException, \ N2VCExecutionException, N2VCInvalidCertificate @@ -39,6 +40,10 @@ from juju.model import Model from juju.application import Application from juju.action import Action from juju.machine import Machine +from juju.client import client +from juju.errors import JujuAPIError + +from n2vc.provisioner import SSHProvisioner class N2VCJujuConnector(N2VCConnector): @@ -58,8 +63,7 @@ class N2VCJujuConnector(N2VCConnector): url: str = '127.0.0.1:17070', username: str = 'admin', vca_config: dict = None, - on_update_db=None, - api_proxy=None + on_update_db=None ): """Initialize juju N2VC connector """ @@ -82,7 +86,7 @@ class N2VCJujuConnector(N2VCConnector): logging.getLogger('juju.client.connection').setLevel(logging.WARN) logging.getLogger('model').setLevel(logging.WARN) - self.info('Initializing N2VC juju connector...') + self.log.info('Initializing N2VC juju connector...') """ ############################################################## @@ -139,7 +143,7 @@ class N2VCJujuConnector(N2VCConnector): cacert, ) except binascii.Error as e: - self.debug("Caught binascii.Error: {}".format(e)) + self.log.debug("Caught binascii.Error: {}".format(e)) raise N2VCInvalidCertificate(message="Invalid CA Certificate") return cacert @@ -148,12 +152,24 @@ class N2VCJujuConnector(N2VCConnector): if self.ca_cert: self.ca_cert = base64_to_cacert(vca_config['ca_cert']) - if api_proxy: - self.api_proxy = api_proxy + if 'api_proxy' in vca_config: + self.api_proxy = vca_config['api_proxy'] + self.log.debug('api_proxy for native charms configured: {}'.format(self.api_proxy)) else: self.warning('api_proxy is not configured. Support for native charms is disabled') - self.debug('Arguments have been checked') + if 'enable_os_upgrade' in vca_config: + self.enable_os_upgrade = vca_config['enable_os_upgrade'] + else: + self.enable_os_upgrade = True + + if 'apt_mirror' in vca_config: + self.apt_mirror = vca_config['apt_mirror'] + else: + self.apt_mirror = None + + self.cloud = vca_config.get('cloud') + self.log.debug('Arguments have been checked') # juju data self.controller = None # it will be filled when connect to juju @@ -166,10 +182,11 @@ class N2VCJujuConnector(N2VCConnector): # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub self._create_juju_public_key() - self.info('N2VC juju connector initialized') + self.log.info('N2VC juju connector initialized') + + async def get_status(self, namespace: str, yaml_format: bool = True): - async def get_status(self, namespace: str): - self.info('Getting NS status. namespace: {}'.format(namespace)) + # self.log.info('Getting NS status. namespace: {}'.format(namespace)) if not self._authenticated: await self._juju_login() @@ -179,7 +196,7 @@ class N2VCJujuConnector(N2VCConnector): model_name = ns_id if model_name is None: msg = 'Namespace {} not valid'.format(namespace) - self.error(msg) + self.log.error(msg) raise N2VCBadArgumentsException(msg, ['namespace']) # get juju model (create model if needed) @@ -187,7 +204,10 @@ class N2VCJujuConnector(N2VCConnector): status = await model.get_status() - return status + if yaml_format: + return obj_to_yaml(status) + else: + return obj_to_dict(status) async def create_execution_environment( self, @@ -198,7 +218,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ) -> (str, dict): - self.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace, reuse_ee_id)) + self.log.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace, reuse_ee_id)) if not self._authenticated: await self._juju_login() @@ -213,7 +233,7 @@ class N2VCJujuConnector(N2VCConnector): # application name application_name = self._get_application_name(namespace=namespace) - self.debug('model name: {}, application name: {}, machine_id: {}' + self.log.debug('model name: {}, application name: {}, machine_id: {}' .format(model_name, application_name, machine_id)) # create or reuse a new juju machine @@ -228,7 +248,7 @@ class N2VCJujuConnector(N2VCConnector): ) except Exception as e: message = 'Error creating machine on juju: {}'.format(e) - self.error(message) + self.log.error(message) raise N2VCException(message=message) # id for the execution environment @@ -237,13 +257,13 @@ class N2VCJujuConnector(N2VCConnector): application_name=application_name, machine_id=str(machine.entity_id) ) - self.debug('ee_id: {}'.format(ee_id)) + self.log.debug('ee_id: {}'.format(ee_id)) # new machine credentials credentials = dict() credentials['hostname'] = machine.dns_name - self.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id, credentials)) + self.log.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id, credentials)) return ee_id, credentials @@ -259,15 +279,15 @@ class N2VCJujuConnector(N2VCConnector): if not self._authenticated: await self._juju_login() - self.info('Registering execution environment. namespace={}, credentials={}'.format(namespace, credentials)) + self.log.info('Registering execution environment. namespace={}, credentials={}'.format(namespace, credentials)) if credentials is None: raise N2VCBadArgumentsException(message='credentials are mandatory', bad_args=['credentials']) - if 'hostname' in credentials: + if credentials.get('hostname'): hostname = credentials['hostname'] else: raise N2VCBadArgumentsException(message='hostname is mandatory', bad_args=['credentials.hostname']) - if 'username' in credentials: + if credentials.get('username'): username = credentials['username'] else: raise N2VCBadArgumentsException(message='username is mandatory', bad_args=['credentials.username']) @@ -286,7 +306,7 @@ class N2VCJujuConnector(N2VCConnector): # register machine on juju try: - machine = await self._juju_provision_machine( + machine_id = await self._juju_provision_machine( model_name=model_name, hostname=hostname, username=username, @@ -296,18 +316,19 @@ class N2VCJujuConnector(N2VCConnector): total_timeout=total_timeout ) except Exception as e: - self.error('Error registering machine: {}'.format(e)) + self.log.error('Error registering machine: {}'.format(e)) raise N2VCException(message='Error registering machine on juju: {}'.format(e)) - self.info('Machine registered') + + self.log.info('Machine registered: {}'.format(machine_id)) # id for the execution environment ee_id = N2VCJujuConnector._build_ee_id( model_name=model_name, application_name=application_name, - machine_id=str(machine.entity_id) + machine_id=str(machine_id) ) - self.info('Execution environment registered. ee_id: {}'.format(ee_id)) + self.log.info('Execution environment registered. ee_id: {}'.format(ee_id)) return ee_id @@ -317,10 +338,11 @@ class N2VCJujuConnector(N2VCConnector): artifact_path: str, db_dict: dict, progress_timeout: float = None, - total_timeout: float = None + total_timeout: float = None, + config: dict = None, ): - self.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}' + self.log.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}' .format(ee_id, artifact_path, db_dict)) if not self._authenticated: @@ -336,7 +358,7 @@ class N2VCJujuConnector(N2VCConnector): try: model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) - self.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id)) + self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id)) except Exception as e: raise N2VCBadArgumentsException( message='ee_id={} is not a valid execution environment id'.format(ee_id), @@ -365,12 +387,13 @@ class N2VCJujuConnector(N2VCConnector): machine_id=machine_id, db_dict=db_dict, progress_timeout=progress_timeout, - total_timeout=total_timeout + total_timeout=total_timeout, + config=config ) except Exception as e: raise N2VCException(message='Error desploying charm into ee={} : {}'.format(ee_id, e)) - self.info('Configuration sw installed') + self.log.info('Configuration sw installed') async def get_ee_ssh_public__key( self, @@ -380,7 +403,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ) -> str: - self.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id, db_dict)) + self.log.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id, db_dict)) if not self._authenticated: await self._juju_login() @@ -393,7 +416,7 @@ class N2VCJujuConnector(N2VCConnector): try: model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) - self.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id)) + self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id)) except Exception as e: raise N2VCBadArgumentsException( message='ee_id={} is not a valid execution environment id'.format(ee_id), @@ -417,7 +440,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout=total_timeout ) except Exception as e: - self.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e)) + self.log.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e)) # execute action: get-ssh-public-key try: @@ -431,11 +454,11 @@ class N2VCJujuConnector(N2VCConnector): ) except Exception as e: msg = 'Cannot execute action get-ssh-public-key: {}\n'.format(e) - self.info(msg) + self.log.info(msg) raise e # return public key if exists - return output + return output["pubkey"] if "pubkey" in output else output async def add_relation( self, @@ -445,28 +468,52 @@ class N2VCJujuConnector(N2VCConnector): endpoint_2: str ): - self.debug('adding new relation between {} and {}, endpoints: {}, {}' + self.log.debug('adding new relation between {} and {}, endpoints: {}, {}' .format(ee_id_1, ee_id_2, endpoint_1, endpoint_2)) + # check arguments + if not ee_id_1: + message = 'EE 1 is mandatory' + self.log.error(message) + raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1']) + if not ee_id_2: + message = 'EE 2 is mandatory' + self.log.error(message) + raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_2']) + if not endpoint_1: + message = 'endpoint 1 is mandatory' + self.log.error(message) + raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_1']) + if not endpoint_2: + message = 'endpoint 2 is mandatory' + self.log.error(message) + raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_2']) + if not self._authenticated: await self._juju_login() - # get model, application and machines + # get the model, the applications and the machines from the ee_id's model_1, app_1, machine_1 = self._get_ee_id_components(ee_id_1) model_2, app_2, machine_2 = self._get_ee_id_components(ee_id_2) # model must be the same if model_1 != model_2: message = 'EE models are not the same: {} vs {}'.format(ee_id_1, ee_id_2) - self.error(message) + self.log.error(message) raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1', 'ee_id_2']) # add juju relations between two applications try: - self._juju_add_relation() + await self._juju_add_relation( + model_name=model_1, + application_name_1=app_1, + application_name_2=app_2, + relation_1=endpoint_1, + relation_2=endpoint_2 + ) except Exception as e: message = 'Error adding relation between {} and {}'.format(ee_id_1, ee_id_2) - self.error(message) + self.log.error(message) raise N2VCException(message=message) async def remove_relation( @@ -475,7 +522,7 @@ class N2VCJujuConnector(N2VCConnector): if not self._authenticated: await self._juju_login() # TODO - self.info('Method not implemented yet') + self.log.info('Method not implemented yet') raise NotImplemented() async def deregister_execution_environments( @@ -484,7 +531,7 @@ class N2VCJujuConnector(N2VCConnector): if not self._authenticated: await self._juju_login() # TODO - self.info('Method not implemented yet') + self.log.info('Method not implemented yet') raise NotImplemented() async def delete_namespace( @@ -493,7 +540,7 @@ class N2VCJujuConnector(N2VCConnector): db_dict: dict = None, total_timeout: float = None ): - self.info('Deleting namespace={}'.format(namespace)) + self.log.info('Deleting namespace={}'.format(namespace)) if not self._authenticated: await self._juju_login() @@ -504,7 +551,6 @@ class N2VCJujuConnector(N2VCConnector): nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace) if ns_id is not None: - self.debug('Deleting model {}'.format(ns_id)) try: await self._juju_destroy_model( model_name=ns_id, @@ -515,7 +561,7 @@ class N2VCJujuConnector(N2VCConnector): else: raise N2VCBadArgumentsException(message='only ns_id is permitted to delete yet', bad_args=['namespace']) - self.info('Namespace {} deleted'.format(namespace)) + self.log.info('Namespace {} deleted'.format(namespace)) async def delete_execution_environment( self, @@ -523,7 +569,7 @@ class N2VCJujuConnector(N2VCConnector): db_dict: dict = None, total_timeout: float = None ): - self.info('Deleting execution environment ee_id={}'.format(ee_id)) + self.log.info('Deleting execution environment ee_id={}'.format(ee_id)) if not self._authenticated: await self._juju_login() @@ -542,17 +588,17 @@ class N2VCJujuConnector(N2VCConnector): .format(ee_id, application_name, e)) # destroy the machine - try: - await self._juju_destroy_machine( - model_name=model_name, - machine_id=machine_id, - total_timeout=total_timeout - ) - except Exception as e: - raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}' - .format(ee_id, machine_id, e)) - - self.info('Execution environment {} deleted'.format(ee_id)) + # try: + # await self._juju_destroy_machine( + # model_name=model_name, + # machine_id=machine_id, + # total_timeout=total_timeout + # ) + # except Exception as e: + # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}' + # .format(ee_id, machine_id, e)) + + self.log.info('Execution environment {} deleted'.format(ee_id)) async def exec_primitive( self, @@ -564,7 +610,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ) -> str: - self.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name, ee_id, params_dict)) + self.log.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name, ee_id, params_dict)) if not self._authenticated: await self._juju_login() @@ -597,7 +643,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout=total_timeout ) except Exception as e: - self.error('Error configuring juju application: {}'.format(e)) + self.log.error('Error configuring juju application: {}'.format(e)) raise N2VCExecutionException( message='Error configuring application into ee={} : {}'.format(ee_id, e), primitive_name=primitive_name @@ -619,14 +665,14 @@ class N2VCJujuConnector(N2VCConnector): else: raise Exception('status is not completed: {}'.format(status)) except Exception as e: - self.error('Error executing primitive {}: {}'.format(primitive_name, e)) + self.log.error('Error executing primitive {}: {}'.format(primitive_name, e)) raise N2VCExecutionException( message='Error executing primitive {} into ee={} : {}'.format(primitive_name, ee_id, e), primitive_name=primitive_name ) async def disconnect(self): - self.info('closing juju N2VC...') + self.log.info('closing juju N2VC...') await self._juju_logout() """ @@ -649,7 +695,7 @@ class N2VCJujuConnector(N2VCConnector): if not the_path[-1] == '.': the_path = the_path + '.' update_dict = {the_path + 'ee_id': ee_id} - self.debug('Writing ee_id to database: {}'.format(the_path)) + # self.log.debug('Writing ee_id to database: {}'.format(the_path)) self.db.set_one( table=the_table, q_filter=the_filter, @@ -657,7 +703,7 @@ class N2VCJujuConnector(N2VCConnector): fail_on_empty=True ) except Exception as e: - self.error('Error writing ee_id to database: {}'.format(e)) + self.log.error('Error writing ee_id to database: {}'.format(e)) @staticmethod def _build_ee_id( @@ -702,18 +748,22 @@ class N2VCJujuConnector(N2VCConnector): :return: app-vnf--vdu--cnt- """ + # TODO: Enforce the Juju 50-character application limit + # split namespace components _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace) if vnf_id is None or len(vnf_id) == 0: vnf_id = '' else: - vnf_id = 'vnf-' + vnf_id + # Shorten the vnf_id to its last twelve characters + vnf_id = 'vnf-' + vnf_id[-12:] if vdu_id is None or len(vdu_id) == 0: vdu_id = '' else: - vdu_id = '-vdu-' + vdu_id + # Shorten the vdu_id to its last twelve characters + vdu_id = '-vdu-' + vdu_id[-12:] if vdu_count is None or len(vdu_count) == 0: vdu_count = '' @@ -734,7 +784,7 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ) -> Machine: - self.debug('creating machine in model: {}, existing machine id: {}'.format(model_name, machine_id)) + self.log.debug('creating machine in model: {}, existing machine id: {}'.format(model_name, machine_id)) # get juju model and observer (create model if needed) model = await self._juju_get_model(model_name=model_name) @@ -743,15 +793,15 @@ class N2VCJujuConnector(N2VCConnector): # find machine id in model machine = None if machine_id is not None: - self.debug('Finding existing machine id {} in model'.format(machine_id)) + self.log.debug('Finding existing machine id {} in model'.format(machine_id)) # get juju existing machines in the model existing_machines = await model.get_machines() if machine_id in existing_machines: - self.debug('Machine id {} found in model (reusing it)'.format(machine_id)) + self.log.debug('Machine id {} found in model (reusing it)'.format(machine_id)) machine = model.machines[machine_id] if machine is None: - self.debug('Creating a new machine in juju...') + self.log.debug('Creating a new machine in juju...') # machine does not exist, create it and wait for it machine = await model.add_machine( spec=None, @@ -785,7 +835,7 @@ class N2VCJujuConnector(N2VCConnector): else: - self.debug('Reusing old machine pending') + self.log.debug('Reusing old machine pending') # register machine with observer observer.register_machine(machine=machine, db_dict=db_dict) @@ -796,7 +846,7 @@ class N2VCJujuConnector(N2VCConnector): progress_timeout=progress_timeout, total_timeout=total_timeout) - self.debug("Machine ready at " + str(machine.dns_name)) + self.log.debug("Machine ready at " + str(machine.dns_name)) return machine async def _juju_provision_machine( @@ -808,9 +858,14 @@ class N2VCJujuConnector(N2VCConnector): db_dict: dict = None, progress_timeout: float = None, total_timeout: float = None - ) -> Machine: + ) -> str: - self.debug('provisioning machine. model: {}, hostname: {}'.format(model_name, hostname)) + if not self.api_proxy: + msg = 'Cannot provision machine: api_proxy is not defined' + self.log.error(msg=msg) + raise N2VCException(message=msg) + + self.log.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name, hostname, username)) if not self._authenticated: await self._juju_login() @@ -819,30 +874,79 @@ class N2VCJujuConnector(N2VCConnector): model = await self._juju_get_model(model_name=model_name) observer = self.juju_observers[model_name] - spec = 'ssh:{}@{}:{}'.format(username, hostname, private_key_path) - self.debug('provisioning machine {}'.format(spec)) + # TODO check if machine is already provisioned + machine_list = await model.get_machines() + + provisioner = SSHProvisioner( + host=hostname, + user=username, + private_key_path=private_key_path, + log=self.log + ) + + params = None try: - machine = await model.add_machine(spec=spec) - except Exception as e: - import sys - import traceback - traceback.print_exc(file=sys.stdout) - print('-' * 60) - raise e + params = provisioner.provision_machine() + except Exception as ex: + msg = "Exception provisioning machine: {}".format(ex) + self.log.error(msg) + raise N2VCException(message=msg) + + params.jobs = ['JobHostUnits'] + + connection = model.connection() + + # Submit the request. + self.log.debug("Adding machine to model") + client_facade = client.ClientFacade.from_connection(connection) + results = await client_facade.AddMachines(params=[params]) + error = results.machines[0].error + if error: + msg = "Error adding machine: {}}".format(error.message) + self.log.error(msg=msg) + raise ValueError(msg) + + machine_id = results.machines[0].machine + + # Need to run this after AddMachines has been called, + # as we need the machine_id + self.log.debug("Installing Juju agent into machine {}".format(machine_id)) + asyncio.ensure_future(provisioner.install_agent( + connection=connection, + nonce=params.nonce, + machine_id=machine_id, + api=self.api_proxy, + )) + + # wait for machine in model (now, machine is not yet in model, so we must wait for it) + machine = None + for i in range(10): + machine_list = await model.get_machines() + if machine_id in machine_list: + self.log.debug('Machine {} found in model!'.format(machine_id)) + machine = model.machines.get(machine_id) + break + await asyncio.sleep(2) + + if machine is None: + msg = 'Machine {} not found in model'.format(machine_id) + self.log.error(msg=msg) + raise Exception(msg) # register machine with observer observer.register_machine(machine=machine, db_dict=db_dict) # wait for machine creation - self.debug('waiting for provision completed... {}'.format(machine.entity_id)) + self.log.debug('waiting for provision finishes... {}'.format(machine_id)) await observer.wait_for_machine( - machine=machine, + machine_id=machine_id, progress_timeout=progress_timeout, total_timeout=total_timeout ) - self.debug("Machine provisioned {}".format(machine.entity_id)) - return machine + self.log.debug("Machine provisioned {}".format(machine_id)) + + return machine_id async def _juju_deploy_charm( self, @@ -852,7 +956,8 @@ class N2VCJujuConnector(N2VCConnector): machine_id: str, db_dict: dict, progress_timeout: float = None, - total_timeout: float = None + total_timeout: float = None, + config: dict = None ) -> (Application, int): # get juju model and observer @@ -867,27 +972,30 @@ class N2VCJujuConnector(N2VCConnector): if application is None: # application does not exist, create it and wait for it - self.debug('deploying application {} to machine {}, model {}' + self.log.debug('deploying application {} to machine {}, model {}' .format(application_name, machine_id, model_name)) - self.debug('charm: {}'.format(charm_path)) + self.log.debug('charm: {}'.format(charm_path)) + series = 'xenial' + # series = None application = await model.deploy( entity_url=charm_path, application_name=application_name, channel='stable', num_units=1, - series='xenial', - to=machine_id + series=series, + to=machine_id, + config=config ) # register application with observer observer.register_application(application=application, db_dict=db_dict) - self.debug('waiting for application deployed... {}'.format(application.entity_id)) + self.log.debug('waiting for application deployed... {}'.format(application.entity_id)) retries = await observer.wait_for_application( application_id=application.entity_id, progress_timeout=progress_timeout, total_timeout=total_timeout) - self.debug('application deployed') + self.log.debug('application deployed') else: @@ -895,12 +1003,12 @@ class N2VCJujuConnector(N2VCConnector): observer.register_application(application=application, db_dict=db_dict) # application already exists, but not finalised - self.debug('application already exists, waiting for deployed...') + self.log.debug('application already exists, waiting for deployed...') retries = await observer.wait_for_application( application_id=application.entity_id, progress_timeout=progress_timeout, total_timeout=total_timeout) - self.debug('application deployed') + self.log.debug('application deployed') return application, retries @@ -921,23 +1029,24 @@ class N2VCJujuConnector(N2VCConnector): application = await self._juju_get_application(model_name=model_name, application_name=application_name) - self.debug('trying to execute action {}'.format(action_name)) - unit = application.units[0] + unit = None + for u in application.units: + if await u.is_leader_from_status(): + unit = u if unit is not None: actions = await application.get_actions() if action_name in actions: - self.debug('executing action {} with params {}'.format(action_name, kwargs)) + self.log.debug('executing action "{}" using params: {}'.format(action_name, kwargs)) action = await unit.run_action(action_name, **kwargs) # register action with observer observer.register_action(action=action, db_dict=db_dict) - self.debug(' waiting for action completed or error...') await observer.wait_for_action( action_id=action.entity_id, progress_timeout=progress_timeout, total_timeout=total_timeout) - self.debug('action completed with status: {}'.format(action.status)) + self.log.debug('action completed with status: {}'.format(action.status)) output = await model.get_action_output(action_uuid=action.entity_id) status = await model.get_action_status(uuid_or_prefix=action.entity_id) if action.entity_id in status: @@ -961,31 +1070,29 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ): - # get juju model - model = await self._juju_get_model(model_name=model_name) - # get the application application = await self._juju_get_application(model_name=model_name, application_name=application_name) - self.debug('configuring the application {} -> {}'.format(application_name, config)) + self.log.debug('configuring the application {} -> {}'.format(application_name, config)) res = await application.set_config(config) - self.debug('application {} configured. res={}'.format(application_name, res)) + self.log.debug('application {} configured. res={}'.format(application_name, res)) # Verify the config is set new_conf = await application.get_config() for key in config: value = new_conf[key]['value'] - self.debug(' {} = {}'.format(key, value)) + self.log.debug(' {} = {}'.format(key, value)) if config[key] != value: raise N2VCException( message='key {} is not configured correctly {} != {}'.format(key, config[key], new_conf[key]) ) # check if 'verify-ssh-credentials' action exists - unit = application.units[0] + # unit = application.units[0] actions = await application.get_actions() if 'verify-ssh-credentials' not in actions: msg = 'Action verify-ssh-credentials does not exist in application {}'.format(application_name) + self.log.debug(msg=msg) return False # execute verify-credentials @@ -993,7 +1100,7 @@ class N2VCJujuConnector(N2VCConnector): retry_timeout = 15.0 for i in range(num_retries): try: - self.debug('Executing action verify-ssh-credentials...') + self.log.debug('Executing action verify-ssh-credentials...') output, ok = await self._juju_execute_action( model_name=model_name, application_name=application_name, @@ -1002,13 +1109,13 @@ class N2VCJujuConnector(N2VCConnector): progress_timeout=progress_timeout, total_timeout=total_timeout ) - self.debug('Result: {}, output: {}'.format(ok, output)) + self.log.debug('Result: {}, output: {}'.format(ok, output)) return True except Exception as e: - self.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e)) + self.log.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e)) await asyncio.sleep(retry_timeout) else: - self.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries)) + self.log.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries)) return False async def _juju_get_application( @@ -1029,6 +1136,7 @@ class N2VCJujuConnector(N2VCConnector): async def _juju_get_model(self, model_name: str) -> Model: """ Get a model object from juju controller + If the model does not exits, it creates it. :param str model_name: name of the model :returns Model: model obtained from juju controller or Exception @@ -1041,7 +1149,7 @@ class N2VCJujuConnector(N2VCConnector): return self.juju_models[model_name] if self._creating_model: - self.debug('Another coroutine is creating a model. Wait...') + self.log.debug('Another coroutine is creating a model. Wait...') while self._creating_model: # another coroutine is creating a model, wait await asyncio.sleep(0.1) @@ -1056,16 +1164,24 @@ class N2VCJujuConnector(N2VCConnector): model_list = await self.controller.list_models() if model_name not in model_list: - self.info('Model {} does not exist. Creating new model...'.format(model_name)) + self.log.info('Model {} does not exist. Creating new model...'.format(model_name)) + config_dict = {'authorized-keys': self.public_key} + if self.apt_mirror: + config_dict['apt-mirror'] = self.apt_mirror + if not self.enable_os_upgrade: + config_dict['enable-os-refresh-update'] = False + config_dict['enable-os-upgrade'] = False + model = await self.controller.add_model( model_name=model_name, - config={'authorized-keys': self.public_key} + config=config_dict, + cloud_name=self.cloud, ) - self.info('New model created, name={}'.format(model_name)) + self.log.info('New model created, name={}'.format(model_name)) else: - self.debug('Model already exists in juju. Getting model {}'.format(model_name)) + self.log.debug('Model already exists in juju. Getting model {}'.format(model_name)) model = await self.controller.get_model(model_name) - self.debug('Existing model in juju, name={}'.format(model_name)) + self.log.debug('Existing model in juju, name={}'.format(model_name)) self.juju_models[model_name] = model self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model) @@ -1073,7 +1189,7 @@ class N2VCJujuConnector(N2VCConnector): except Exception as e: msg = 'Cannot get model {}. Exception: {}'.format(model_name, e) - self.error(msg) + self.log.error(msg) raise N2VCException(msg) finally: self._creating_model = False @@ -1087,14 +1203,24 @@ class N2VCJujuConnector(N2VCConnector): relation_2: str ): - self.debug('adding relation') - # get juju model and observer model = await self._juju_get_model(model_name=model_name) r1 = '{}:{}'.format(application_name_1, relation_1) r2 = '{}:{}'.format(application_name_2, relation_2) - await model.add_relation(relation1=r1, relation2=r2) + + self.log.debug('adding relation: {} -> {}'.format(r1, r2)) + try: + await model.add_relation(relation1=r1, relation2=r2) + 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 + # another execption, raise it + raise e async def _juju_destroy_application( self, @@ -1102,16 +1228,18 @@ class N2VCJujuConnector(N2VCConnector): application_name: str ): - self.debug('Destroying application {} in model {}'.format(application_name, model_name)) + self.log.debug('Destroying application {} in model {}'.format(application_name, model_name)) # get juju model and observer model = await self._juju_get_model(model_name=model_name) + observer = self.juju_observers[model_name] application = model.applications.get(application_name) if application: + observer.unregister_application(application_name) await application.destroy() else: - self.debug('Application not found: {}'.format(application_name)) + self.log.debug('Application not found: {}'.format(application_name)) async def _juju_destroy_machine( self, @@ -1120,29 +1248,37 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ): - self.debug('Destroying machine {} in model {}'.format(machine_id, model_name)) + self.log.debug('Destroying machine {} in model {}'.format(machine_id, model_name)) if total_timeout is None: total_timeout = 3600 # get juju model and observer model = await self._juju_get_model(model_name=model_name) + observer = self.juju_observers[model_name] machines = await model.get_machines() if machine_id in machines: machine = model.machines[machine_id] - await machine.destroy(force=True) - # max timeout - end = time.time() + total_timeout - # wait for machine removal - machines = await model.get_machines() - while machine_id in machines and time.time() < end: - self.debug('Waiting for machine {} is destroyed'.format(machine_id)) - await asyncio.sleep(0.5) + observer.unregister_machine(machine_id) + # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396 + if "instance-id" in machine.safe_data and machine.safe_data[ + "instance-id" + ].startswith("manual:"): + self.log.debug("machine.destroy(force=True) started.") + await machine.destroy(force=True) + self.log.debug("machine.destroy(force=True) passed.") + # max timeout + end = time.time() + total_timeout + # wait for machine removal machines = await model.get_machines() - self.debug('Machine destroyed: {}'.format(machine_id)) + while machine_id in machines and time.time() < end: + self.log.debug("Waiting for machine {} is destroyed".format(machine_id)) + await asyncio.sleep(0.5) + machines = await model.get_machines() + self.log.debug("Machine destroyed: {}".format(machine_id)) else: - self.debug('Machine not found: {}'.format(machine_id)) + self.log.debug('Machine not found: {}'.format(machine_id)) async def _juju_destroy_model( self, @@ -1150,31 +1286,60 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None ): - self.debug('Destroying model {}'.format(model_name)) + self.log.debug('Destroying model {}'.format(model_name)) if total_timeout is None: total_timeout = 3600 model = await self._juju_get_model(model_name=model_name) + + if not model: + raise N2VCException( + message="Model {} does not exist".format(model_name) + ) + uuid = model.info.uuid - self.debug('disconnecting model {}...'.format(model_name)) + # destroy applications + for application_name in model.applications: + try: + await self._juju_destroy_application(model_name=model_name, application_name=application_name) + except Exception as e: + self.log.error( + "Error destroying application {} in model {}: {}".format( + application_name, + model_name, + e + ) + ) + + # destroy machines + machines = await model.get_machines() + for machine_id in machines: + try: + await self._juju_destroy_machine(model_name=model_name, machine_id=machine_id) + except Exception as e: + # ignore exceptions destroying machine + pass + await self._juju_disconnect_model(model_name=model_name) - self.juju_models[model_name] = None - self.juju_observers[model_name] = None - self.debug('destroying model {}...'.format(model_name)) + self.log.debug('destroying model {}...'.format(model_name)) await self.controller.destroy_model(uuid) + self.log.debug('model destroy requested {}'.format(model_name)) # wait for model is completely destroyed end = time.time() + total_timeout while time.time() < end: - self.debug('waiting for model is destroyed...') + self.log.debug('Waiting for model is destroyed...') try: - await self.controller.get_model(uuid) - except Exception: - self.debug('model destroyed') - return + # await self.controller.get_model(uuid) + models = await self.controller.list_models() + if model_name not in models: + self.log.debug('The model {} ({}) was destroyed'.format(model_name, uuid)) + return + except Exception as e: + pass await asyncio.sleep(1.0) async def _juju_login(self): @@ -1197,7 +1362,7 @@ class N2VCJujuConnector(N2VCConnector): try: self._connecting = True - self.info( + self.log.info( 'connecting to juju controller: {} {}:{} ca_cert: {}' .format(self.url, self.username, self.secret, '\n'+self.ca_cert if self.ca_cert else 'None')) @@ -1211,10 +1376,10 @@ class N2VCJujuConnector(N2VCConnector): cacert=self.ca_cert ) self._authenticated = True - self.info('juju controller connected') + self.log.info('juju controller connected') except Exception as e: message = 'Exception connecting to juju: {}'.format(e) - self.error(message) + self.log.error(message) raise N2VCConnectionException( message=message, url=self.url @@ -1232,10 +1397,10 @@ class N2VCJujuConnector(N2VCConnector): try: await self._juju_disconnect_model(model_name) except Exception as e: - self.error('Error disconnecting model {} : {}'.format(model_name, e)) + self.log.error('Error disconnecting model {} : {}'.format(model_name, e)) # continue with next model... - self.info("Disconnecting controller") + self.log.info("Disconnecting controller") try: await self.controller.disconnect() except Exception as e: @@ -1243,17 +1408,19 @@ class N2VCJujuConnector(N2VCConnector): self.controller = None self._authenticated = False - self.info('disconnected') + self.log.info('disconnected') async def _juju_disconnect_model( self, model_name: str ): - self.debug("Disconnecting model {}".format(model_name)) + self.log.debug("Disconnecting model {}".format(model_name)) if model_name in self.juju_models: await self.juju_models[model_name].disconnect() self.juju_models[model_name] = None self.juju_observers[model_name] = None + else: + self.warning('Cannot disconnect model: {}'.format(model_name)) def _create_juju_public_key(self): """Recreate the Juju public key on lcm container, if needed @@ -1273,15 +1440,15 @@ class N2VCJujuConnector(N2VCConnector): pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser('~')) file_path = "{}/juju_id_rsa.pub".format(pk_path) - self.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path, self.public_key)) + self.log.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path, self.public_key)) if not os.path.exists(pk_path): # create path and write file os.makedirs(pk_path) with open(file_path, 'w') as f: - self.debug('Creating juju public key file: {}'.format(file_path)) + self.log.debug('Creating juju public key file: {}'.format(file_path)) f.write(self.public_key) else: - self.debug('juju public key file already exists: {}'.format(file_path)) + self.log.debug('juju public key file already exists: {}'.format(file_path)) @staticmethod def _format_model_name(name: str) -> str: