X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;ds=inline;f=modules%2Flibjuju%2Fjuju%2Fcontroller.py;h=957ab85da834ff43e60e2993e614303ba784eca2;hb=c3e6c2ec9a1fddfc8e9bd31509b366e633b6d99e;hp=55ea55e979f80387b475eced23fd5e36d715ed5b;hpb=1a15d1c84fc826fa7996c1c9d221a324edd33432;p=osm%2FN2VC.git diff --git a/modules/libjuju/juju/controller.py b/modules/libjuju/juju/controller.py index 55ea55e..957ab85 100644 --- a/modules/libjuju/juju/controller.py +++ b/modules/libjuju/juju/controller.py @@ -1,69 +1,101 @@ import asyncio import logging -from . import errors -from . import tag -from . import utils -from .client import client -from .client import connection -from .model import Model +from . import errors, tag, utils +from .client import client, connector from .user import User log = logging.getLogger(__name__) -class Controller(object): - def __init__(self, loop=None, - max_frame_size=connection.Connection.DEFAULT_FRAME_SIZE): +class Controller: + def __init__( + self, + loop=None, + max_frame_size=None, + bakery_client=None, + jujudata=None, + ): """Instantiate a new Controller. One of the connect_* methods will need to be called before this object can be used for anything interesting. - :param loop: an asyncio event loop + If jujudata is None, jujudata.FileJujuData will be used. + :param loop: an asyncio event loop + :param max_frame_size: See + `juju.client.connection.Connection.MAX_FRAME_SIZE` + :param bakery_client httpbakery.Client: The bakery client to use + for macaroon authorization. + :param jujudata JujuData: The source for current controller information. """ - self.loop = loop or asyncio.get_event_loop() - self.max_frame_size = None - self.connection = None - self.controller_name = None + self._connector = connector.Connector( + loop=loop, + max_frame_size=max_frame_size, + bakery_client=bakery_client, + jujudata=jujudata, + ) - async def connect( - self, endpoint, username, password, cacert=None, macaroons=None): - """Connect to an arbitrary Juju controller. + async def __aenter__(self): + await self.connect() + return self - """ - self.connection = await connection.Connection.connect( - endpoint, None, username, password, cacert, macaroons, - max_frame_size=self.max_frame_size) + async def __aexit__(self, exc_type, exc, tb): + await self.disconnect() - async def connect_current(self): - """Connect to the current Juju controller. + @property + def loop(self): + return self._connector.loop - """ - jujudata = connection.JujuData() - controller_name = jujudata.current_controller() - if not controller_name: - raise errors.JujuConnectionError('No current controller') - return await self.connect_controller(controller_name) + async def connect(self, controller_name=None, **kwargs): + """Connect to a Juju controller. + + If any arguments are specified other than controller_name, + then controller_name must be None and an explicit + connection will be made using Connection.connect + using those parameters (the 'uuid' parameter must + be absent or None). - async def connect_controller(self, controller_name): - """Connect to a Juju controller by name. + Otherwise, if controller_name is None, connect to the + current controller. + Otherwise, controller_name must specify the name + of a known controller. """ - self.connection = ( - await connection.Connection.connect_controller( - controller_name, max_frame_size=self.max_frame_size)) - self.controller_name = controller_name + await self.disconnect() + if not kwargs: + await self._connector.connect_controller(controller_name) + else: + if controller_name is not None: + raise ValueError('controller name may not be specified with other connect parameters') + if kwargs.get('uuid') is not None: + # A UUID implies a model connection, not a controller connection. + raise ValueError('model UUID specified when connecting to controller') + await self._connector.connect(**kwargs) + + async def _connect_direct(self, **kwargs): + await self.disconnect() + await self._connector.connect(**kwargs) + + def is_connected(self): + """Reports whether the Controller is currently connected.""" + return self._connector.is_connected() + + def connection(self): + """Return the current Connection object. It raises an exception + if the Controller is disconnected""" + return self._connector.connection() + + @property + def controller_name(self): + return self._connector.controller_name async def disconnect(self): """Shut down the watcher task and close websockets. """ - if self.connection and self.connection.is_open: - log.debug('Closing controller connection') - await self.connection.close() - self.connection = None + await self._connector.disconnect() async def add_credential(self, name=None, credential=None, cloud=None, owner=None): @@ -84,20 +116,19 @@ class Controller(object): cloud = await self.get_cloud() if not owner: - owner = self.connection.info['user-info']['identity'] + owner = self.connection().info['user-info']['identity'] if credential and not name: raise errors.JujuError('Name must be provided for credential') if not credential: - name, credential = connection.JujuData().load_credential(cloud, - name) + name, credential = self._connector.jujudata.load_credential(cloud, name) if credential is None: - raise errors.JujuError('Unable to find credential: ' - '{}'.format(name)) + raise errors.JujuError( + 'Unable to find credential: {}'.format(name)) log.debug('Uploading credential %s', name) - cloud_facade = client.CloudFacade.from_connection(self.connection) + cloud_facade = client.CloudFacade.from_connection(self.connection()) await cloud_facade.UpdateCredentials([ client.UpdateCloudCredential( tag=tag.credential(cloud, tag.untag('user-', owner), name), @@ -121,12 +152,12 @@ class Controller(object): the current user. :param dict config: Model configuration. :param str region: Region in which to create the model. - + :return Model: A connection to the newly created model. """ model_facade = client.ModelManagerFacade.from_connection( - self.connection) + self.connection()) - owner = owner or self.connection.info['user-info']['identity'] + owner = owner or self.connection().info['user-info']['identity'] cloud_name = cloud_name or await self.get_cloud() try: @@ -153,7 +184,7 @@ class Controller(object): if not config or 'authorized-keys' not in config: config = config or {} config['authorized-keys'] = await utils.read_ssh_key( - loop=self.loop) + loop=self._connector.loop) model_info = await model_facade.CreateModel( tag.cloud(cloud_name), @@ -163,17 +194,11 @@ class Controller(object): owner, region ) - - model = Model() - await model.connect( - self.connection.endpoint, - model_info.uuid, - self.connection.username, - self.connection.password, - self.connection.cacert, - self.connection.macaroons, - loop=self.loop, - ) + from juju.model import Model + model = Model(jujudata=self._connector.jujudata) + kwargs = self.connection().connect_params() + kwargs['uuid'] = model_info.uuid + await model._connect_direct(**kwargs) return model @@ -183,12 +208,12 @@ class Controller(object): :param str \*models: Names or UUIDs of models to destroy """ - uuids = await self._model_uuids() + uuids = await self.model_uuids() models = [uuids[model] if model in uuids else model for model in models] model_facade = client.ModelManagerFacade.from_connection( - self.connection) + self.connection()) log.debug( 'Destroying model%s %s', @@ -212,7 +237,7 @@ class Controller(object): """ if not display_name: display_name = username - user_facade = client.UserManagerFacade.from_connection(self.connection) + user_facade = client.UserManagerFacade.from_connection(self.connection()) users = [client.AddUser(display_name=display_name, username=username, password=password)] @@ -223,7 +248,7 @@ class Controller(object): """Remove a user from this controller. """ client_facade = client.UserManagerFacade.from_connection( - self.connection) + self.connection()) user = tag.user(username) await client_facade.RemoveUser([client.Entity(user)]) @@ -234,7 +259,7 @@ class Controller(object): :param str password: New password """ - user_facade = client.UserManagerFacade.from_connection(self.connection) + user_facade = client.UserManagerFacade.from_connection(self.connection()) entity = client.EntityPassword(password, tag.user(username)) return await user_facade.SetPassword([entity]) @@ -246,7 +271,7 @@ class Controller(object): """ controller_facade = client.ControllerFacade.from_connection( - self.connection) + self.connection()) return await controller_facade.DestroyController(destroy_all_models) async def disable_user(self, username): @@ -255,7 +280,7 @@ class Controller(object): :param str username: Username """ - user_facade = client.UserManagerFacade.from_connection(self.connection) + user_facade = client.UserManagerFacade.from_connection(self.connection()) entity = client.Entity(tag.user(username)) return await user_facade.DisableUser([entity]) @@ -263,7 +288,7 @@ class Controller(object): """Re-enable a previously disabled user. """ - user_facade = client.UserManagerFacade.from_connection(self.connection) + user_facade = client.UserManagerFacade.from_connection(self.connection()) entity = client.Entity(tag.user(username)) return await user_facade.EnableUser([entity]) @@ -278,15 +303,33 @@ class Controller(object): """ Get the name of the cloud that this controller lives on. """ - cloud_facade = client.CloudFacade.from_connection(self.connection) + cloud_facade = client.CloudFacade.from_connection(self.connection()) result = await cloud_facade.Clouds() cloud = list(result.clouds.keys())[0] # only lives on one cloud return tag.untag('cloud-', cloud) - async def _model_uuids(self, all_=False, username=None): + async def get_models(self, all_=False, username=None): + """ + .. deprecated:: 0.7.0 + Use :meth:`.list_models` instead. + """ controller_facade = client.ControllerFacade.from_connection( self.connection) + for attempt in (1, 2, 3): + try: + return await controller_facade.AllModels() + except errors.JujuAPIError as e: + # retry concurrency error until resolved in Juju + # see: https://bugs.launchpad.net/juju/+bug/1721786 + if 'has been removed' not in e.message or attempt == 3: + raise + + async def model_uuids(self): + """Return a mapping of model names to UUIDs. + """ + controller_facade = client.ControllerFacade.from_connection( + self.connection()) for attempt in (1, 2, 3): try: response = await controller_facade.AllModels() @@ -297,17 +340,14 @@ class Controller(object): # see: https://bugs.launchpad.net/juju/+bug/1721786 if 'has been removed' not in e.message or attempt == 3: raise - await asyncio.sleep(attempt, loop=self.loop) + await asyncio.sleep(attempt, loop=self._connector.loop) - async def list_models(self, all_=False, username=None): + async def list_models(self): """Return list of names of the available models on this controller. - :param bool all_: List all models, regardless of user accessibilty - (admin use only) - :param str username: User for which to list models (admin use only) - + Equivalent to ``sorted((await self.model_uuids()).keys())`` """ - uuids = await self._model_uuids(all_, username) + uuids = await self.model_uuids() return sorted(uuids.keys()) def get_payloads(self, *patterns): @@ -347,24 +387,19 @@ class Controller(object): """Get a model by name or UUID. :param str model: Model name or UUID - + :returns Model: Connected Model instance. """ - uuids = await self._model_uuids() + uuids = await self.model_uuids() if model in uuids: - name_or_uuid = uuids[model] + uuid = uuids[model] else: - name_or_uuid = model + uuid = model + from juju.model import Model model = Model() - await model.connect( - self.connection.endpoint, - name_or_uuid, - self.connection.username, - self.connection.password, - self.connection.cacert, - self.connection.macaroons, - loop=self.loop, - ) + kwargs = self.connection().connect_params() + kwargs['uuid'] = uuid + await model._connect_direct(**kwargs) return model async def get_user(self, username): @@ -374,7 +409,7 @@ class Controller(object): :returns: A :class:`~juju.user.User` instance """ client_facade = client.UserManagerFacade.from_connection( - self.connection) + self.connection()) user = tag.user(username) args = [client.Entity(user)] try: @@ -396,32 +431,77 @@ class Controller(object): :returns: A list of :class:`~juju.user.User` instances """ client_facade = client.UserManagerFacade.from_connection( - self.connection) + self.connection()) response = await client_facade.UserInfo(None, include_disabled) return [User(self, r.result) for r in response.results] async def grant(self, username, acl='login'): - """Set access level of the given user on the controller - + """Grant access level of the given user on the controller. + Note that if the user already has higher permissions than the + provided ACL, this will do nothing (see revoke for a way to + remove permissions). :param str username: Username :param str acl: Access control ('login', 'add-model' or 'superuser') - + :returns: True if new access was granted, False if user already had + requested access or greater. Raises JujuError if failed. """ controller_facade = client.ControllerFacade.from_connection( - self.connection) + self.connection()) user = tag.user(username) - await self.revoke(username) changes = client.ModifyControllerAccess(acl, 'grant', user) - return await controller_facade.ModifyControllerAccess([changes]) + try: + await controller_facade.ModifyControllerAccess([changes]) + return True + except errors.JujuError as e: + if 'user already has' in str(e): + return False + else: + raise - async def revoke(self, username): - """Removes all access from a controller + async def revoke(self, username, acl='login'): + """Removes some or all access of a user to from a controller + If 'login' access is revoked, the user will no longer have any + permissions on the controller. Revoking a higher privilege from + a user without that privilege will have no effect. :param str username: username - + :param str acl: Access to remove ('login', 'add-model' or 'superuser') """ controller_facade = client.ControllerFacade.from_connection( - self.connection) + self.connection()) user = tag.user(username) changes = client.ModifyControllerAccess('login', 'revoke', user) return await controller_facade.ModifyControllerAccess([changes]) + + async def grant_model(self, username, model_uuid, acl='read'): + """Grant a user access to a model. Note that if the user + already has higher permissions than the provided ACL, + this will do nothing (see revoke_model for a way to remove permissions). + + :param str username: Username + :param str model_uuid: The UUID of the model to change. + :param str acl: Access control ('read, 'write' or 'admin') + """ + model_facade = client.ModelManagerFacade.from_connection( + self.connection()) + user = tag.user(username) + model = tag.model(model_uuid) + changes = client.ModifyModelAccess(acl, 'grant', model, user) + return await model_facade.ModifyModelAccess([changes]) + + async def revoke_model(self, username, model_uuid, acl='read'): + """Revoke some or all of a user's access to a model. + If 'read' access is revoked, the user will no longer have any + permissions on the model. Revoking a higher privilege from + a user without that privilege will have no effect. + + :param str username: Username to revoke + :param str model_uuid: The UUID of the model to change. + :param str acl: Access control ('read, 'write' or 'admin') + """ + model_facade = client.ModelManagerFacade.from_connection( + self.connection()) + user = tag.user(username) + model = tag.model(self.info.uuid) + changes = client.ModifyModelAccess(acl, 'revoke', model, user) + return await model_facade.ModifyModelAccess([changes])