| import asyncio |
| import logging |
| import copy |
| |
| import macaroonbakery.httpbakery as httpbakery |
| from juju.client.connection import Connection |
| from juju.client.gocookies import go_to_py_cookie, GoCookieJar |
| 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) |
| if 'macaroons' in kwargs: |
| if not kwargs['bakery_client']: |
| kwargs['bakery_client'] = httpbakery.Client() |
| if not kwargs['bakery_client'].cookies: |
| kwargs['bakery_client'].cookies = GoCookieJar() |
| jar = kwargs['bakery_client'].cookies |
| for macaroon in kwargs.pop('macaroons'): |
| jar.set_cookie(go_to_py_cookie(macaroon)) |
| 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: <controller>:<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 |