X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=modules%2Flibjuju%2Fjuju%2Fclient%2Fconnector.py;fp=modules%2Flibjuju%2Fjuju%2Fclient%2Fconnector.py;h=64fbe4438692b8959117d4ee6f675efa697234b7;hp=0000000000000000000000000000000000000000;hb=c3e6c2ec9a1fddfc8e9bd31509b366e633b6d99e;hpb=1a15d1c84fc826fa7996c1c9d221a324edd33432 diff --git a/modules/libjuju/juju/client/connector.py b/modules/libjuju/juju/client/connector.py new file mode 100644 index 0000000..64fbe44 --- /dev/null +++ b/modules/libjuju/juju/client/connector.py @@ -0,0 +1,147 @@ +import asyncio +import logging +import copy + +import macaroonbakery.httpbakery as httpbakery +from juju.client.connection import Connection +from juju.client.jujudata import FileJujuData +from juju.errors import JujuConnectionError, JujuError + +log = logging.getLogger('connector') + + +class NoConnectionException(Exception): + '''Raised by Connector when the connection method is called + and there is no current connection.''' + pass + + +class Connector: + '''This class abstracts out a reconnectable client that can connect + to controllers and models found in the Juju data files. + ''' + def __init__( + self, + loop=None, + max_frame_size=None, + bakery_client=None, + jujudata=None, + ): + '''Initialize a connector that will use the given parameters + by default when making a new connection''' + self.max_frame_size = max_frame_size + self.loop = loop or asyncio.get_event_loop() + self.bakery_client = bakery_client + self._connection = None + self.controller_name = None + self.model_name = None + self.jujudata = jujudata or FileJujuData() + + def is_connected(self): + '''Report whether there is a currently connected controller or not''' + return self._connection is not None + + def connection(self): + '''Return the current connection; raises an exception if there + is no current connection.''' + if not self.is_connected(): + raise NoConnectionException('not connected') + return self._connection + + async def connect(self, **kwargs): + """Connect to an arbitrary Juju model. + + kwargs are passed through to Connection.connect() + """ + kwargs.setdefault('loop', self.loop) + kwargs.setdefault('max_frame_size', self.max_frame_size) + kwargs.setdefault('bakery_client', self.bakery_client) + self._connection = await Connection.connect(**kwargs) + + async def disconnect(self): + """Shut down the watcher task and close websockets. + """ + if self._connection: + log.debug('Closing model connection') + await self._connection.close() + self._connection = None + + async def connect_controller(self, controller_name=None): + """Connect to a controller by name. If the name is empty, it + connect to the current controller. + """ + if not controller_name: + controller_name = self.jujudata.current_controller() + if not controller_name: + raise JujuConnectionError('No current controller') + + controller = self.jujudata.controllers()[controller_name] + # TODO change Connection so we can pass all the endpoints + # instead of just the first. + endpoint = controller['api-endpoints'][0] + accounts = self.jujudata.accounts().get(controller_name, {}) + + await self.connect( + endpoint=endpoint, + uuid=None, + username=accounts.get('user'), + password=accounts.get('password'), + cacert=controller.get('ca-cert'), + bakery_client=self.bakery_client_for_controller(controller_name), + ) + self.controller_name = controller_name + + async def connect_model(self, model_name=None): + """Connect to a model by name. If either controller or model + parts of the name are empty, the current controller and/or model + will be used. + + :param str model: : + """ + + try: + controller_name, model_name = self.jujudata.parse_model(model_name) + controller = self.jujudata.controllers().get(controller_name) + except JujuError as e: + raise JujuConnectionError(e.message) from e + if controller is None: + raise JujuConnectionError('Controller {} not found'.format( + controller_name)) + # TODO change Connection so we can pass all the endpoints + # instead of just the first one. + endpoint = controller['api-endpoints'][0] + account = self.jujudata.accounts().get(controller_name, {}) + models = self.jujudata.models().get(controller_name, {}).get('models', + {}) + if model_name not in models: + raise JujuConnectionError('Model not found: {}'.format(model_name)) + + # TODO if there's no record for the required model name, connect + # to the controller to find out the model's uuid, then connect + # to that. This will let connect_model work with models that + # haven't necessarily synced with the local juju data, + # and also remove the need for base.CleanModel to + # subclass JujuData. + await self.connect( + endpoint=endpoint, + uuid=models[model_name]['uuid'], + username=account.get('user'), + password=account.get('password'), + cacert=controller.get('ca-cert'), + bakery_client=self.bakery_client_for_controller(controller_name), + ) + self.controller_name = controller_name + self.model_name = controller_name + ':' + model_name + + def bakery_client_for_controller(self, controller_name): + '''Make a copy of the bakery client with a the appropriate controller's + cookiejar in it. + ''' + bakery_client = self.bakery_client + if bakery_client: + bakery_client = copy.copy(bakery_client) + else: + bakery_client = httpbakery.Client() + bakery_client.cookies = self.jujudata.cookies_for_controller( + controller_name) + return bakery_client