4 from . import errors
, tag
, utils
5 from .client
import client
, connector
8 log
= logging
.getLogger(__name__
)
19 """Instantiate a new Controller.
21 One of the connect_* methods will need to be called before this
22 object can be used for anything interesting.
24 If jujudata is None, jujudata.FileJujuData will be used.
26 :param loop: an asyncio event loop
27 :param max_frame_size: See
28 `juju.client.connection.Connection.MAX_FRAME_SIZE`
29 :param bakery_client httpbakery.Client: The bakery client to use
30 for macaroon authorization.
31 :param jujudata JujuData: The source for current controller information.
33 self
._connector
= connector
.Connector(
35 max_frame_size
=max_frame_size
,
36 bakery_client
=bakery_client
,
40 async def __aenter__(self
):
44 async def __aexit__(self
, exc_type
, exc
, tb
):
45 await self
.disconnect()
49 return self
._connector
.loop
51 async def connect(self
, controller_name
=None, **kwargs
):
52 """Connect to a Juju controller.
54 If any arguments are specified other than controller_name,
55 then controller_name must be None and an explicit
56 connection will be made using Connection.connect
57 using those parameters (the 'uuid' parameter must
60 Otherwise, if controller_name is None, connect to the
63 Otherwise, controller_name must specify the name
64 of a known controller.
66 await self
.disconnect()
68 await self
._connector
.connect_controller(controller_name
)
70 if controller_name
is not None:
71 raise ValueError('controller name may not be specified with other connect parameters')
72 if kwargs
.get('uuid') is not None:
73 # A UUID implies a model connection, not a controller connection.
74 raise ValueError('model UUID specified when connecting to controller')
75 await self
._connector
.connect(**kwargs
)
77 async def _connect_direct(self
, **kwargs
):
78 await self
.disconnect()
79 await self
._connector
.connect(**kwargs
)
81 def is_connected(self
):
82 """Reports whether the Controller is currently connected."""
83 return self
._connector
.is_connected()
86 """Return the current Connection object. It raises an exception
87 if the Controller is disconnected"""
88 return self
._connector
.connection()
91 def controller_name(self
):
92 return self
._connector
.controller_name
94 async def disconnect(self
):
95 """Shut down the watcher task and close websockets.
98 await self
._connector
.disconnect()
100 async def add_credential(self
, name
=None, credential
=None, cloud
=None,
102 """Add or update a credential to the controller.
104 :param str name: Name of new credential. If None, the default
105 local credential is used. Name must be provided if a credential
107 :param CloudCredential credential: Credential to add. If not given,
108 it will attempt to read from local data, if available.
109 :param str cloud: Name of cloud to associate the credential with.
110 Defaults to the same cloud as the controller.
111 :param str owner: Username that will own the credential. Defaults to
113 :returns: Name of credential that was uploaded.
116 cloud
= await self
.get_cloud()
119 owner
= self
.connection().info
['user-info']['identity']
121 if credential
and not name
:
122 raise errors
.JujuError('Name must be provided for credential')
125 name
, credential
= self
._connector
.jujudata
.load_credential(cloud
, name
)
126 if credential
is None:
127 raise errors
.JujuError(
128 'Unable to find credential: {}'.format(name
))
130 log
.debug('Uploading credential %s', name
)
131 cloud_facade
= client
.CloudFacade
.from_connection(self
.connection())
132 await cloud_facade
.UpdateCredentials([
133 client
.UpdateCloudCredential(
134 tag
=tag
.credential(cloud
, tag
.untag('user-', owner
), name
),
135 credential
=credential
,
141 self
, model_name
, cloud_name
=None, credential_name
=None,
142 owner
=None, config
=None, region
=None):
143 """Add a model to this controller.
145 :param str model_name: Name to give the new model.
146 :param str cloud_name: Name of the cloud in which to create the
147 model, e.g. 'aws'. Defaults to same cloud as controller.
148 :param str credential_name: Name of the credential to use when
149 creating the model. If not given, it will attempt to find a
151 :param str owner: Username that will own the model. Defaults to
153 :param dict config: Model configuration.
154 :param str region: Region in which to create the model.
155 :return Model: A connection to the newly created model.
157 model_facade
= client
.ModelManagerFacade
.from_connection(
160 owner
= owner
or self
.connection().info
['user-info']['identity']
161 cloud_name
= cloud_name
or await self
.get_cloud()
164 # attempt to add/update the credential from local data if available
165 credential_name
= await self
.add_credential(
166 name
=credential_name
,
169 except errors
.JujuError
:
170 # if it's not available locally, assume it's on the controller
174 credential
= tag
.credential(
176 tag
.untag('user-', owner
),
182 log
.debug('Creating model %s', model_name
)
184 if not config
or 'authorized-keys' not in config
:
185 config
= config
or {}
186 config
['authorized-keys'] = await utils
.read_ssh_key(
187 loop
=self
._connector
.loop
)
189 model_info
= await model_facade
.CreateModel(
190 tag
.cloud(cloud_name
),
197 from juju
.model
import Model
198 model
= Model(jujudata
=self
._connector
.jujudata
)
199 kwargs
= self
.connection().connect_params()
200 kwargs
['uuid'] = model_info
.uuid
201 await model
._connect
_direct
(**kwargs
)
205 async def destroy_models(self
, *models
):
206 """Destroy one or more models.
208 :param str \*models: Names or UUIDs of models to destroy
211 uuids
= await self
.model_uuids()
212 models
= [uuids
[model
] if model
in uuids
else model
215 model_facade
= client
.ModelManagerFacade
.from_connection(
219 'Destroying model%s %s',
220 '' if len(models
) == 1 else 's',
224 await model_facade
.DestroyModels([
225 client
.Entity(tag
.model(model
))
228 destroy_model
= destroy_models
230 async def add_user(self
, username
, password
=None, display_name
=None):
231 """Add a user to this controller.
233 :param str username: Username
234 :param str password: Password
235 :param str display_name: Display name
236 :returns: A :class:`~juju.user.User` instance
239 display_name
= username
240 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection())
241 users
= [client
.AddUser(display_name
=display_name
,
244 await user_facade
.AddUser(users
)
245 return await self
.get_user(username
)
247 async def remove_user(self
, username
):
248 """Remove a user from this controller.
250 client_facade
= client
.UserManagerFacade
.from_connection(
252 user
= tag
.user(username
)
253 await client_facade
.RemoveUser([client
.Entity(user
)])
255 async def change_user_password(self
, username
, password
):
256 """Change the password for a user in this controller.
258 :param str username: Username
259 :param str password: New password
262 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection())
263 entity
= client
.EntityPassword(password
, tag
.user(username
))
264 return await user_facade
.SetPassword([entity
])
266 async def destroy(self
, destroy_all_models
=False):
267 """Destroy this controller.
269 :param bool destroy_all_models: Destroy all hosted models in the
273 controller_facade
= client
.ControllerFacade
.from_connection(
275 return await controller_facade
.DestroyController(destroy_all_models
)
277 async def disable_user(self
, username
):
280 :param str username: Username
283 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection())
284 entity
= client
.Entity(tag
.user(username
))
285 return await user_facade
.DisableUser([entity
])
287 async def enable_user(self
, username
):
288 """Re-enable a previously disabled user.
291 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection())
292 entity
= client
.Entity(tag
.user(username
))
293 return await user_facade
.EnableUser([entity
])
296 """Forcibly terminate all machines and other associated resources for
300 raise NotImplementedError()
302 async def get_cloud(self
):
304 Get the name of the cloud that this controller lives on.
306 cloud_facade
= client
.CloudFacade
.from_connection(self
.connection())
308 result
= await cloud_facade
.Clouds()
309 cloud
= list(result
.clouds
.keys())[0] # only lives on one cloud
310 return tag
.untag('cloud-', cloud
)
312 async def get_models(self
, all_
=False, username
=None):
314 .. deprecated:: 0.7.0
315 Use :meth:`.list_models` instead.
317 controller_facade
= client
.ControllerFacade
.from_connection(
319 for attempt
in (1, 2, 3):
321 return await controller_facade
.AllModels()
322 except errors
.JujuAPIError
as e
:
323 # retry concurrency error until resolved in Juju
324 # see: https://bugs.launchpad.net/juju/+bug/1721786
325 if 'has been removed' not in e
.message
or attempt
== 3:
328 async def model_uuids(self
):
329 """Return a mapping of model names to UUIDs.
331 controller_facade
= client
.ControllerFacade
.from_connection(
333 for attempt
in (1, 2, 3):
335 response
= await controller_facade
.AllModels()
336 return {um
.model
.name
: um
.model
.uuid
337 for um
in response
.user_models
}
338 except errors
.JujuAPIError
as e
:
339 # retry concurrency error until resolved in Juju
340 # see: https://bugs.launchpad.net/juju/+bug/1721786
341 if 'has been removed' not in e
.message
or attempt
== 3:
343 await asyncio
.sleep(attempt
, loop
=self
._connector
.loop
)
345 async def list_models(self
):
346 """Return list of names of the available models on this controller.
348 Equivalent to ``sorted((await self.model_uuids()).keys())``
350 uuids
= await self
.model_uuids()
351 return sorted(uuids
.keys())
353 def get_payloads(self
, *patterns
):
354 """Return list of known payloads.
356 :param str \*patterns: Patterns to match against
358 Each pattern will be checked against the following info in Juju::
369 raise NotImplementedError()
372 """Log in to this controller.
375 raise NotImplementedError()
377 def logout(self
, force
=False):
378 """Log out of this controller.
380 :param bool force: Don't fail even if user not previously logged in
384 raise NotImplementedError()
386 async def get_model(self
, model
):
387 """Get a model by name or UUID.
389 :param str model: Model name or UUID
390 :returns Model: Connected Model instance.
392 uuids
= await self
.model_uuids()
398 from juju
.model
import Model
400 kwargs
= self
.connection().connect_params()
401 kwargs
['uuid'] = uuid
402 await model
._connect
_direct
(**kwargs
)
405 async def get_user(self
, username
):
406 """Get a user by name.
408 :param str username: Username
409 :returns: A :class:`~juju.user.User` instance
411 client_facade
= client
.UserManagerFacade
.from_connection(
413 user
= tag
.user(username
)
414 args
= [client
.Entity(user
)]
416 response
= await client_facade
.UserInfo(args
, True)
417 except errors
.JujuError
as e
:
418 if 'permission denied' in e
.errors
:
419 # apparently, trying to get info for a nonexistent user returns
420 # a "permission denied" error rather than an empty result set
423 if response
.results
and response
.results
[0].result
:
424 return User(self
, response
.results
[0].result
)
427 async def get_users(self
, include_disabled
=False):
428 """Return list of users that can connect to this controller.
430 :param bool include_disabled: Include disabled users
431 :returns: A list of :class:`~juju.user.User` instances
433 client_facade
= client
.UserManagerFacade
.from_connection(
435 response
= await client_facade
.UserInfo(None, include_disabled
)
436 return [User(self
, r
.result
) for r
in response
.results
]
438 async def grant(self
, username
, acl
='login'):
439 """Grant access level of the given user on the controller.
440 Note that if the user already has higher permissions than the
441 provided ACL, this will do nothing (see revoke for a way to
443 :param str username: Username
444 :param str acl: Access control ('login', 'add-model' or 'superuser')
445 :returns: True if new access was granted, False if user already had
446 requested access or greater. Raises JujuError if failed.
448 controller_facade
= client
.ControllerFacade
.from_connection(
450 user
= tag
.user(username
)
451 changes
= client
.ModifyControllerAccess(acl
, 'grant', user
)
453 await controller_facade
.ModifyControllerAccess([changes
])
455 except errors
.JujuError
as e
:
456 if 'user already has' in str(e
):
461 async def revoke(self
, username
, acl
='login'):
462 """Removes some or all access of a user to from a controller
463 If 'login' access is revoked, the user will no longer have any
464 permissions on the controller. Revoking a higher privilege from
465 a user without that privilege will have no effect.
467 :param str username: username
468 :param str acl: Access to remove ('login', 'add-model' or 'superuser')
470 controller_facade
= client
.ControllerFacade
.from_connection(
472 user
= tag
.user(username
)
473 changes
= client
.ModifyControllerAccess('login', 'revoke', user
)
474 return await controller_facade
.ModifyControllerAccess([changes
])
476 async def grant_model(self
, username
, model_uuid
, acl
='read'):
477 """Grant a user access to a model. Note that if the user
478 already has higher permissions than the provided ACL,
479 this will do nothing (see revoke_model for a way to remove permissions).
481 :param str username: Username
482 :param str model_uuid: The UUID of the model to change.
483 :param str acl: Access control ('read, 'write' or 'admin')
485 model_facade
= client
.ModelManagerFacade
.from_connection(
487 user
= tag
.user(username
)
488 model
= tag
.model(model_uuid
)
489 changes
= client
.ModifyModelAccess(acl
, 'grant', model
, user
)
490 return await model_facade
.ModifyModelAccess([changes
])
492 async def revoke_model(self
, username
, model_uuid
, acl
='read'):
493 """Revoke some or all of a user's access to a model.
494 If 'read' access is revoked, the user will no longer have any
495 permissions on the model. Revoking a higher privilege from
496 a user without that privilege will have no effect.
498 :param str username: Username to revoke
499 :param str model_uuid: The UUID of the model to change.
500 :param str acl: Access control ('read, 'write' or 'admin')
502 model_facade
= client
.ModelManagerFacade
.from_connection(
504 user
= tag
.user(username
)
505 model
= tag
.model(self
.info
.uuid
)
506 changes
= client
.ModifyModelAccess(acl
, 'revoke', model
, user
)
507 return await model_facade
.ModifyModelAccess([changes
])