blob: b4c544edab5633350e1dcec82bd92da9084f4070 [file] [log] [blame]
Adam Israeldcdf82b2017-08-15 15:26:43 -04001import asyncio
Adam Israelb0943662018-08-02 15:32:00 -04002import json
Adam Israeldcdf82b2017-08-15 15:26:43 -04003import logging
Adam Israelb0943662018-08-02 15:32:00 -04004from pathlib import Path
Adam Israeldcdf82b2017-08-15 15:26:43 -04005
Adam Israelc3e6c2e2018-03-01 09:31:50 -05006from . import errors, tag, utils
7from .client import client, connector
Adam Israel1a15d1c2017-10-23 12:00:49 -04008from .user import User
Adam Israeldcdf82b2017-08-15 15:26:43 -04009
10log = logging.getLogger(__name__)
11
12
Adam Israelc3e6c2e2018-03-01 09:31:50 -050013class Controller:
14 def __init__(
15 self,
16 loop=None,
17 max_frame_size=None,
18 bakery_client=None,
19 jujudata=None,
20 ):
Adam Israeldcdf82b2017-08-15 15:26:43 -040021 """Instantiate a new Controller.
22
23 One of the connect_* methods will need to be called before this
24 object can be used for anything interesting.
25
Adam Israelc3e6c2e2018-03-01 09:31:50 -050026 If jujudata is None, jujudata.FileJujuData will be used.
27
Adam Israeldcdf82b2017-08-15 15:26:43 -040028 :param loop: an asyncio event loop
Adam Israelc3e6c2e2018-03-01 09:31:50 -050029 :param max_frame_size: See
30 `juju.client.connection.Connection.MAX_FRAME_SIZE`
31 :param bakery_client httpbakery.Client: The bakery client to use
32 for macaroon authorization.
33 :param jujudata JujuData: The source for current controller information.
Adam Israeldcdf82b2017-08-15 15:26:43 -040034 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -050035 self._connector = connector.Connector(
36 loop=loop,
37 max_frame_size=max_frame_size,
38 bakery_client=bakery_client,
39 jujudata=jujudata,
40 )
Adam Israeldcdf82b2017-08-15 15:26:43 -040041
Adam Israelc3e6c2e2018-03-01 09:31:50 -050042 async def __aenter__(self):
43 await self.connect()
44 return self
Adam Israeldcdf82b2017-08-15 15:26:43 -040045
Adam Israelc3e6c2e2018-03-01 09:31:50 -050046 async def __aexit__(self, exc_type, exc, tb):
47 await self.disconnect()
48
49 @property
50 def loop(self):
51 return self._connector.loop
52
Adam Israelb0943662018-08-02 15:32:00 -040053 async def connect(self, *args, **kwargs):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050054 """Connect to a Juju controller.
55
Adam Israelb0943662018-08-02 15:32:00 -040056 This supports two calling conventions:
Adam Israelc3e6c2e2018-03-01 09:31:50 -050057
Adam Israelb0943662018-08-02 15:32:00 -040058 The controller and (optionally) authentication information can be
59 taken from the data files created by the Juju CLI. This convention
60 will be used if a ``controller_name`` is specified, or if the
61 ``endpoint`` is not.
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062
Adam Israelb0943662018-08-02 15:32:00 -040063 Otherwise, both the ``endpoint`` and authentication information
64 (``username`` and ``password``, or ``bakery_client`` and/or
65 ``macaroons``) are required.
66
67 If a single positional argument is given, it will be assumed to be
68 the ``controller_name``. Otherwise, the first positional argument,
69 if any, must be the ``endpoint``.
70
71 Available parameters are:
72
73 :param str controller_name: Name of controller registered with the
74 Juju CLI.
75 :param str endpoint: The hostname:port of the controller to connect to.
76 :param str username: The username for controller-local users (or None
77 to use macaroon-based login.)
78 :param str password: The password for controller-local users.
79 :param str cacert: The CA certificate of the controller
80 (PEM formatted).
81 :param httpbakery.Client bakery_client: The macaroon bakery client to
82 to use when performing macaroon-based login. Macaroon tokens
83 acquired when logging will be saved to bakery_client.cookies.
84 If this is None, a default bakery_client will be used.
85 :param list macaroons: List of macaroons to load into the
86 ``bakery_client``.
87 :param asyncio.BaseEventLoop loop: The event loop to use for async
88 operations.
89 :param int max_frame_size: The maximum websocket frame size to allow.
Adam Israeldcdf82b2017-08-15 15:26:43 -040090 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -050091 await self.disconnect()
Adam Israelb0943662018-08-02 15:32:00 -040092 if 'endpoint' not in kwargs and len(args) < 2:
93 if args and 'model_name' in kwargs:
94 raise TypeError('connect() got multiple values for '
95 'controller_name')
96 elif args:
97 controller_name = args[0]
98 else:
99 controller_name = kwargs.pop('controller_name', None)
100 await self._connector.connect_controller(controller_name, **kwargs)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500101 else:
Adam Israelb0943662018-08-02 15:32:00 -0400102 if 'controller_name' in kwargs:
103 raise TypeError('connect() got values for both '
104 'controller_name and endpoint')
105 if args and 'endpoint' in kwargs:
106 raise TypeError('connect() got multiple values for endpoint')
107 has_userpass = (len(args) >= 3 or
108 {'username', 'password'}.issubset(kwargs))
109 has_macaroons = (len(args) >= 5 or not
110 {'bakery_client', 'macaroons'}.isdisjoint(kwargs))
111 if not (has_userpass or has_macaroons):
112 raise TypeError('connect() missing auth params')
113 arg_names = [
114 'endpoint',
115 'username',
116 'password',
117 'cacert',
118 'bakery_client',
119 'macaroons',
120 'loop',
121 'max_frame_size',
122 ]
123 for i, arg in enumerate(args):
124 kwargs[arg_names[i]] = arg
125 if 'endpoint' not in kwargs:
126 raise ValueError('endpoint is required '
127 'if controller_name not given')
128 if not ({'username', 'password'}.issubset(kwargs) or
129 {'bakery_client', 'macaroons'}.intersection(kwargs)):
130 raise ValueError('Authentication parameters are required '
131 'if controller_name not given')
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500132 await self._connector.connect(**kwargs)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400133
Adam Israelb0943662018-08-02 15:32:00 -0400134 async def connect_current(self):
135 """
136 .. deprecated:: 0.7.3
137 Use :meth:`.connect()` instead.
138 """
139 return await self.connect()
140
141 async def connect_controller(self, controller_name):
142 """
143 .. deprecated:: 0.7.3
144 Use :meth:`.connect(controller_name)` instead.
145 """
146 return await self.connect(controller_name)
147
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500148 async def _connect_direct(self, **kwargs):
149 await self.disconnect()
150 await self._connector.connect(**kwargs)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400151
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500152 def is_connected(self):
153 """Reports whether the Controller is currently connected."""
154 return self._connector.is_connected()
Adam Israeldcdf82b2017-08-15 15:26:43 -0400155
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500156 def connection(self):
157 """Return the current Connection object. It raises an exception
158 if the Controller is disconnected"""
159 return self._connector.connection()
Adam Israeldcdf82b2017-08-15 15:26:43 -0400160
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500161 @property
162 def controller_name(self):
163 return self._connector.controller_name
Adam Israeldcdf82b2017-08-15 15:26:43 -0400164
165 async def disconnect(self):
166 """Shut down the watcher task and close websockets.
167
168 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500169 await self._connector.disconnect()
Adam Israeldcdf82b2017-08-15 15:26:43 -0400170
Adam Israel1a15d1c2017-10-23 12:00:49 -0400171 async def add_credential(self, name=None, credential=None, cloud=None,
172 owner=None):
173 """Add or update a credential to the controller.
174
175 :param str name: Name of new credential. If None, the default
176 local credential is used. Name must be provided if a credential
177 is given.
178 :param CloudCredential credential: Credential to add. If not given,
179 it will attempt to read from local data, if available.
180 :param str cloud: Name of cloud to associate the credential with.
181 Defaults to the same cloud as the controller.
182 :param str owner: Username that will own the credential. Defaults to
183 the current user.
184 :returns: Name of credential that was uploaded.
185 """
186 if not cloud:
187 cloud = await self.get_cloud()
188
189 if not owner:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500190 owner = self.connection().info['user-info']['identity']
Adam Israel1a15d1c2017-10-23 12:00:49 -0400191
192 if credential and not name:
193 raise errors.JujuError('Name must be provided for credential')
194
195 if not credential:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500196 name, credential = self._connector.jujudata.load_credential(cloud, name)
Adam Israel1a15d1c2017-10-23 12:00:49 -0400197 if credential is None:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500198 raise errors.JujuError(
199 'Unable to find credential: {}'.format(name))
Adam Israel1a15d1c2017-10-23 12:00:49 -0400200
Adam Israelb0943662018-08-02 15:32:00 -0400201 if credential.auth_type == 'jsonfile' and 'file' in credential.attrs:
202 # file creds have to be loaded before being sent to the controller
203 try:
204 # it might already be JSON
205 json.loads(credential.attrs['file'])
206 except json.JSONDecodeError:
207 # not valid JSON, so maybe it's a file
208 cred_path = Path(credential.attrs['file'])
209 if cred_path.exists():
210 # make a copy
211 cred_json = credential.to_json()
212 credential = client.CloudCredential.from_json(cred_json)
213 # inline the cred
214 credential.attrs['file'] = cred_path.read_text()
215
Adam Israel1a15d1c2017-10-23 12:00:49 -0400216 log.debug('Uploading credential %s', name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500217 cloud_facade = client.CloudFacade.from_connection(self.connection())
Adam Israel1a15d1c2017-10-23 12:00:49 -0400218 await cloud_facade.UpdateCredentials([
219 client.UpdateCloudCredential(
220 tag=tag.credential(cloud, tag.untag('user-', owner), name),
221 credential=credential,
222 )])
223
224 return name
225
Adam Israeldcdf82b2017-08-15 15:26:43 -0400226 async def add_model(
227 self, model_name, cloud_name=None, credential_name=None,
228 owner=None, config=None, region=None):
229 """Add a model to this controller.
230
231 :param str model_name: Name to give the new model.
232 :param str cloud_name: Name of the cloud in which to create the
233 model, e.g. 'aws'. Defaults to same cloud as controller.
234 :param str credential_name: Name of the credential to use when
Adam Israel1a15d1c2017-10-23 12:00:49 -0400235 creating the model. If not given, it will attempt to find a
236 default credential.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400237 :param str owner: Username that will own the model. Defaults to
238 the current user.
239 :param dict config: Model configuration.
240 :param str region: Region in which to create the model.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500241 :return Model: A connection to the newly created model.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400242 """
243 model_facade = client.ModelManagerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500244 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400245
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500246 owner = owner or self.connection().info['user-info']['identity']
Adam Israeldcdf82b2017-08-15 15:26:43 -0400247 cloud_name = cloud_name or await self.get_cloud()
248
Adam Israel1a15d1c2017-10-23 12:00:49 -0400249 try:
250 # attempt to add/update the credential from local data if available
251 credential_name = await self.add_credential(
252 name=credential_name,
253 cloud=cloud_name,
254 owner=owner)
255 except errors.JujuError:
256 # if it's not available locally, assume it's on the controller
257 pass
258
Adam Israeldcdf82b2017-08-15 15:26:43 -0400259 if credential_name:
260 credential = tag.credential(
261 cloud_name,
262 tag.untag('user-', owner),
263 credential_name
264 )
265 else:
266 credential = None
267
268 log.debug('Creating model %s', model_name)
269
Adam Israel1a15d1c2017-10-23 12:00:49 -0400270 if not config or 'authorized-keys' not in config:
271 config = config or {}
272 config['authorized-keys'] = await utils.read_ssh_key(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500273 loop=self._connector.loop)
Adam Israel1a15d1c2017-10-23 12:00:49 -0400274
Adam Israeldcdf82b2017-08-15 15:26:43 -0400275 model_info = await model_facade.CreateModel(
276 tag.cloud(cloud_name),
277 config,
278 credential,
279 model_name,
280 owner,
281 region
282 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500283 from juju.model import Model
284 model = Model(jujudata=self._connector.jujudata)
285 kwargs = self.connection().connect_params()
286 kwargs['uuid'] = model_info.uuid
287 await model._connect_direct(**kwargs)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400288
289 return model
290
Adam Israel1a15d1c2017-10-23 12:00:49 -0400291 async def destroy_models(self, *models):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400292 """Destroy one or more models.
293
Adam Israel1a15d1c2017-10-23 12:00:49 -0400294 :param str \*models: Names or UUIDs of models to destroy
Adam Israeldcdf82b2017-08-15 15:26:43 -0400295
296 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500297 uuids = await self.model_uuids()
Adam Israel1a15d1c2017-10-23 12:00:49 -0400298 models = [uuids[model] if model in uuids else model
299 for model in models]
300
Adam Israeldcdf82b2017-08-15 15:26:43 -0400301 model_facade = client.ModelManagerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500302 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400303
304 log.debug(
305 'Destroying model%s %s',
Adam Israel1a15d1c2017-10-23 12:00:49 -0400306 '' if len(models) == 1 else 's',
307 ', '.join(models)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400308 )
309
310 await model_facade.DestroyModels([
Adam Israel1a15d1c2017-10-23 12:00:49 -0400311 client.Entity(tag.model(model))
312 for model in models
Adam Israeldcdf82b2017-08-15 15:26:43 -0400313 ])
314 destroy_model = destroy_models
315
316 async def add_user(self, username, password=None, display_name=None):
317 """Add a user to this controller.
318
319 :param str username: Username
Adam Israel1a15d1c2017-10-23 12:00:49 -0400320 :param str password: Password
Adam Israeldcdf82b2017-08-15 15:26:43 -0400321 :param str display_name: Display name
Adam Israel1a15d1c2017-10-23 12:00:49 -0400322 :returns: A :class:`~juju.user.User` instance
Adam Israeldcdf82b2017-08-15 15:26:43 -0400323 """
324 if not display_name:
325 display_name = username
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500326 user_facade = client.UserManagerFacade.from_connection(self.connection())
Adam Israel1a15d1c2017-10-23 12:00:49 -0400327 users = [client.AddUser(display_name=display_name,
328 username=username,
329 password=password)]
330 await user_facade.AddUser(users)
331 return await self.get_user(username)
332
333 async def remove_user(self, username):
334 """Remove a user from this controller.
335 """
336 client_facade = client.UserManagerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500337 self.connection())
Adam Israel1a15d1c2017-10-23 12:00:49 -0400338 user = tag.user(username)
339 await client_facade.RemoveUser([client.Entity(user)])
Adam Israeldcdf82b2017-08-15 15:26:43 -0400340
341 async def change_user_password(self, username, password):
342 """Change the password for a user in this controller.
343
344 :param str username: Username
345 :param str password: New password
346
347 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500348 user_facade = client.UserManagerFacade.from_connection(self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400349 entity = client.EntityPassword(password, tag.user(username))
350 return await user_facade.SetPassword([entity])
351
352 async def destroy(self, destroy_all_models=False):
353 """Destroy this controller.
354
355 :param bool destroy_all_models: Destroy all hosted models in the
356 controller.
357
358 """
359 controller_facade = client.ControllerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500360 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400361 return await controller_facade.DestroyController(destroy_all_models)
362
363 async def disable_user(self, username):
364 """Disable a user.
365
366 :param str username: Username
367
368 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500369 user_facade = client.UserManagerFacade.from_connection(self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400370 entity = client.Entity(tag.user(username))
371 return await user_facade.DisableUser([entity])
372
373 async def enable_user(self, username):
374 """Re-enable a previously disabled user.
375
376 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500377 user_facade = client.UserManagerFacade.from_connection(self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400378 entity = client.Entity(tag.user(username))
379 return await user_facade.EnableUser([entity])
380
381 def kill(self):
382 """Forcibly terminate all machines and other associated resources for
383 this controller.
384
385 """
386 raise NotImplementedError()
387
388 async def get_cloud(self):
389 """
390 Get the name of the cloud that this controller lives on.
391 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500392 cloud_facade = client.CloudFacade.from_connection(self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400393
394 result = await cloud_facade.Clouds()
395 cloud = list(result.clouds.keys())[0] # only lives on one cloud
396 return tag.untag('cloud-', cloud)
397
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500398 async def get_models(self, all_=False, username=None):
399 """
400 .. deprecated:: 0.7.0
401 Use :meth:`.list_models` instead.
402 """
Adam Israel1a15d1c2017-10-23 12:00:49 -0400403 controller_facade = client.ControllerFacade.from_connection(
Adam Israelb0943662018-08-02 15:32:00 -0400404 self.connection())
Adam Israel1a15d1c2017-10-23 12:00:49 -0400405 for attempt in (1, 2, 3):
406 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500407 return await controller_facade.AllModels()
408 except errors.JujuAPIError as e:
409 # retry concurrency error until resolved in Juju
410 # see: https://bugs.launchpad.net/juju/+bug/1721786
411 if 'has been removed' not in e.message or attempt == 3:
412 raise
413
414 async def model_uuids(self):
415 """Return a mapping of model names to UUIDs.
416 """
417 controller_facade = client.ControllerFacade.from_connection(
418 self.connection())
419 for attempt in (1, 2, 3):
420 try:
Adam Israel1a15d1c2017-10-23 12:00:49 -0400421 response = await controller_facade.AllModels()
422 return {um.model.name: um.model.uuid
423 for um in response.user_models}
424 except errors.JujuAPIError as e:
425 # retry concurrency error until resolved in Juju
426 # see: https://bugs.launchpad.net/juju/+bug/1721786
427 if 'has been removed' not in e.message or attempt == 3:
428 raise
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500429 await asyncio.sleep(attempt, loop=self._connector.loop)
Adam Israel1a15d1c2017-10-23 12:00:49 -0400430
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500431 async def list_models(self):
Adam Israel1a15d1c2017-10-23 12:00:49 -0400432 """Return list of names of the available models on this controller.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400433
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500434 Equivalent to ``sorted((await self.model_uuids()).keys())``
Adam Israeldcdf82b2017-08-15 15:26:43 -0400435 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500436 uuids = await self.model_uuids()
Adam Israel1a15d1c2017-10-23 12:00:49 -0400437 return sorted(uuids.keys())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400438
439 def get_payloads(self, *patterns):
440 """Return list of known payloads.
441
442 :param str \*patterns: Patterns to match against
443
444 Each pattern will be checked against the following info in Juju::
445
446 - unit name
447 - machine id
448 - payload type
449 - payload class
450 - payload id
451 - payload tag
452 - payload status
453
454 """
455 raise NotImplementedError()
456
Adam Israeldcdf82b2017-08-15 15:26:43 -0400457 def login(self):
458 """Log in to this controller.
459
460 """
461 raise NotImplementedError()
462
463 def logout(self, force=False):
464 """Log out of this controller.
465
466 :param bool force: Don't fail even if user not previously logged in
467 with a password
468
469 """
470 raise NotImplementedError()
471
Adam Israel1a15d1c2017-10-23 12:00:49 -0400472 async def get_model(self, model):
473 """Get a model by name or UUID.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400474
Adam Israel1a15d1c2017-10-23 12:00:49 -0400475 :param str model: Model name or UUID
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500476 :returns Model: Connected Model instance.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400477 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 uuids = await self.model_uuids()
Adam Israel1a15d1c2017-10-23 12:00:49 -0400479 if model in uuids:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500480 uuid = uuids[model]
Adam Israel1a15d1c2017-10-23 12:00:49 -0400481 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500482 uuid = model
Adam Israeldcdf82b2017-08-15 15:26:43 -0400483
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500484 from juju.model import Model
Adam Israel1a15d1c2017-10-23 12:00:49 -0400485 model = Model()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500486 kwargs = self.connection().connect_params()
487 kwargs['uuid'] = uuid
488 await model._connect_direct(**kwargs)
Adam Israel1a15d1c2017-10-23 12:00:49 -0400489 return model
490
491 async def get_user(self, username):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400492 """Get a user by name.
493
494 :param str username: Username
Adam Israel1a15d1c2017-10-23 12:00:49 -0400495 :returns: A :class:`~juju.user.User` instance
Adam Israeldcdf82b2017-08-15 15:26:43 -0400496 """
497 client_facade = client.UserManagerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500498 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400499 user = tag.user(username)
Adam Israel1a15d1c2017-10-23 12:00:49 -0400500 args = [client.Entity(user)]
501 try:
502 response = await client_facade.UserInfo(args, True)
503 except errors.JujuError as e:
504 if 'permission denied' in e.errors:
505 # apparently, trying to get info for a nonexistent user returns
506 # a "permission denied" error rather than an empty result set
507 return None
508 raise
509 if response.results and response.results[0].result:
510 return User(self, response.results[0].result)
511 return None
512
513 async def get_users(self, include_disabled=False):
514 """Return list of users that can connect to this controller.
515
516 :param bool include_disabled: Include disabled users
517 :returns: A list of :class:`~juju.user.User` instances
518 """
519 client_facade = client.UserManagerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500520 self.connection())
Adam Israel1a15d1c2017-10-23 12:00:49 -0400521 response = await client_facade.UserInfo(None, include_disabled)
522 return [User(self, r.result) for r in response.results]
Adam Israeldcdf82b2017-08-15 15:26:43 -0400523
524 async def grant(self, username, acl='login'):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500525 """Grant access level of the given user on the controller.
526 Note that if the user already has higher permissions than the
527 provided ACL, this will do nothing (see revoke for a way to
528 remove permissions).
Adam Israeldcdf82b2017-08-15 15:26:43 -0400529 :param str username: Username
530 :param str acl: Access control ('login', 'add-model' or 'superuser')
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500531 :returns: True if new access was granted, False if user already had
532 requested access or greater. Raises JujuError if failed.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400533 """
534 controller_facade = client.ControllerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500535 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400536 user = tag.user(username)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400537 changes = client.ModifyControllerAccess(acl, 'grant', user)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500538 try:
539 await controller_facade.ModifyControllerAccess([changes])
540 return True
541 except errors.JujuError as e:
542 if 'user already has' in str(e):
543 return False
544 else:
545 raise
Adam Israeldcdf82b2017-08-15 15:26:43 -0400546
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500547 async def revoke(self, username, acl='login'):
548 """Removes some or all access of a user to from a controller
549 If 'login' access is revoked, the user will no longer have any
550 permissions on the controller. Revoking a higher privilege from
551 a user without that privilege will have no effect.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400552
553 :param str username: username
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500554 :param str acl: Access to remove ('login', 'add-model' or 'superuser')
Adam Israeldcdf82b2017-08-15 15:26:43 -0400555 """
556 controller_facade = client.ControllerFacade.from_connection(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500557 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400558 user = tag.user(username)
559 changes = client.ModifyControllerAccess('login', 'revoke', user)
560 return await controller_facade.ModifyControllerAccess([changes])
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500561
562 async def grant_model(self, username, model_uuid, acl='read'):
563 """Grant a user access to a model. Note that if the user
564 already has higher permissions than the provided ACL,
565 this will do nothing (see revoke_model for a way to remove permissions).
566
567 :param str username: Username
568 :param str model_uuid: The UUID of the model to change.
569 :param str acl: Access control ('read, 'write' or 'admin')
570 """
571 model_facade = client.ModelManagerFacade.from_connection(
572 self.connection())
573 user = tag.user(username)
574 model = tag.model(model_uuid)
575 changes = client.ModifyModelAccess(acl, 'grant', model, user)
576 return await model_facade.ModifyModelAccess([changes])
577
578 async def revoke_model(self, username, model_uuid, acl='read'):
579 """Revoke some or all of a user's access to a model.
580 If 'read' access is revoked, the user will no longer have any
581 permissions on the model. Revoking a higher privilege from
582 a user without that privilege will have no effect.
583
584 :param str username: Username to revoke
585 :param str model_uuid: The UUID of the model to change.
586 :param str acl: Access control ('read, 'write' or 'admin')
587 """
588 model_facade = client.ModelManagerFacade.from_connection(
589 self.connection())
590 user = tag.user(username)
591 model = tag.model(self.info.uuid)
592 changes = client.ModifyModelAccess(acl, 'revoke', model, user)
593 return await model_facade.ModifyModelAccess([changes])