Bug 1000: Fix authentication when deleting service
[osm/N2VC.git] / n2vc / n2vc_juju_conn.py
index 2d2fbdb..e0f1824 100644 (file)
@@ -29,6 +29,7 @@ import binascii
 import re
 
 from n2vc.n2vc_conn import N2VCConnector
 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
 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.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):
 
 
 class N2VCJujuConnector(N2VCConnector):
@@ -58,8 +63,7 @@ class N2VCJujuConnector(N2VCConnector):
         url: str = '127.0.0.1:17070',
         username: str = 'admin',
         vca_config: dict = None,
         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
         """
     ):
         """Initialize juju N2VC connector
         """
@@ -148,11 +152,22 @@ class N2VCJujuConnector(N2VCConnector):
         if self.ca_cert:
             self.ca_cert = base64_to_cacert(vca_config['ca_cert'])
 
         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.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')
 
         else:
             self.warning('api_proxy is not configured. Support for native charms is disabled')
 
+        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.debug('Arguments have been checked')
 
         # juju data
         self.debug('Arguments have been checked')
 
         # juju data
@@ -168,8 +183,9 @@ class N2VCJujuConnector(N2VCConnector):
 
         self.info('N2VC juju connector initialized')
 
 
         self.info('N2VC juju connector initialized')
 
-    async def get_status(self, namespace: str):
-        self.info('Getting NS status. namespace: {}'.format(namespace))
+    async def get_status(self, namespace: str, yaml_format: bool = True):
+
+        # self.info('Getting NS status. namespace: {}'.format(namespace))
 
         if not self._authenticated:
             await self._juju_login()
 
         if not self._authenticated:
             await self._juju_login()
@@ -187,7 +203,10 @@ class N2VCJujuConnector(N2VCConnector):
 
         status = await model.get_status()
 
 
         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,
 
     async def create_execution_environment(
         self,
@@ -263,11 +282,11 @@ class N2VCJujuConnector(N2VCConnector):
 
         if credentials is None:
             raise N2VCBadArgumentsException(message='credentials are mandatory', bad_args=['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'])
             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'])
             username = credentials['username']
         else:
             raise N2VCBadArgumentsException(message='username is mandatory', bad_args=['credentials.username'])
@@ -286,7 +305,7 @@ class N2VCJujuConnector(N2VCConnector):
 
         # register machine on juju
         try:
 
         # 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,
                 model_name=model_name,
                 hostname=hostname,
                 username=username,
@@ -298,13 +317,14 @@ class N2VCJujuConnector(N2VCConnector):
         except Exception as e:
             self.error('Error registering machine: {}'.format(e))
             raise N2VCException(message='Error registering machine on juju: {}'.format(e))
         except Exception as e:
             self.error('Error registering machine: {}'.format(e))
             raise N2VCException(message='Error registering machine on juju: {}'.format(e))
-        self.info('Machine registered')
+
+        self.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,
 
         # 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.info('Execution environment registered. ee_id: {}'.format(ee_id))
@@ -435,7 +455,7 @@ class N2VCJujuConnector(N2VCConnector):
             raise e
 
         # return public key if exists
             raise e
 
         # return public key if exists
-        return output
+        return output["pubkey"] if "pubkey" in output else output
 
     async def add_relation(
         self,
 
     async def add_relation(
         self,
@@ -448,10 +468,28 @@ class N2VCJujuConnector(N2VCConnector):
         self.debug('adding new relation between {} and {}, endpoints: {}, {}'
                    .format(ee_id_1, ee_id_2, endpoint_1, endpoint_2))
 
         self.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.error(message)
+            raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1'])
+        if not ee_id_2:
+            message = 'EE 2 is mandatory'
+            self.error(message)
+            raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_2'])
+        if not endpoint_1:
+            message = 'endpoint 1 is mandatory'
+            self.error(message)
+            raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_1'])
+        if not endpoint_2:
+            message = 'endpoint 2 is mandatory'
+            self.error(message)
+            raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_2'])
+
         if not self._authenticated:
             await self._juju_login()
 
         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_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)
 
@@ -463,7 +501,13 @@ class N2VCJujuConnector(N2VCConnector):
 
         # add juju relations between two applications
         try:
 
         # 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)
         except Exception as e:
             message = 'Error adding relation between {} and {}'.format(ee_id_1, ee_id_2)
             self.error(message)
@@ -504,7 +548,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:
 
         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,
             try:
                 await self._juju_destroy_model(
                     model_name=ns_id,
@@ -649,7 +692,7 @@ class N2VCJujuConnector(N2VCConnector):
             if not the_path[-1] == '.':
                 the_path = the_path + '.'
             update_dict = {the_path + 'ee_id': ee_id}
             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.debug('Writing ee_id to database: {}'.format(the_path))
             self.db.set_one(
                 table=the_table,
                 q_filter=the_filter,
             self.db.set_one(
                 table=the_table,
                 q_filter=the_filter,
@@ -702,18 +745,22 @@ class N2VCJujuConnector(N2VCConnector):
         :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
         """
 
         :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
         """
 
+        # 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:
         # 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:
 
         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 = ''
 
         if vdu_count is None or len(vdu_count) == 0:
             vdu_count = ''
@@ -808,9 +855,14 @@ class N2VCJujuConnector(N2VCConnector):
             db_dict: dict = None,
             progress_timeout: float = None,
             total_timeout: float = None
             db_dict: dict = None,
             progress_timeout: float = None,
             total_timeout: float = None
-    ) -> Machine:
+    ) -> str:
+
+        if not self.api_proxy:
+            msg = 'Cannot provision machine: api_proxy is not defined'
+            self.error(msg=msg)
+            raise N2VCException(message=msg)
 
 
-        self.debug('provisioning machine. model: {}, hostname: {}'.format(model_name, hostname))
+        self.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name, hostname, username))
 
         if not self._authenticated:
             await self._juju_login()
 
         if not self._authenticated:
             await self._juju_login()
@@ -819,30 +871,79 @@ class N2VCJujuConnector(N2VCConnector):
         model = await self._juju_get_model(model_name=model_name)
         observer = self.juju_observers[model_name]
 
         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:
         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.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.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.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.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.error(msg=msg)
+            raise Exception(msg)
 
         # register machine with observer
         observer.register_machine(machine=machine, db_dict=db_dict)
 
         # wait for machine creation
 
         # 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.debug('waiting for provision finishes... {}'.format(machine_id))
         await observer.wait_for_machine(
         await observer.wait_for_machine(
-            machine=machine,
+            machine_id=machine_id,
             progress_timeout=progress_timeout,
             total_timeout=total_timeout
         )
 
             progress_timeout=progress_timeout,
             total_timeout=total_timeout
         )
 
-        self.debug("Machine provisioned {}".format(machine.entity_id))
-        return machine
+        self.debug("Machine provisioned {}".format(machine_id))
+
+        return machine_id
 
     async def _juju_deploy_charm(
             self,
 
     async def _juju_deploy_charm(
             self,
@@ -870,12 +971,14 @@ class N2VCJujuConnector(N2VCConnector):
             self.debug('deploying application {} to machine {}, model {}'
                        .format(application_name, machine_id, model_name))
             self.debug('charm: {}'.format(charm_path))
             self.debug('deploying application {} to machine {}, model {}'
                        .format(application_name, machine_id, model_name))
             self.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,
             application = await model.deploy(
                 entity_url=charm_path,
                 application_name=application_name,
                 channel='stable',
                 num_units=1,
-                series='xenial',
+                series=series,
                 to=machine_id
             )
 
                 to=machine_id
             )
 
@@ -921,18 +1024,16 @@ class N2VCJujuConnector(N2VCConnector):
 
         application = await self._juju_get_application(model_name=model_name, application_name=application_name)
 
 
         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]
         if unit is not None:
             actions = await application.get_actions()
             if action_name in actions:
         unit = application.units[0]
         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.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)
 
                 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,
                 await observer.wait_for_action(
                     action_id=action.entity_id,
                     progress_timeout=progress_timeout,
@@ -961,9 +1062,6 @@ class N2VCJujuConnector(N2VCConnector):
             total_timeout: float = None
     ):
 
             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)
 
         # get the application
         application = await self._juju_get_application(model_name=model_name, application_name=application_name)
 
@@ -982,10 +1080,11 @@ class N2VCJujuConnector(N2VCConnector):
                 )
 
         # check if 'verify-ssh-credentials' action exists
                 )
 
         # 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)
         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.debug(msg=msg)
             return False
 
         # execute verify-credentials
             return False
 
         # execute verify-credentials
@@ -1029,6 +1128,7 @@ class N2VCJujuConnector(N2VCConnector):
 
     async def _juju_get_model(self, model_name: str) -> Model:
         """ Get a model object from juju controller
 
     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
 
         :param str model_name: name of the model
         :returns Model: model obtained from juju controller or Exception
