55ea55e979f80387b475eced23fd5e36d715ed5b
7 from .client
import client
8 from .client
import connection
9 from .model
import Model
10 from .user
import User
12 log
= logging
.getLogger(__name__
)
15 class Controller(object):
16 def __init__(self
, loop
=None,
17 max_frame_size
=connection
.Connection
.DEFAULT_FRAME_SIZE
):
18 """Instantiate a new Controller.
20 One of the connect_* methods will need to be called before this
21 object can be used for anything interesting.
23 :param loop: an asyncio event loop
26 self
.loop
= loop
or asyncio
.get_event_loop()
27 self
.max_frame_size
= None
28 self
.connection
= None
29 self
.controller_name
= None
32 self
, endpoint
, username
, password
, cacert
=None, macaroons
=None):
33 """Connect to an arbitrary Juju controller.
36 self
.connection
= await connection
.Connection
.connect(
37 endpoint
, None, username
, password
, cacert
, macaroons
,
38 max_frame_size
=self
.max_frame_size
)
40 async def connect_current(self
):
41 """Connect to the current Juju controller.
44 jujudata
= connection
.JujuData()
45 controller_name
= jujudata
.current_controller()
46 if not controller_name
:
47 raise errors
.JujuConnectionError('No current controller')
48 return await self
.connect_controller(controller_name
)
50 async def connect_controller(self
, controller_name
):
51 """Connect to a Juju controller by name.
55 await connection
.Connection
.connect_controller(
56 controller_name
, max_frame_size
=self
.max_frame_size
))
57 self
.controller_name
= controller_name
59 async def disconnect(self
):
60 """Shut down the watcher task and close websockets.
63 if self
.connection
and self
.connection
.is_open
:
64 log
.debug('Closing controller connection')
65 await self
.connection
.close()
66 self
.connection
= None
68 async def add_credential(self
, name
=None, credential
=None, cloud
=None,
70 """Add or update a credential to the controller.
72 :param str name: Name of new credential. If None, the default
73 local credential is used. Name must be provided if a credential
75 :param CloudCredential credential: Credential to add. If not given,
76 it will attempt to read from local data, if available.
77 :param str cloud: Name of cloud to associate the credential with.
78 Defaults to the same cloud as the controller.
79 :param str owner: Username that will own the credential. Defaults to
81 :returns: Name of credential that was uploaded.
84 cloud
= await self
.get_cloud()
87 owner
= self
.connection
.info
['user-info']['identity']
89 if credential
and not name
:
90 raise errors
.JujuError('Name must be provided for credential')
93 name
, credential
= connection
.JujuData().load_credential(cloud
,
95 if credential
is None:
96 raise errors
.JujuError('Unable to find credential: '
99 log
.debug('Uploading credential %s', name
)
100 cloud_facade
= client
.CloudFacade
.from_connection(self
.connection
)
101 await cloud_facade
.UpdateCredentials([
102 client
.UpdateCloudCredential(
103 tag
=tag
.credential(cloud
, tag
.untag('user-', owner
), name
),
104 credential
=credential
,
110 self
, model_name
, cloud_name
=None, credential_name
=None,
111 owner
=None, config
=None, region
=None):
112 """Add a model to this controller.
114 :param str model_name: Name to give the new model.
115 :param str cloud_name: Name of the cloud in which to create the
116 model, e.g. 'aws'. Defaults to same cloud as controller.
117 :param str credential_name: Name of the credential to use when
118 creating the model. If not given, it will attempt to find a
120 :param str owner: Username that will own the model. Defaults to
122 :param dict config: Model configuration.
123 :param str region: Region in which to create the model.
126 model_facade
= client
.ModelManagerFacade
.from_connection(
129 owner
= owner
or self
.connection
.info
['user-info']['identity']
130 cloud_name
= cloud_name
or await self
.get_cloud()
133 # attempt to add/update the credential from local data if available
134 credential_name
= await self
.add_credential(
135 name
=credential_name
,
138 except errors
.JujuError
:
139 # if it's not available locally, assume it's on the controller
143 credential
= tag
.credential(
145 tag
.untag('user-', owner
),
151 log
.debug('Creating model %s', model_name
)
153 if not config
or 'authorized-keys' not in config
:
154 config
= config
or {}
155 config
['authorized-keys'] = await utils
.read_ssh_key(
158 model_info
= await model_facade
.CreateModel(
159 tag
.cloud(cloud_name
),
169 self
.connection
.endpoint
,
171 self
.connection
.username
,
172 self
.connection
.password
,
173 self
.connection
.cacert
,
174 self
.connection
.macaroons
,
180 async def destroy_models(self
, *models
):
181 """Destroy one or more models.
183 :param str \*models: Names or UUIDs of models to destroy
186 uuids
= await self
._model
_uuids
()
187 models
= [uuids
[model
] if model
in uuids
else model
190 model_facade
= client
.ModelManagerFacade
.from_connection(
194 'Destroying model%s %s',
195 '' if len(models
) == 1 else 's',
199 await model_facade
.DestroyModels([
200 client
.Entity(tag
.model(model
))
203 destroy_model
= destroy_models
205 async def add_user(self
, username
, password
=None, display_name
=None):
206 """Add a user to this controller.
208 :param str username: Username
209 :param str password: Password
210 :param str display_name: Display name
211 :returns: A :class:`~juju.user.User` instance
214 display_name
= username
215 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection
)
216 users
= [client
.AddUser(display_name
=display_name
,
219 await user_facade
.AddUser(users
)
220 return await self
.get_user(username
)
222 async def remove_user(self
, username
):
223 """Remove a user from this controller.
225 client_facade
= client
.UserManagerFacade
.from_connection(
227 user
= tag
.user(username
)
228 await client_facade
.RemoveUser([client
.Entity(user
)])
230 async def change_user_password(self
, username
, password
):
231 """Change the password for a user in this controller.
233 :param str username: Username
234 :param str password: New password
237 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection
)
238 entity
= client
.EntityPassword(password
, tag
.user(username
))
239 return await user_facade
.SetPassword([entity
])
241 async def destroy(self
, destroy_all_models
=False):
242 """Destroy this controller.
244 :param bool destroy_all_models: Destroy all hosted models in the
248 controller_facade
= client
.ControllerFacade
.from_connection(
250 return await controller_facade
.DestroyController(destroy_all_models
)
252 async def disable_user(self
, username
):
255 :param str username: Username
258 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection
)
259 entity
= client
.Entity(tag
.user(username
))
260 return await user_facade
.DisableUser([entity
])
262 async def enable_user(self
, username
):
263 """Re-enable a previously disabled user.
266 user_facade
= client
.UserManagerFacade
.from_connection(self
.connection
)
267 entity
= client
.Entity(tag
.user(username
))
268 return await user_facade
.EnableUser([entity
])
271 """Forcibly terminate all machines and other associated resources for
275 raise NotImplementedError()
277 async def get_cloud(self
):
279 Get the name of the cloud that this controller lives on.
281 cloud_facade
= client
.CloudFacade
.from_connection(self
.connection
)
283 result
= await cloud_facade
.Clouds()
284 cloud
= list(result
.clouds
.keys())[0] # only lives on one cloud
285 return tag
.untag('cloud-', cloud
)
287 async def _model_uuids(self
, all_
=False, username
=None):
288 controller_facade
= client
.ControllerFacade
.from_connection(
290 for attempt
in (1, 2, 3):
292 response
= await controller_facade
.AllModels()
293 return {um
.model
.name
: um
.model
.uuid
294 for um
in response
.user_models
}
295 except errors
.JujuAPIError
as e
:
296 # retry concurrency error until resolved in Juju
297 # see: https://bugs.launchpad.net/juju/+bug/1721786
298 if 'has been removed' not in e
.message
or attempt
== 3:
300 await asyncio
.sleep(attempt
, loop
=self
.loop
)
302 async def list_models(self
, all_
=False, username
=None):
303 """Return list of names of the available models on this controller.
305 :param bool all_: List all models, regardless of user accessibilty
307 :param str username: User for which to list models (admin use only)
310 uuids
= await self
._model
_uuids
(all_
, username
)
311 return sorted(uuids
.keys())
313 def get_payloads(self
, *patterns
):
314 """Return list of known payloads.
316 :param str \*patterns: Patterns to match against
318 Each pattern will be checked against the following info in Juju::
329 raise NotImplementedError()
332 """Log in to this controller.
335 raise NotImplementedError()
337 def logout(self
, force
=False):
338 """Log out of this controller.
340 :param bool force: Don't fail even if user not previously logged in
344 raise NotImplementedError()
346 async def get_model(self
, model
):
347 """Get a model by name or UUID.
349 :param str model: Model name or UUID
352 uuids
= await self
._model
_uuids
()
354 name_or_uuid
= uuids
[model
]
360 self
.connection
.endpoint
,
362 self
.connection
.username
,
363 self
.connection
.password
,
364 self
.connection
.cacert
,
365 self
.connection
.macaroons
,
370 async def get_user(self
, username
):
371 """Get a user by name.
373 :param str username: Username
374 :returns: A :class:`~juju.user.User` instance
376 client_facade
= client
.UserManagerFacade
.from_connection(
378 user
= tag
.user(username
)
379 args
= [client
.Entity(user
)]
381 response
= await client_facade
.UserInfo(args
, True)
382 except errors
.JujuError
as e
:
383 if 'permission denied' in e
.errors
:
384 # apparently, trying to get info for a nonexistent user returns
385 # a "permission denied" error rather than an empty result set
388 if response
.results
and response
.results
[0].result
:
389 return User(self
, response
.results
[0].result
)
392 async def get_users(self
, include_disabled
=False):
393 """Return list of users that can connect to this controller.
395 :param bool include_disabled: Include disabled users
396 :returns: A list of :class:`~juju.user.User` instances
398 client_facade
= client
.UserManagerFacade
.from_connection(
400 response
= await client_facade
.UserInfo(None, include_disabled
)
401 return [User(self
, r
.result
) for r
in response
.results
]
403 async def grant(self
, username
, acl
='login'):
404 """Set access level of the given user on the controller
406 :param str username: Username
407 :param str acl: Access control ('login', 'add-model' or 'superuser')
410 controller_facade
= client
.ControllerFacade
.from_connection(
412 user
= tag
.user(username
)
413 await self
.revoke(username
)
414 changes
= client
.ModifyControllerAccess(acl
, 'grant', user
)
415 return await controller_facade
.ModifyControllerAccess([changes
])
417 async def revoke(self
, username
):
418 """Removes all access from a controller
420 :param str username: username
423 controller_facade
= client
.ControllerFacade
.from_connection(
425 user
= tag
.user(username
)
426 changes
= client
.ModifyControllerAccess('login', 'revoke', user
)
427 return await controller_facade
.ModifyControllerAccess([changes
])