X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=modules%2Flibjuju%2Fjuju%2Fcontroller.py;h=d3902ba1135f41f2520171b27e22b3e149db9927;hp=957ab85da834ff43e60e2993e614303ba784eca2;hb=34cc6609cad010420aee843c15c0ded8fa608835;hpb=c3e6c2ec9a1fddfc8e9bd31509b366e633b6d99e;ds=sidebyside diff --git a/modules/libjuju/juju/controller.py b/modules/libjuju/juju/controller.py index 957ab85..d3902ba 100644 --- a/modules/libjuju/juju/controller.py +++ b/modules/libjuju/juju/controller.py @@ -1,5 +1,7 @@ import asyncio +import json import logging +from pathlib import Path from . import errors, tag, utils from .client import client, connector @@ -28,7 +30,8 @@ class Controller: `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. + :param jujudata JujuData: The source for current controller + information. """ self._connector = connector.Connector( loop=loop, @@ -48,32 +51,101 @@ class Controller: def loop(self): return self._connector.loop - async def connect(self, controller_name=None, **kwargs): + async def connect(self, *args, **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). - - Otherwise, if controller_name is None, connect to the - current controller. - - Otherwise, controller_name must specify the name - of a known controller. + This supports two calling conventions: + + The controller and (optionally) authentication information can be + taken from the data files created by the Juju CLI. This convention + will be used if a ``controller_name`` is specified, or if the + ``endpoint`` is not. + + Otherwise, both the ``endpoint`` and authentication information + (``username`` and ``password``, or ``bakery_client`` and/or + ``macaroons``) are required. + + If a single positional argument is given, it will be assumed to be + the ``controller_name``. Otherwise, the first positional argument, + if any, must be the ``endpoint``. + + Available parameters are: + + :param str controller_name: Name of controller registered with the + Juju CLI. + :param str endpoint: The hostname:port of the controller to connect to. + :param str username: The username for controller-local users (or None + to use macaroon-based login.) + :param str password: The password for controller-local users. + :param str cacert: The CA certificate of the controller + (PEM formatted). + :param httpbakery.Client bakery_client: The macaroon bakery client to + to use when performing macaroon-based login. Macaroon tokens + acquired when logging will be saved to bakery_client.cookies. + If this is None, a default bakery_client will be used. + :param list macaroons: List of macaroons to load into the + ``bakery_client``. + :param asyncio.BaseEventLoop loop: The event loop to use for async + operations. + :param int max_frame_size: The maximum websocket frame size to allow. """ await self.disconnect() - if not kwargs: - await self._connector.connect_controller(controller_name) + if 'endpoint' not in kwargs and len(args) < 2: + if args and 'model_name' in kwargs: + raise TypeError('connect() got multiple values for ' + 'controller_name') + elif args: + controller_name = args[0] + else: + controller_name = kwargs.pop('controller_name', None) + await self._connector.connect_controller(controller_name, **kwargs) 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') + if 'controller_name' in kwargs: + raise TypeError('connect() got values for both ' + 'controller_name and endpoint') + if args and 'endpoint' in kwargs: + raise TypeError('connect() got multiple values for endpoint') + has_userpass = (len(args) >= 3 or + {'username', 'password'}.issubset(kwargs)) + has_macaroons = (len(args) >= 5 or not + {'bakery_client', 'macaroons'}.isdisjoint(kwargs)) + if not (has_userpass or has_macaroons): + raise TypeError('connect() missing auth params') + arg_names = [ + 'endpoint', + 'username', + 'password', + 'cacert', + 'bakery_client', + 'macaroons', + 'loop', + 'max_frame_size', + ] + for i, arg in enumerate(args): + kwargs[arg_names[i]] = arg + if 'endpoint' not in kwargs: + raise ValueError('endpoint is required ' + 'if controller_name not given') + if not ({'username', 'password'}.issubset(kwargs) or + {'bakery_client', 'macaroons'}.intersection(kwargs)): + raise ValueError('Authentication parameters are required ' + 'if controller_name not given') await self._connector.connect(**kwargs) + async def connect_current(self): + """ + .. deprecated:: 0.7.3 + Use :meth:`.connect()` instead. + """ + return await self.connect() + + async def connect_controller(self, controller_name): + """ + .. deprecated:: 0.7.3 + Use :meth:`.connect(controller_name)` instead. + """ + return await self.connect(controller_name) + async def _connect_direct(self, **kwargs): await self.disconnect() await self._connector.connect(**kwargs) @@ -98,7 +170,7 @@ class Controller: await self._connector.disconnect() async def add_credential(self, name=None, credential=None, cloud=None, - owner=None): + owner=None, force=False): """Add or update a credential to the controller. :param str name: Name of new credential. If None, the default @@ -110,6 +182,9 @@ class Controller: Defaults to the same cloud as the controller. :param str owner: Username that will own the credential. Defaults to the current user. + :param bool force: Force indicates whether the update should be forced. + It's only supported for facade v3 or later. + Defaults to false. :returns: Name of credential that was uploaded. """ if not cloud: @@ -122,19 +197,42 @@ class Controller: raise errors.JujuError('Name must be provided for credential') if not credential: - name, credential = self._connector.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)) + if credential.auth_type == 'jsonfile' and 'file' in credential.attrs: + # file creds have to be loaded before being sent to the controller + try: + # it might already be JSON + json.loads(credential.attrs['file']) + except json.JSONDecodeError: + # not valid JSON, so maybe it's a file + cred_path = Path(credential.attrs['file']) + if cred_path.exists(): + # make a copy + cred_json = credential.to_json() + credential = client.CloudCredential.from_json(cred_json) + # inline the cred + credential.attrs['file'] = cred_path.read_text() + log.debug('Uploading credential %s', name) cloud_facade = client.CloudFacade.from_connection(self.connection()) - await cloud_facade.UpdateCredentials([ + tagged_credentials = [ client.UpdateCloudCredential( tag=tag.credential(cloud, tag.untag('user-', owner), name), credential=credential, - )]) - + )] + if cloud_facade.version >= 3: + # UpdateCredentials was renamed to UpdateCredentialsCheckModels + # in facade version 3. + await cloud_facade.UpdateCredentialsCheckModels( + credentials=tagged_credentials, force=force, + ) + else: + await cloud_facade.UpdateCredentials(tagged_credentials) return name async def add_model( @@ -202,10 +300,12 @@ class Controller: return model - async def destroy_models(self, *models): + async def destroy_models(self, *models, destroy_storage=False): """Destroy one or more models. - :param str \*models: Names or UUIDs of models to destroy + :param str *models: Names or UUIDs of models to destroy + :param bool destroy_storage: Whether or not to destroy storage when + destroying the models. Defaults to false. """ uuids = await self.model_uuids() @@ -221,10 +321,15 @@ class Controller: ', '.join(models) ) - await model_facade.DestroyModels([ - client.Entity(tag.model(model)) - for model in models - ]) + if model_facade.version >= 5: + params = [ + client.DestroyModelParams(model_tag=tag.model(model), + destroy_storage=destroy_storage) + for model in models] + else: + params = [client.Entity(tag.model(model)) for model in models] + + await model_facade.DestroyModels(params) destroy_model = destroy_models async def add_user(self, username, password=None, display_name=None): @@ -237,12 +342,14 @@ class Controller: """ 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)] - await user_facade.AddUser(users) - return await self.get_user(username) + results = await user_facade.AddUser(users) + secret_key = results.results[0].secret_key + return await self.get_user(username, secret_key=secret_key) async def remove_user(self, username): """Remove a user from this controller. @@ -259,10 +366,24 @@ class Controller: :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]) + async def reset_user_password(self, username): + """Reset user password. + + :param str username: Username + :returns: A :class:`~juju.user.User` instance + """ + user_facade = client.UserManagerFacade.from_connection( + self.connection()) + entity = client.Entity(tag.user(username)) + results = await user_facade.ResetPassword([entity]) + secret_key = results.results[0].secret_key + return await self.get_user(username, secret_key=secret_key) + async def destroy(self, destroy_all_models=False): """Destroy this controller. @@ -280,7 +401,8 @@ class Controller: :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]) @@ -288,7 +410,8 @@ class Controller: """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]) @@ -315,7 +438,7 @@ class Controller: Use :meth:`.list_models` instead. """ controller_facade = client.ControllerFacade.from_connection( - self.connection) + self.connection()) for attempt in (1, 2, 3): try: return await controller_facade.AllModels() @@ -353,7 +476,7 @@ class Controller: def get_payloads(self, *patterns): """Return list of known payloads. - :param str \*patterns: Patterns to match against + :param str *patterns: Patterns to match against Each pattern will be checked against the following info in Juju:: @@ -402,10 +525,12 @@ class Controller: await model._connect_direct(**kwargs) return model - async def get_user(self, username): + async def get_user(self, username, secret_key=None): """Get a user by name. :param str username: Username + :param str secret_key: Issued by juju when add or reset user + password :returns: A :class:`~juju.user.User` instance """ client_facade = client.UserManagerFacade.from_connection( @@ -421,7 +546,7 @@ class Controller: return None raise if response.results and response.results[0].result: - return User(self, response.results[0].result) + return User(self, response.results[0].result, secret_key=secret_key) return None async def get_users(self, include_disabled=False): @@ -476,7 +601,8 @@ class Controller: 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). + 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. @@ -502,6 +628,6 @@ class Controller: model_facade = client.ModelManagerFacade.from_connection( self.connection()) user = tag.user(username) - model = tag.model(self.info.uuid) + model = tag.model(model_uuid) changes = client.ModifyModelAccess(acl, 'revoke', model, user) return await model_facade.ModifyModelAccess([changes])