957ab85da834ff43e60e2993e614303ba784eca2
[osm/N2VC.git] / modules / libjuju / juju / controller.py
1 import asyncio
2 import logging
3
4 from . import errors, tag, utils
5 from .client import client, connector
6 from .user import User
7
8 log = logging.getLogger(__name__)
9
10
11 class Controller:
12 def __init__(
13 self,
14 loop=None,
15 max_frame_size=None,
16 bakery_client=None,
17 jujudata=None,
18 ):
19 """Instantiate a new Controller.
20
21 One of the connect_* methods will need to be called before this
22 object can be used for anything interesting.
23
24 If jujudata is None, jujudata.FileJujuData will be used.
25
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.
32 """
33 self._connector = connector.Connector(
34 loop=loop,
35 max_frame_size=max_frame_size,
36 bakery_client=bakery_client,
37 jujudata=jujudata,
38 )
39
40 async def __aenter__(self):
41 await self.connect()
42 return self
43
44 async def __aexit__(self, exc_type, exc, tb):
45 await self.disconnect()
46
47 @property
48 def loop(self):
49 return self._connector.loop
50
51 async def connect(self, controller_name=None, **kwargs):
52 """Connect to a Juju controller.
53
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
58 be absent or None).
59
60 Otherwise, if controller_name is None, connect to the
61 current controller.
62
63 Otherwise, controller_name must specify the name
64 of a known controller.
65 """
66 await self.disconnect()
67 if not kwargs:
68 await self._connector.connect_controller(controller_name)
69 else:
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)
76
77 async def _connect_direct(self, **kwargs):
78 await self.disconnect()
79 await self._connector.connect(**kwargs)
80
81 def is_connected(self):
82 """Reports whether the Controller is currently connected."""
83 return self._connector.is_connected()
84
85 def connection(self):
86 """Return the current Connection object. It raises an exception
87 if the Controller is disconnected"""
88 return self._connector.connection()
89
90 @property
91 def controller_name(self):
92 return self._connector.controller_name
93
94 async def disconnect(self):
95 """Shut down the watcher task and close websockets.
96
97 """
98 await self._connector.disconnect()
99
100 async def add_credential(self, name=None, credential=None, cloud=None,
101 owner=None):
102 """Add or update a credential to the controller.
103
104 :param str name: Name of new credential. If None, the default
105 local credential is used. Name must be provided if a credential
106 is given.
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
112 the current user.
113 :returns: Name of credential that was uploaded.
114 """
115 if not cloud:
116 cloud = await self.get_cloud()
117
118 if not owner:
119 owner = self.connection().info['user-info']['identity']
120
121 if credential and not name:
122 raise errors.JujuError('Name must be provided for credential')
123
124 if not 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))
129
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,
136 )])
137
138 return name
139
140 async def add_model(
141 self, model_name, cloud_name=None, credential_name=None,
142 owner=None, config=None, region=None):
143 """Add a model to this controller.
144
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
150 default credential.
151 :param str owner: Username that will own the model. Defaults to
152 the current user.
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.
156 """
157 model_facade = client.ModelManagerFacade.from_connection(
158 self.connection())
159
160 owner = owner or self.connection().info['user-info']['identity']
161 cloud_name = cloud_name or await self.get_cloud()
162
163 try:
164 # attempt to add/update the credential from local data if available
165 credential_name = await self.add_credential(
166 name=credential_name,
167 cloud=cloud_name,
168 owner=owner)
169 except errors.JujuError:
170 # if it's not available locally, assume it's on the controller
171 pass
172
173 if credential_name:
174 credential = tag.credential(
175 cloud_name,
176 tag.untag('user-', owner),
177 credential_name
178 )
179 else:
180 credential = None
181
182 log.debug('Creating model %s', model_name)
183
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)
188
189 model_info = await model_facade.CreateModel(
190 tag.cloud(cloud_name),
191 config,
192 credential,
193 model_name,
194 owner,
195 region
196 )
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)
202
203 return model
204
205 async def destroy_models(self, *models):
206 """Destroy one or more models.
207
208 :param str \*models: Names or UUIDs of models to destroy
209
210 """
211 uuids = await self.model_uuids()
212 models = [uuids[model] if model in uuids else model
213 for model in models]
214
215 model_facade = client.ModelManagerFacade.from_connection(
216 self.connection())
217
218 log.debug(
219 'Destroying model%s %s',
220 '' if len(models) == 1 else 's',
221 ', '.join(models)
222 )
223
224 await model_facade.DestroyModels([
225 client.Entity(tag.model(model))
226 for model in models
227 ])
228 destroy_model = destroy_models
229
230 async def add_user(self, username, password=None, display_name=None):
231 """Add a user to this controller.
232
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
237 """
238 if not display_name:
239 display_name = username
240 user_facade = client.UserManagerFacade.from_connection(self.connection())
241 users = [client.AddUser(display_name=display_name,
242 username=username,
243 password=password)]
244 await user_facade.AddUser(users)
245 return await self.get_user(username)
246
247 async def remove_user(self, username):
248 """Remove a user from this controller.
249 """
250 client_facade = client.UserManagerFacade.from_connection(
251 self.connection())
252 user = tag.user(username)
253 await client_facade.RemoveUser([client.Entity(user)])
254
255 async def change_user_password(self, username, password):
256 """Change the password for a user in this controller.
257
258 :param str username: Username
259 :param str password: New password
260
261 """
262 user_facade = client.UserManagerFacade.from_connection(self.connection())
263 entity = client.EntityPassword(password, tag.user(username))
264 return await user_facade.SetPassword([entity])
265
266 async def destroy(self, destroy_all_models=False):
267 """Destroy this controller.
268
269 :param bool destroy_all_models: Destroy all hosted models in the
270 controller.
271
272 """
273 controller_facade = client.ControllerFacade.from_connection(
274 self.connection())
275 return await controller_facade.DestroyController(destroy_all_models)
276
277 async def disable_user(self, username):
278 """Disable a user.
279
280 :param str username: Username
281
282 """
283 user_facade = client.UserManagerFacade.from_connection(self.connection())
284 entity = client.Entity(tag.user(username))
285 return await user_facade.DisableUser([entity])
286
287 async def enable_user(self, username):
288 """Re-enable a previously disabled user.
289
290 """
291 user_facade = client.UserManagerFacade.from_connection(self.connection())
292 entity = client.Entity(tag.user(username))
293 return await user_facade.EnableUser([entity])
294
295 def kill(self):
296 """Forcibly terminate all machines and other associated resources for
297 this controller.
298
299 """
300 raise NotImplementedError()
301
302 async def get_cloud(self):
303 """
304 Get the name of the cloud that this controller lives on.
305 """
306 cloud_facade = client.CloudFacade.from_connection(self.connection())
307
308 result = await cloud_facade.Clouds()
309 cloud = list(result.clouds.keys())[0] # only lives on one cloud
310 return tag.untag('cloud-', cloud)
311
312 async def get_models(self, all_=False, username=None):
313 """
314 .. deprecated:: 0.7.0
315 Use :meth:`.list_models` instead.
316 """
317 controller_facade = client.ControllerFacade.from_connection(
318 self.connection)
319 for attempt in (1, 2, 3):
320 try:
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:
326 raise
327
328 async def model_uuids(self):
329 """Return a mapping of model names to UUIDs.
330 """
331 controller_facade = client.ControllerFacade.from_connection(
332 self.connection())
333 for attempt in (1, 2, 3):
334 try:
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:
342 raise
343 await asyncio.sleep(attempt, loop=self._connector.loop)
344
345 async def list_models(self):
346 """Return list of names of the available models on this controller.
347
348 Equivalent to ``sorted((await self.model_uuids()).keys())``
349 """
350 uuids = await self.model_uuids()
351 return sorted(uuids.keys())
352
353 def get_payloads(self, *patterns):
354 """Return list of known payloads.
355
356 :param str \*patterns: Patterns to match against
357
358 Each pattern will be checked against the following info in Juju::
359
360 - unit name
361 - machine id
362 - payload type
363 - payload class
364 - payload id
365 - payload tag
366 - payload status
367
368 """
369 raise NotImplementedError()
370
371 def login(self):
372 """Log in to this controller.
373
374 """
375 raise NotImplementedError()
376
377 def logout(self, force=False):
378 """Log out of this controller.
379
380 :param bool force: Don't fail even if user not previously logged in
381 with a password
382
383 """
384 raise NotImplementedError()
385
386 async def get_model(self, model):
387 """Get a model by name or UUID.
388
389 :param str model: Model name or UUID
390 :returns Model: Connected Model instance.
391 """
392 uuids = await self.model_uuids()
393 if model in uuids:
394 uuid = uuids[model]
395 else:
396 uuid = model
397
398 from juju.model import Model
399 model = Model()
400 kwargs = self.connection().connect_params()
401 kwargs['uuid'] = uuid
402 await model._connect_direct(**kwargs)
403 return model
404
405 async def get_user(self, username):
406 """Get a user by name.
407
408 :param str username: Username
409 :returns: A :class:`~juju.user.User` instance
410 """
411 client_facade = client.UserManagerFacade.from_connection(
412 self.connection())
413 user = tag.user(username)
414 args = [client.Entity(user)]
415 try:
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
421 return None
422 raise
423 if response.results and response.results[0].result:
424 return User(self, response.results[0].result)
425 return None
426
427 async def get_users(self, include_disabled=False):
428 """Return list of users that can connect to this controller.
429
430 :param bool include_disabled: Include disabled users
431 :returns: A list of :class:`~juju.user.User` instances
432 """
433 client_facade = client.UserManagerFacade.from_connection(
434 self.connection())
435 response = await client_facade.UserInfo(None, include_disabled)
436 return [User(self, r.result) for r in response.results]
437
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
442 remove permissions).
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.
447 """
448 controller_facade = client.ControllerFacade.from_connection(
449 self.connection())
450 user = tag.user(username)
451 changes = client.ModifyControllerAccess(acl, 'grant', user)
452 try:
453 await controller_facade.ModifyControllerAccess([changes])
454 return True
455 except errors.JujuError as e:
456 if 'user already has' in str(e):
457 return False
458 else:
459 raise
460
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.
466
467 :param str username: username
468 :param str acl: Access to remove ('login', 'add-model' or 'superuser')
469 """
470 controller_facade = client.ControllerFacade.from_connection(
471 self.connection())
472 user = tag.user(username)
473 changes = client.ModifyControllerAccess('login', 'revoke', user)
474 return await controller_facade.ModifyControllerAccess([changes])
475
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).
480
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')
484 """
485 model_facade = client.ModelManagerFacade.from_connection(
486 self.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])
491
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.
497
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')
501 """
502 model_facade = client.ModelManagerFacade.from_connection(
503 self.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])