blob: a30adbffa7811503421fff1ff81c4cf4b4b284fa [file] [log] [blame]
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