@@ -1057,9 +1157,16 @@ class N2VCJujuConnector(N2VCConnector):
 
             if model_name not in model_list:
                 self.info('Model {} does not exist. Creating new model...'.format(model_name))
 
             if model_name not in model_list:
                 self.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,
                 model = await self.controller.add_model(
                     model_name=model_name,
-                    config={'authorized-keys': self.public_key}
+                    config=config_dict
                 )
                 self.info('New model created, name={}'.format(model_name))
             else:
                 )
                 self.info('New model created, name={}'.format(model_name))
             else:
@@ -1087,14 +1194,24 @@ class N2VCJujuConnector(N2VCConnector):
             relation_2: str
     ):
 
             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)
         # 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.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,
 
     async def _juju_destroy_application(
         self,
@@ -1158,23 +1275,35 @@ class N2VCJujuConnector(N2VCConnector):
         model = await self._juju_get_model(model_name=model_name)
         uuid = model.info.uuid
 
         model = await self._juju_get_model(model_name=model_name)
         uuid = model.info.uuid
 
-        self.debug('disconnecting model {}...'.format(model_name))
+        # 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))
         await self.controller.destroy_model(uuid)
         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))
         await self.controller.destroy_model(uuid)
+        self.debug('model destroy requested {}'.format(model_name))
 
         # wait for model is completely destroyed
         end = time.time() + total_timeout
         while time.time() < end:
 
         # wait for model is completely destroyed
         end = time.time() + total_timeout
         while time.time() < end:
-            self.debug('waiting for model is destroyed...')
+            self.debug('Waiting for model is destroyed...')
             try:
             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.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):
             await asyncio.sleep(1.0)
 
     async def _juju_login(self):
@@ -1254,6 +1383,8 @@ class N2VCJujuConnector(N2VCConnector):
             await self.juju_models[model_name].disconnect()
             self.juju_models[model_name] = None
             self.juju_observers[model_name] = None
             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
 
     def _create_juju_public_key(self):
         """Recreate the Juju public key on lcm container, if needed