X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Fn2vc_juju_conn.py;h=690d3bee57cc98ae451e7f3c414849068022792e;hp=856f79f638df62324c97b50ce96a31cf849dcb3e;hb=2c791b34626ff76ab1886a110599998f9de0df80;hpb=0a8c9afacca5010da381e6382b01e31dd4e59d23 diff --git a/n2vc/n2vc_juju_conn.py b/n2vc/n2vc_juju_conn.py index 856f79f..690d3be 100644 --- a/n2vc/n2vc_juju_conn.py +++ b/n2vc/n2vc_juju_conn.py @@ -43,19 +43,21 @@ from n2vc.exceptions import ( N2VCInvalidCertificate, N2VCNotFound, MethodNotImplemented, + JujuK8sProxycharmNotSupported, ) from n2vc.juju_observer import JujuModelObserver from n2vc.n2vc_conn import N2VCConnector from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml from n2vc.provisioner import AsyncSSHProvisioner +from n2vc.libjuju import Libjuju class N2VCJujuConnector(N2VCConnector): """ - #################################################################################### - ################################### P U B L I C #################################### - #################################################################################### +#################################################################################### +################################### P U B L I C #################################### +#################################################################################### """ BUILT_IN_CLOUDS = ["localhost", "microk8s"] @@ -164,15 +166,16 @@ class N2VCJujuConnector(N2VCConnector): if self.ca_cert: self.ca_cert = base64_to_cacert(vca_config["ca_cert"]) - if "api_proxy" in vca_config: + if "api_proxy" in vca_config and vca_config["api_proxy"] != "": 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" + "api_proxy is not configured" ) + self.api_proxy = None if "enable_os_upgrade" in vca_config: self.enable_os_upgrade = vca_config["enable_os_upgrade"] @@ -184,8 +187,11 @@ class N2VCJujuConnector(N2VCConnector): else: self.apt_mirror = None - self.cloud = vca_config.get("cloud") - # self.log.debug('Arguments have been checked') + self.cloud = vca_config.get('cloud') + self.k8s_cloud = None + if "k8s_cloud" in vca_config: + self.k8s_cloud = vca_config.get("k8s_cloud") + self.log.debug('Arguments have been checked') # juju data self.controller = None # it will be filled when connect to juju @@ -198,6 +204,19 @@ class N2VCJujuConnector(N2VCConnector): False # it will be True when juju connection be stablished ) self._creating_model = False # True during model creation + self.libjuju = Libjuju( + endpoint=self.url, + api_proxy=self.api_proxy, + enable_os_upgrade=self.enable_os_upgrade, + apt_mirror=self.apt_mirror, + username=self.username, + password=self.secret, + cacert=self.ca_cert, + loop=self.loop, + log=self.log, + db=self.db, + n2vc=self, + ) # create juju pub key file in lcm container at # ./local/share/juju/ssh/juju_id_rsa.pub @@ -209,9 +228,6 @@ class N2VCJujuConnector(N2VCConnector): # self.log.info('Getting NS status. namespace: {}'.format(namespace)) - if not self._authenticated: - await self._juju_login() - _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( namespace=namespace ) @@ -222,10 +238,11 @@ class N2VCJujuConnector(N2VCConnector): self.log.error(msg) raise N2VCBadArgumentsException(msg, ["namespace"]) - # get juju model (create model if needed) - model = await self._juju_get_model(model_name=model_name) + status = {} + models = await self.libjuju.list_models(contains=ns_id) - status = await model.get_status() + for m in models: + status[m] = await self.libjuju.get_model_status(m) if yaml_format: return obj_to_yaml(status) @@ -247,9 +264,6 @@ class N2VCJujuConnector(N2VCConnector): ) ) - if not self._authenticated: - await self._juju_login() - machine_id = None if reuse_ee_id: model_name, application_name, machine_id = self._get_ee_id_components( @@ -276,30 +290,36 @@ class N2VCJujuConnector(N2VCConnector): # create or reuse a new juju machine try: - machine = await self._juju_create_machine( + if not await self.libjuju.model_exists(model_name): + await self.libjuju.add_model(model_name, cloud_name=self.cloud) + machine, new = await self.libjuju.create_machine( model_name=model_name, - application_name=application_name, machine_id=machine_id, db_dict=db_dict, progress_timeout=progress_timeout, total_timeout=total_timeout, ) + # 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), + ) + self.log.debug("ee_id: {}".format(ee_id)) + + if new: + # write ee_id in database + self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) + except Exception as e: message = "Error creating machine on juju: {}".format(e) self.log.error(message) raise N2VCException(message=message) - # 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), - ) - self.log.debug("ee_id: {}".format(ee_id)) - # new machine credentials - credentials = dict() - credentials["hostname"] = machine.dns_name + credentials = { + "hostname": machine.dns_name, + } self.log.info( "Execution environment created. ee_id: {}, credentials: {}".format( @@ -318,9 +338,6 @@ class N2VCJujuConnector(N2VCConnector): total_timeout: float = None, ) -> str: - if not self._authenticated: - await self._juju_login() - self.log.info( "Registering execution environment. namespace={}, credentials={}".format( namespace, credentials @@ -360,7 +377,9 @@ class N2VCJujuConnector(N2VCConnector): # register machine on juju try: - machine_id = await self._juju_provision_machine( + if not await self.libjuju.model_exists(model_name): + await self.libjuju.add_model(model_name, cloud_name=self.cloud) + machine_id = await self.libjuju.provision_machine( model_name=model_name, hostname=hostname, username=username, @@ -396,6 +415,7 @@ class N2VCJujuConnector(N2VCConnector): progress_timeout: float = None, total_timeout: float = None, config: dict = None, + num_units: int = 1, ): self.log.info( @@ -405,9 +425,6 @@ class N2VCJujuConnector(N2VCConnector): ).format(ee_id, artifact_path, db_dict) ) - if not self._authenticated: - await self._juju_login() - # check arguments if ee_id is None or len(ee_id) == 0: raise N2VCBadArgumentsException( @@ -456,15 +473,16 @@ class N2VCJujuConnector(N2VCConnector): full_path = self.fs.path + "/" + artifact_path try: - await self._juju_deploy_charm( + await self.libjuju.deploy_charm( model_name=model_name, application_name=application_name, - charm_path=full_path, + path=full_path, machine_id=machine_id, db_dict=db_dict, progress_timeout=progress_timeout, total_timeout=total_timeout, config=config, + num_units=num_units, ) except Exception as e: raise N2VCException( @@ -473,6 +491,94 @@ class N2VCJujuConnector(N2VCConnector): self.log.info("Configuration sw installed") + async def install_k8s_proxy_charm( + self, + charm_name: str, + namespace: str, + artifact_path: str, + db_dict: dict, + progress_timeout: float = None, + total_timeout: float = None, + config: dict = None, + ) -> str: + """ + Install a k8s proxy charm + + :param charm_name: Name of the charm being deployed + :param namespace: collection of all the uuids related to the charm. + :param str artifact_path: where to locate the artifacts (parent folder) using + the self.fs + the final artifact path will be a combination of this artifact_path and + additional string from the config_dict (e.g. charm name) + :param dict db_dict: where to write into database when the status changes. + It contains a dict with + {collection: , filter: {}, path: }, + e.g. {collection: "nsrs", filter: + {_id: , path: "_admin.deployed.VCA.3"} + :param float progress_timeout: + :param float total_timeout: + :param config: Dictionary with additional configuration + + :returns ee_id: execution environment id. + """ + self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}' + .format(charm_name, artifact_path, db_dict)) + + if not self.k8s_cloud: + raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available") + + if artifact_path is None or len(artifact_path) == 0: + raise N2VCBadArgumentsException( + message="artifact_path is mandatory", bad_args=["artifact_path"] + ) + if db_dict is None: + raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict']) + + # remove // in charm path + while artifact_path.find('//') >= 0: + artifact_path = artifact_path.replace('//', '/') + + # check charm path + if not self.fs.file_exists(artifact_path, mode="dir"): + msg = 'artifact path does not exist: {}'.format(artifact_path) + raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path']) + + if artifact_path.startswith('/'): + full_path = self.fs.path + artifact_path + else: + full_path = self.fs.path + '/' + artifact_path + + _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace) + model_name = '{}-k8s'.format(ns_id) + + await self.libjuju.add_model(model_name, self.k8s_cloud) + application_name = self._get_application_name(namespace) + + try: + await self.libjuju.deploy_charm( + model_name=model_name, + application_name=application_name, + path=full_path, + machine_id=None, + db_dict=db_dict, + progress_timeout=progress_timeout, + total_timeout=total_timeout, + config=config + ) + except Exception as e: + raise N2VCException(message='Error deploying charm: {}'.format(e)) + + self.log.info('K8s proxy charm installed') + ee_id = N2VCJujuConnector._build_ee_id( + model_name=model_name, + application_name=application_name, + machine_id="k8s", + ) + + self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) + + return ee_id + async def get_ee_ssh_public__key( self, ee_id: str, @@ -487,9 +593,6 @@ class N2VCJujuConnector(N2VCConnector): ).format(ee_id, db_dict) ) - if not self._authenticated: - await self._juju_login() - # check arguments if ee_id is None or len(ee_id) == 0: raise N2VCBadArgumentsException( @@ -525,9 +628,11 @@ class N2VCJujuConnector(N2VCConnector): output = None + application_name = N2VCJujuConnector._format_app_name(application_name) + # execute action: generate-ssh-key try: - output, _status = await self._juju_execute_action( + output, _status = await self.libjuju.execute_action( model_name=model_name, application_name=application_name, action_name="generate-ssh-key", @@ -544,7 +649,7 @@ class N2VCJujuConnector(N2VCConnector): # execute action: get-ssh-public-key try: - output, _status = await self._juju_execute_action( + output, _status = await self.libjuju.execute_action( model_name=model_name, application_name=application_name, action_name="get-ssh-public-key", @@ -555,7 +660,7 @@ class N2VCJujuConnector(N2VCConnector): except Exception as e: msg = "Cannot execute action get-ssh-public-key: {}\n".format(e) self.log.info(msg) - raise N2VCException(msg) + raise N2VCExecutionException(e, primitive_name="get-ssh-public-key") # return public key if exists return output["pubkey"] if "pubkey" in output else output @@ -588,9 +693,6 @@ class N2VCJujuConnector(N2VCConnector): self.log.error(message) raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"]) - if not self._authenticated: - await self._juju_login() - # 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) @@ -605,7 +707,7 @@ class N2VCJujuConnector(N2VCConnector): # add juju relations between two applications try: - await self._juju_add_relation( + await self.libjuju.add_relation( model_name=model_1, application_name_1=app_1, application_name_2=app_2, @@ -620,16 +722,11 @@ class N2VCJujuConnector(N2VCConnector): raise N2VCException(message=message) async def remove_relation(self): - if not self._authenticated: - await self._juju_login() # TODO self.log.info("Method not implemented yet") raise MethodNotImplemented() async def deregister_execution_environments(self): - if not self._authenticated: - await self._juju_login() - # TODO self.log.info("Method not implemented yet") raise MethodNotImplemented() @@ -638,9 +735,6 @@ class N2VCJujuConnector(N2VCConnector): ): self.log.info("Deleting namespace={}".format(namespace)) - if not self._authenticated: - await self._juju_login() - # check arguments if namespace is None: raise N2VCBadArgumentsException( @@ -652,11 +746,11 @@ class N2VCJujuConnector(N2VCConnector): ) if ns_id is not None: try: - await self._juju_destroy_model( - model_name=ns_id, total_timeout=total_timeout - ) - except N2VCNotFound: - raise + models = await self.libjuju.list_models(contains=ns_id) + for model in models: + await self.libjuju.destroy_model( + model_name=model, total_timeout=total_timeout + ) except Exception as e: raise N2VCException( message="Error deleting namespace {} : {}".format(namespace, e) @@ -673,9 +767,6 @@ class N2VCJujuConnector(N2VCConnector): ): self.log.info("Deleting execution environment ee_id={}".format(ee_id)) - if not self._authenticated: - await self._juju_login() - # check arguments if ee_id is None: raise N2VCBadArgumentsException( @@ -688,8 +779,8 @@ class N2VCJujuConnector(N2VCConnector): # destroy the application try: - await self._juju_destroy_application( - model_name=model_name, application_name=application_name + await self.libjuju.destroy_model( + model_name=model_name, total_timeout=total_timeout ) except Exception as e: raise N2VCException( @@ -728,9 +819,6 @@ class N2VCJujuConnector(N2VCConnector): ) ) - if not self._authenticated: - await self._juju_login() - # check arguments if ee_id is None or len(ee_id) == 0: raise N2VCBadArgumentsException( @@ -760,14 +848,57 @@ class N2VCJujuConnector(N2VCConnector): if primitive_name == "config": # Special case: config primitive try: - await self._juju_configure_application( + await self.libjuju.configure_application( model_name=model_name, application_name=application_name, config=params_dict, - db_dict=db_dict, - progress_timeout=progress_timeout, - total_timeout=total_timeout, ) + actions = await self.libjuju.get_actions( + application_name=application_name, model_name=model_name, + ) + self.log.debug( + "Application {} has these actions: {}".format( + application_name, actions + ) + ) + if "verify-ssh-credentials" in actions: + # execute verify-credentials + num_retries = 20 + retry_timeout = 15.0 + for _ in range(num_retries): + try: + self.log.debug("Executing action verify-ssh-credentials...") + output, ok = await self.libjuju.execute_action( + model_name=model_name, + application_name=application_name, + action_name="verify-ssh-credentials", + db_dict=db_dict, + progress_timeout=progress_timeout, + total_timeout=total_timeout, + ) + + if ok == "failed": + self.log.debug( + "Error executing verify-ssh-credentials: {}. Retrying..." + ) + await asyncio.sleep(retry_timeout) + + continue + self.log.debug("Result: {}, output: {}".format(ok, output)) + break + except asyncio.CancelledError: + raise + else: + self.log.error( + "Error executing verify-ssh-credentials after {} retries. ".format( + num_retries + ) + ) + else: + msg = "Action verify-ssh-credentials does not exist in application {}".format( + application_name + ) + self.log.debug(msg=msg) except Exception as e: self.log.error("Error configuring juju application: {}".format(e)) raise N2VCExecutionException( @@ -779,7 +910,7 @@ class N2VCJujuConnector(N2VCConnector): return "CONFIG OK" else: try: - output, status = await self._juju_execute_action( + output, status = await self.libjuju.execute_action( model_name=model_name, application_name=application_name, action_name=primitive_name, @@ -805,12 +936,17 @@ class N2VCJujuConnector(N2VCConnector): async def disconnect(self): self.log.info("closing juju N2VC...") - await self._juju_logout() + try: + await self.libjuju.disconnect() + except Exception as e: + raise N2VCConnectionException( + message="Error disconnecting controller: {}".format(e), url=self.url + ) """ - #################################################################################### - ################################### P R I V A T E ################################## - #################################################################################### +#################################################################################### +################################### P R I V A T E ################################## +#################################################################################### """ def _write_ee_id_db(self, db_dict: dict, ee_id: str): @@ -1048,7 +1184,7 @@ class N2VCJujuConnector(N2VCConnector): connection=connection, nonce=params.nonce, machine_id=machine_id, - api=self.api_proxy, + proxy=self.api_proxy, ) ) @@ -1113,14 +1249,14 @@ class N2VCJujuConnector(N2VCConnector): ) ) self.log.debug("charm: {}".format(charm_path)) - series = "xenial" + machine = model.machines[machine_id] # series = None application = await model.deploy( entity_url=charm_path, application_name=application_name, channel="stable", num_units=1, - series=series, + series=machine.series, to=machine_id, config=config, ) @@ -1326,7 +1462,6 @@ class N2VCJujuConnector(N2VCConnector): # get juju model names from juju model_list = await self.controller.list_models() - if model_name not in model_list: self.log.info( "Model {} does not exist. Creating new model...".format(model_name)