blob: d3902ba1135f41f2520171b27e22b3e149db9927 [file] [log] [blame]
Adam Israeldcdf82b2017-08-15 15:26:43 -04001import asyncio
Adam Israelb8a82812019-03-27 14:50:11 -04002import json
Adam Israeldcdf82b2017-08-15 15:26:43 -04003import logging
Adam Israelb8a82812019-03-27 14:50:11 -04004from pathlib import Path
Adam Israeldcdf82b2017-08-15 15:26:43 -04005
Adam Israelb8a82812019-03-27 14:50:11 -04006from . import errors, tag, utils
7from .client import client, connector
8from .user import User
Adam Israeldcdf82b2017-08-15 15:26:43 -04009
10log = logging.getLogger(__name__)
11
12
Adam Israelb8a82812019-03-27 14:50:11 -040013class 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 Israelb8a82812019-03-27 14:50:11 -040026 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 Israelb8a82812019-03-27 14:50:11 -040029 :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
34 information.
Adam Israeldcdf82b2017-08-15 15:26:43 -040035 """
Adam Israelb8a82812019-03-27 14:50:11 -040036 self._connector = connector.Connector(
37 loop=loop,
38 max_frame_size=max_frame_size,
39 bakery_client=bakery_client,
40 jujudata=jujudata,
41 )
Adam Israeldcdf82b2017-08-15 15:26:43 -040042
Adam Israelb8a82812019-03-27 14:50:11 -040043 async def __aenter__(self):
44 await self.connect()
45 return self
Adam Israeldcdf82b2017-08-15 15:26:43 -040046
Adam Israelb8a82812019-03-27 14:50:11 -040047 async def __aexit__(self, exc_type, exc, tb):
48 await self.disconnect()
49
50 @property
51 def loop(self):
52 return self._connector.loop
53
54 async def connect(self, *args, **kwargs):
55 """Connect to a Juju controller.
56
57 This supports two calling conventions:
58
59 The controller and (optionally) authentication information can be
60 taken from the data files created by the Juju CLI. This convention
61 will be used if a ``controller_name`` is specified, or if the
62 ``endpoint`` is not.
63
64 Otherwise, both the ``endpoint`` and authentication information
65 (``username`` and ``password``, or ``bakery_client`` and/or
66 ``macaroons``) are required.
67
68 If a single positional argument is given, it will be assumed to be
69 the ``controller_name``. Otherwise, the first positional argument,
70 if any, must be the ``endpoint``.
71
72 Available parameters are:
73
74 :param str controller_name: Name of controller registered with the
75 Juju CLI.
76 :param str endpoint: The hostname:port of the controller to connect to.
77 :param str username: The username for controller-local users (or None
78 to use macaroon-based login.)
79 :param str password: The password for controller-local users.
80 :param str cacert: The CA certificate of the controller
81 (PEM formatted).
82 :param httpbakery.Client bakery_client: The macaroon bakery client to
83 to use when performing macaroon-based login. Macaroon tokens
84 acquired when logging will be saved to bakery_client.cookies.
85 If this is None, a default bakery_client will be used.
86 :param list macaroons: List of macaroons to load into the
87 ``bakery_client``.
88 :param asyncio.BaseEventLoop loop: The event loop to use for async
89 operations.
90 :param int max_frame_size: The maximum websocket frame size to allow.
Adam Israeldcdf82b2017-08-15 15:26:43 -040091 """
Adam Israelb8a82812019-03-27 14:50:11 -040092 await self.disconnect()
93 if 'endpoint' not in kwargs and len(args) < 2:
94 if args and 'model_name' in kwargs:
95 raise TypeError('connect() got multiple values for '
96 'controller_name')
97 elif args:
98 controller_name = args[0]
99 else:
100 controller_name = kwargs.pop('controller_name', None)
101 await self._connector.connect_controller(controller_name, **kwargs)
102 else:
103 if 'controller_name' in kwargs:
104 raise TypeError('connect() got values for both '
105 'controller_name and endpoint')
106 if args and 'endpoint' in kwargs:
107 raise TypeError('connect() got multiple values for endpoint')
108 has_userpass = (len(args) >= 3 or
109 {'username', 'password'}.issubset(kwargs))
110 has_macaroons = (len(args) >= 5 or not
111 {'bakery_client', 'macaroons'}.isdisjoint(kwargs))
112 if not (has_userpass or has_macaroons):
113 raise TypeError('connect() missing auth params')
114 arg_names = [
115 'endpoint',
116 'username',
117 'password',
118 'cacert',
119 'bakery_client',
120 'macaroons',
121 'loop',
122 'max_frame_size',
123 ]
124 for i, arg in enumerate(args):
125 kwargs[arg_names[i]] = arg
126 if 'endpoint' not in kwargs:
127 raise ValueError('endpoint is required '
128 'if controller_name not given')
129 if not ({'username', 'password'}.issubset(kwargs) or
130 {'bakery_client', 'macaroons'}.intersection(kwargs)):
131 raise ValueError('Authentication parameters are required '
132 'if controller_name not given')
133 await self._connector.connect(**kwargs)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400134
135 async def connect_current(self):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400136 """
Adam Israelb8a82812019-03-27 14:50:11 -0400137 .. deprecated:: 0.7.3
138 Use :meth:`.connect()` instead.
139 """
140 return await self.connect()
Adam Israeldcdf82b2017-08-15 15:26:43 -0400141
142 async def connect_controller(self, controller_name):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400143 """
Adam Israelb8a82812019-03-27 14:50:11 -0400144 .. deprecated:: 0.7.3
145 Use :meth:`.connect(controller_name)` instead.
146 """
147 return await self.connect(controller_name)
148
149 async def _connect_direct(self, **kwargs):
150 await self.disconnect()
151 await self._connector.connect(**kwargs)
152
153 def is_connected(self):
154 """Reports whether the Controller is currently connected."""
155 return self._connector.is_connected()
156
157 def connection(self):
158 """Return the current Connection object. It raises an exception
159 if the Controller is disconnected"""
160 return self._connector.connection()
161
162 @property
163 def controller_name(self):
164 return self._connector.controller_name
Adam Israeldcdf82b2017-08-15 15:26:43 -0400165
166 async def disconnect(self):
167 """Shut down the watcher task and close websockets.
168
169 """
Adam Israelb8a82812019-03-27 14:50:11 -0400170 await self._connector.disconnect()
171
172 async def add_credential(self, name=None, credential=None, cloud=None,
173 owner=None, force=False):
174 """Add or update a credential to the controller.
175
176 :param str name: Name of new credential. If None, the default
177 local credential is used. Name must be provided if a credential
178 is given.
179 :param CloudCredential credential: Credential to add. If not given,
180 it will attempt to read from local data, if available.
181 :param str cloud: Name of cloud to associate the credential with.
182 Defaults to the same cloud as the controller.
183 :param str owner: Username that will own the credential. Defaults to
184 the current user.
185 :param bool force: Force indicates whether the update should be forced.
186 It's only supported for facade v3 or later.
187 Defaults to false.
188 :returns: Name of credential that was uploaded.
189 """
190 if not cloud:
191 cloud = await self.get_cloud()
192
193 if not owner:
194 owner = self.connection().info['user-info']['identity']
195
196 if credential and not name:
197 raise errors.JujuError('Name must be provided for credential')
198
199 if not credential:
200 name, credential = self._connector.jujudata.load_credential(cloud,
201 name)
202 if credential is None:
203 raise errors.JujuError(
204 'Unable to find credential: {}'.format(name))
205
206 if credential.auth_type == 'jsonfile' and 'file' in credential.attrs:
207 # file creds have to be loaded before being sent to the controller
208 try:
209 # it might already be JSON
210 json.loads(credential.attrs['file'])
211 except json.JSONDecodeError:
212 # not valid JSON, so maybe it's a file
213 cred_path = Path(credential.attrs['file'])
214 if cred_path.exists():
215 # make a copy
216 cred_json = credential.to_json()
217 credential = client.CloudCredential.from_json(cred_json)
218 # inline the cred
219 credential.attrs['file'] = cred_path.read_text()
220
221 log.debug('Uploading credential %s', name)
222 cloud_facade = client.CloudFacade.from_connection(self.connection())
223 tagged_credentials = [
224 client.UpdateCloudCredential(
225 tag=tag.credential(cloud, tag.untag('user-', owner), name),
226 credential=credential,
227 )]
228 if cloud_facade.version >= 3:
229 # UpdateCredentials was renamed to UpdateCredentialsCheckModels
230 # in facade version 3.
231 await cloud_facade.UpdateCredentialsCheckModels(
232 credentials=tagged_credentials, force=force,
233 )
234 else:
235 await cloud_facade.UpdateCredentials(tagged_credentials)
236 return name
Adam Israeldcdf82b2017-08-15 15:26:43 -0400237
238 async def add_model(
239 self, model_name, cloud_name=None, credential_name=None,
240 owner=None, config=None, region=None):
241 """Add a model to this controller.
242
243 :param str model_name: Name to give the new model.
244 :param str cloud_name: Name of the cloud in which to create the
245 model, e.g. 'aws'. Defaults to same cloud as controller.
246 :param str credential_name: Name of the credential to use when
Adam Israelb8a82812019-03-27 14:50:11 -0400247 creating the model. If not given, it will attempt to find a
248 default credential.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400249 :param str owner: Username that will own the model. Defaults to
250 the current user.
251 :param dict config: Model configuration.
252 :param str region: Region in which to create the model.
Adam Israelb8a82812019-03-27 14:50:11 -0400253 :return Model: A connection to the newly created model.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400254 """
255 model_facade = client.ModelManagerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400256 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400257
Adam Israelb8a82812019-03-27 14:50:11 -0400258 owner = owner or self.connection().info['user-info']['identity']
Adam Israeldcdf82b2017-08-15 15:26:43 -0400259 cloud_name = cloud_name or await self.get_cloud()
260
Adam Israelb8a82812019-03-27 14:50:11 -0400261 try:
262 # attempt to add/update the credential from local data if available
263 credential_name = await self.add_credential(
264 name=credential_name,
265 cloud=cloud_name,
266 owner=owner)
267 except errors.JujuError:
268 # if it's not available locally, assume it's on the controller
269 pass
270
Adam Israeldcdf82b2017-08-15 15:26:43 -0400271 if credential_name:
272 credential = tag.credential(
273 cloud_name,
274 tag.untag('user-', owner),
275 credential_name
276 )
277 else:
278 credential = None
279
280 log.debug('Creating model %s', model_name)
281
Adam Israelb8a82812019-03-27 14:50:11 -0400282 if not config or 'authorized-keys' not in config:
283 config = config or {}
284 config['authorized-keys'] = await utils.read_ssh_key(
285 loop=self._connector.loop)
286
Adam Israeldcdf82b2017-08-15 15:26:43 -0400287 model_info = await model_facade.CreateModel(
288 tag.cloud(cloud_name),
289 config,
290 credential,
291 model_name,
292 owner,
293 region
294 )
Adam Israelb8a82812019-03-27 14:50:11 -0400295 from juju.model import Model
296 model = Model(jujudata=self._connector.jujudata)
297 kwargs = self.connection().connect_params()
298 kwargs['uuid'] = model_info.uuid
299 await model._connect_direct(**kwargs)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400300
301 return model
302
Adam Israelb8a82812019-03-27 14:50:11 -0400303 async def destroy_models(self, *models, destroy_storage=False):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400304 """Destroy one or more models.
305
Adam Israelb8a82812019-03-27 14:50:11 -0400306 :param str *models: Names or UUIDs of models to destroy
307 :param bool destroy_storage: Whether or not to destroy storage when
308 destroying the models. Defaults to false.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400309
310 """
Adam Israelb8a82812019-03-27 14:50:11 -0400311 uuids = await self.model_uuids()
312 models = [uuids[model] if model in uuids else model
313 for model in models]
314
Adam Israeldcdf82b2017-08-15 15:26:43 -0400315 model_facade = client.ModelManagerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400316 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400317
318 log.debug(
319 'Destroying model%s %s',
Adam Israelb8a82812019-03-27 14:50:11 -0400320 '' if len(models) == 1 else 's',
321 ', '.join(models)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400322 )
323
Adam Israelb8a82812019-03-27 14:50:11 -0400324 if model_facade.version >= 5:
325 params = [
326 client.DestroyModelParams(model_tag=tag.model(model),
327 destroy_storage=destroy_storage)
328 for model in models]
329 else:
330 params = [client.Entity(tag.model(model)) for model in models]
331
332 await model_facade.DestroyModels(params)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400333 destroy_model = destroy_models
334
335 async def add_user(self, username, password=None, display_name=None):
336 """Add a user to this controller.
337
338 :param str username: Username
Adam Israelb8a82812019-03-27 14:50:11 -0400339 :param str password: Password
Adam Israeldcdf82b2017-08-15 15:26:43 -0400340 :param str display_name: Display name
Adam Israelb8a82812019-03-27 14:50:11 -0400341 :returns: A :class:`~juju.user.User` instance
Adam Israeldcdf82b2017-08-15 15:26:43 -0400342 """
343 if not display_name:
344 display_name = username
Adam Israelb8a82812019-03-27 14:50:11 -0400345 user_facade = client.UserManagerFacade.from_connection(
346 self.connection())
347 users = [client.AddUser(display_name=display_name,
348 username=username,
349 password=password)]
350 results = await user_facade.AddUser(users)
351 secret_key = results.results[0].secret_key
352 return await self.get_user(username, secret_key=secret_key)
353
354 async def remove_user(self, username):
355 """Remove a user from this controller.
356 """
357 client_facade = client.UserManagerFacade.from_connection(
358 self.connection())
359 user = tag.user(username)
360 await client_facade.RemoveUser([client.Entity(user)])
Adam Israeldcdf82b2017-08-15 15:26:43 -0400361
362 async def change_user_password(self, username, password):
363 """Change the password for a user in this controller.
364
365 :param str username: Username
366 :param str password: New password
367
368 """
Adam Israelb8a82812019-03-27 14:50:11 -0400369 user_facade = client.UserManagerFacade.from_connection(
370 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400371 entity = client.EntityPassword(password, tag.user(username))
372 return await user_facade.SetPassword([entity])
373
Adam Israelb8a82812019-03-27 14:50:11 -0400374 async def reset_user_password(self, username):
375 """Reset user password.
376
377 :param str username: Username
378 :returns: A :class:`~juju.user.User` instance
379 """
380 user_facade = client.UserManagerFacade.from_connection(
381 self.connection())
382 entity = client.Entity(tag.user(username))
383 results = await user_facade.ResetPassword([entity])
384 secret_key = results.results[0].secret_key
385 return await self.get_user(username, secret_key=secret_key)
386
Adam Israeldcdf82b2017-08-15 15:26:43 -0400387 async def destroy(self, destroy_all_models=False):
388 """Destroy this controller.
389
390 :param bool destroy_all_models: Destroy all hosted models in the
391 controller.
392
393 """
394 controller_facade = client.ControllerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400395 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400396 return await controller_facade.DestroyController(destroy_all_models)
397
398 async def disable_user(self, username):
399 """Disable a user.
400
401 :param str username: Username
402
403 """
Adam Israelb8a82812019-03-27 14:50:11 -0400404 user_facade = client.UserManagerFacade.from_connection(
405 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400406 entity = client.Entity(tag.user(username))
407 return await user_facade.DisableUser([entity])
408
409 async def enable_user(self, username):
410 """Re-enable a previously disabled user.
411
412 """
Adam Israelb8a82812019-03-27 14:50:11 -0400413 user_facade = client.UserManagerFacade.from_connection(
414 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400415 entity = client.Entity(tag.user(username))
416 return await user_facade.EnableUser([entity])
417
418 def kill(self):
419 """Forcibly terminate all machines and other associated resources for
420 this controller.
421
422 """
423 raise NotImplementedError()
424
425 async def get_cloud(self):
426 """
427 Get the name of the cloud that this controller lives on.
428 """
Adam Israelb8a82812019-03-27 14:50:11 -0400429 cloud_facade = client.CloudFacade.from_connection(self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400430
431 result = await cloud_facade.Clouds()
432 cloud = list(result.clouds.keys())[0] # only lives on one cloud
433 return tag.untag('cloud-', cloud)
434
435 async def get_models(self, all_=False, username=None):
Adam Israelb8a82812019-03-27 14:50:11 -0400436 """
437 .. deprecated:: 0.7.0
438 Use :meth:`.list_models` instead.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400439 """
440 controller_facade = client.ControllerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400441 self.connection())
442 for attempt in (1, 2, 3):
443 try:
444 return await controller_facade.AllModels()
445 except errors.JujuAPIError as e:
446 # retry concurrency error until resolved in Juju
447 # see: https://bugs.launchpad.net/juju/+bug/1721786
448 if 'has been removed' not in e.message or attempt == 3:
449 raise
450
451 async def model_uuids(self):
452 """Return a mapping of model names to UUIDs.
453 """
454 controller_facade = client.ControllerFacade.from_connection(
455 self.connection())
456 for attempt in (1, 2, 3):
457 try:
458 response = await controller_facade.AllModels()
459 return {um.model.name: um.model.uuid
460 for um in response.user_models}
461 except errors.JujuAPIError as e:
462 # retry concurrency error until resolved in Juju
463 # see: https://bugs.launchpad.net/juju/+bug/1721786
464 if 'has been removed' not in e.message or attempt == 3:
465 raise
466 await asyncio.sleep(attempt, loop=self._connector.loop)
467
468 async def list_models(self):
469 """Return list of names of the available models on this controller.
470
471 Equivalent to ``sorted((await self.model_uuids()).keys())``
472 """
473 uuids = await self.model_uuids()
474 return sorted(uuids.keys())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400475
476 def get_payloads(self, *patterns):
477 """Return list of known payloads.
478
Adam Israelb8a82812019-03-27 14:50:11 -0400479 :param str *patterns: Patterns to match against
Adam Israeldcdf82b2017-08-15 15:26:43 -0400480
481 Each pattern will be checked against the following info in Juju::
482
483 - unit name
484 - machine id
485 - payload type
486 - payload class
487 - payload id
488 - payload tag
489 - payload status
490
491 """
492 raise NotImplementedError()
493
Adam Israeldcdf82b2017-08-15 15:26:43 -0400494 def login(self):
495 """Log in to this controller.
496
497 """
498 raise NotImplementedError()
499
500 def logout(self, force=False):
501 """Log out of this controller.
502
503 :param bool force: Don't fail even if user not previously logged in
504 with a password
505
506 """
507 raise NotImplementedError()
508
Adam Israelb8a82812019-03-27 14:50:11 -0400509 async def get_model(self, model):
510 """Get a model by name or UUID.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400511
Adam Israelb8a82812019-03-27 14:50:11 -0400512 :param str model: Model name or UUID
513 :returns Model: Connected Model instance.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400514 """
Adam Israelb8a82812019-03-27 14:50:11 -0400515 uuids = await self.model_uuids()
516 if model in uuids:
517 uuid = uuids[model]
518 else:
519 uuid = model
Adam Israeldcdf82b2017-08-15 15:26:43 -0400520
Adam Israelb8a82812019-03-27 14:50:11 -0400521 from juju.model import Model
522 model = Model()
523 kwargs = self.connection().connect_params()
524 kwargs['uuid'] = uuid
525 await model._connect_direct(**kwargs)
526 return model
527
528 async def get_user(self, username, secret_key=None):
Adam Israeldcdf82b2017-08-15 15:26:43 -0400529 """Get a user by name.
530
531 :param str username: Username
Adam Israelb8a82812019-03-27 14:50:11 -0400532 :param str secret_key: Issued by juju when add or reset user
533 password
534 :returns: A :class:`~juju.user.User` instance
Adam Israeldcdf82b2017-08-15 15:26:43 -0400535 """
536 client_facade = client.UserManagerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400537 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400538 user = tag.user(username)
Adam Israelb8a82812019-03-27 14:50:11 -0400539 args = [client.Entity(user)]
540 try:
541 response = await client_facade.UserInfo(args, True)
542 except errors.JujuError as e:
543 if 'permission denied' in e.errors:
544 # apparently, trying to get info for a nonexistent user returns
545 # a "permission denied" error rather than an empty result set
546 return None
547 raise
548 if response.results and response.results[0].result:
549 return User(self, response.results[0].result, secret_key=secret_key)
550 return None
551
552 async def get_users(self, include_disabled=False):
553 """Return list of users that can connect to this controller.
554
555 :param bool include_disabled: Include disabled users
556 :returns: A list of :class:`~juju.user.User` instances
557 """
558 client_facade = client.UserManagerFacade.from_connection(
559 self.connection())
560 response = await client_facade.UserInfo(None, include_disabled)
561 return [User(self, r.result) for r in response.results]
Adam Israeldcdf82b2017-08-15 15:26:43 -0400562
563 async def grant(self, username, acl='login'):
Adam Israelb8a82812019-03-27 14:50:11 -0400564 """Grant access level of the given user on the controller.
565 Note that if the user already has higher permissions than the
566 provided ACL, this will do nothing (see revoke for a way to
567 remove permissions).
Adam Israeldcdf82b2017-08-15 15:26:43 -0400568 :param str username: Username
569 :param str acl: Access control ('login', 'add-model' or 'superuser')
Adam Israelb8a82812019-03-27 14:50:11 -0400570 :returns: True if new access was granted, False if user already had
571 requested access or greater. Raises JujuError if failed.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400572 """
573 controller_facade = client.ControllerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400574 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400575 user = tag.user(username)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400576 changes = client.ModifyControllerAccess(acl, 'grant', user)
Adam Israelb8a82812019-03-27 14:50:11 -0400577 try:
578 await controller_facade.ModifyControllerAccess([changes])
579 return True
580 except errors.JujuError as e:
581 if 'user already has' in str(e):
582 return False
583 else:
584 raise
Adam Israeldcdf82b2017-08-15 15:26:43 -0400585
Adam Israelb8a82812019-03-27 14:50:11 -0400586 async def revoke(self, username, acl='login'):
587 """Removes some or all access of a user to from a controller
588 If 'login' access is revoked, the user will no longer have any
589 permissions on the controller. Revoking a higher privilege from
590 a user without that privilege will have no effect.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400591
592 :param str username: username
Adam Israelb8a82812019-03-27 14:50:11 -0400593 :param str acl: Access to remove ('login', 'add-model' or 'superuser')
Adam Israeldcdf82b2017-08-15 15:26:43 -0400594 """
595 controller_facade = client.ControllerFacade.from_connection(
Adam Israelb8a82812019-03-27 14:50:11 -0400596 self.connection())
Adam Israeldcdf82b2017-08-15 15:26:43 -0400597 user = tag.user(username)
598 changes = client.ModifyControllerAccess('login', 'revoke', user)
599 return await controller_facade.ModifyControllerAccess([changes])
Adam Israelb8a82812019-03-27 14:50:11 -0400600
601 async def grant_model(self, username, model_uuid, acl='read'):
602 """Grant a user access to a model. Note that if the user
603 already has higher permissions than the provided ACL,
604 this will do nothing (see revoke_model for a way to remove
605 permissions).
606
607 :param str username: Username
608 :param str model_uuid: The UUID of the model to change.
609 :param str acl: Access control ('read, 'write' or 'admin')
610 """
611 model_facade = client.ModelManagerFacade.from_connection(
612 self.connection())
613 user = tag.user(username)
614 model = tag.model(model_uuid)
615 changes = client.ModifyModelAccess(acl, 'grant', model, user)
616 return await model_facade.ModifyModelAccess([changes])
617
618 async def revoke_model(self, username, model_uuid, acl='read'):
619 """Revoke some or all of a user's access to a model.
620 If 'read' access is revoked, the user will no longer have any
621 permissions on the model. Revoking a higher privilege from
622 a user without that privilege will have no effect.
623
624 :param str username: Username to revoke
625 :param str model_uuid: The UUID of the model to change.
626 :param str acl: Access control ('read, 'write' or 'admin')
627 """
628 model_facade = client.ModelManagerFacade.from_connection(
629 self.connection())
630 user = tag.user(username)
631 model = tag.model(model_uuid)
632 changes = client.ModifyModelAccess(acl, 'revoke', model, user)
633 return await model_facade.ModifyModelAccess([changes])