Update libjuju
[osm/N2VC.git] / modules / libjuju / juju / controller.py
1 import asyncio
2 import logging
3
4 from . import errors
5 from . import tag
6 from . import utils
7 from .client import client
8 from .client import connection
9 from .model import Model
10 from .user import User
11
12 log = logging.getLogger(__name__)
13
14
15 class Controller(object):
16 def __init__(self, loop=None,
17 max_frame_size=connection.Connection.DEFAULT_FRAME_SIZE):
18 """Instantiate a new Controller.
19
20 One of the connect_* methods will need to be called before this
21 object can be used for anything interesting.
22
23 :param loop: an asyncio event loop
24
25 """
26 self.loop = loop or asyncio.get_event_loop()
27 self.max_frame_size = None
28 self.connection = None
29 self.controller_name = None
30
31 async def connect(
32 self, endpoint, username, password, cacert=None, macaroons=None):
33 """Connect to an arbitrary Juju controller.
34
35 """
36 self.connection = await connection.Connection.connect(
37 endpoint, None, username, password, cacert, macaroons,
38 max_frame_size=self.max_frame_size)
39
40 async def connect_current(self):
41 """Connect to the current Juju controller.
42
43 """
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)
49
50 async def connect_controller(self, controller_name):
51 """Connect to a Juju controller by name.
52
53 """
54 self.connection = (
55 await connection.Connection.connect_controller(
56 controller_name, max_frame_size=self.max_frame_size))
57 self.controller_name = controller_name
58
59 async def disconnect(self):
60 """Shut down the watcher task and close websockets.
61
62 """
63 if self.connection and self.connection.is_open:
64 log.debug('Closing controller connection')
65 await self.connection.close()
66 self.connection = None
67
68 async def add_credential(self, name=None, credential=None, cloud=None,
69 owner=None):
70 """Add or update a credential to the controller.
71
72 :param str name: Name of new credential. If None, the default
73 local credential is used. Name must be provided if a credential
74 is given.
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
80 the current user.
81 :returns: Name of credential that was uploaded.
82 """
83 if not cloud:
84 cloud = await self.get_cloud()
85
86 if not owner:
87 owner = self.connection.info['user-info']['identity']
88
89 if credential and not name:
90 raise errors.JujuError('Name must be provided for credential')
91
92 if not credential:
93 name, credential = connection.JujuData().load_credential(cloud,
94 name)
95 if credential is None:
96 raise errors.JujuError('Unable to find credential: '
97 '{}'.format(name))
98
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,
105 )])
106
107 return name
108
109 async def add_model(
110 self, model_name, cloud_name=None, credential_name=None,
111 owner=None, config=None, region=None):
112 """Add a model to this controller.
113
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
119 default credential.
120 :param str owner: Username that will own the model. Defaults to
121 the current user.
122 :param dict config: Model configuration.
123 :param str region: Region in which to create the model.
124
125 """
126 model_facade = client.ModelManagerFacade.from_connection(
127 self.connection)
128
129 owner = owner or self.connection.info['user-info']['identity']
130 cloud_name = cloud_name or await self.get_cloud()
131
132 try:
133 # attempt to add/update the credential from local data if available
134 credential_name = await self.add_credential(
135 name=credential_name,
136 cloud=cloud_name,
137 owner=owner)
138 except errors.JujuError:
139 # if it's not available locally, assume it's on the controller
140 pass
141
142 if credential_name:
143 credential = tag.credential(
144 cloud_name,
145 tag.untag('user-', owner),
146 credential_name
147 )
148 else:
149 credential = None
150
151 log.debug('Creating model %s', model_name)
152
153 if not config or 'authorized-keys' not in config:
154 config = config or {}
155 config['authorized-keys'] = await utils.read_ssh_key(
156 loop=self.loop)
157
158 model_info = await model_facade.CreateModel(
159 tag.cloud(cloud_name),
160 config,
161 credential,
162 model_name,
163 owner,
164 region
165 )
166
167 model = Model()
168 await model.connect(
169 self.connection.endpoint,
170 model_info.uuid,
171 self.connection.username,
172 self.connection.password,
173 self.connection.cacert,
174 self.connection.macaroons,
175 loop=self.loop,
176 )
177
178 return model
179
180 async def destroy_models(self, *models):
181 """Destroy one or more models.
182
183 :param str \*models: Names or UUIDs of models to destroy
184
185 """
186 uuids = await self._model_uuids()
187 models = [uuids[model] if model in uuids else model
188 for model in models]
189
190 model_facade = client.ModelManagerFacade.from_connection(
191 self.connection)
192
193 log.debug(
194 'Destroying model%s %s',
195 '' if len(models) == 1 else 's',
196 ', '.join(models)
197 )
198
199 await model_facade.DestroyModels([
200 client.Entity(tag.model(model))
201 for model in models
202 ])
203 destroy_model = destroy_models
204
205 async def add_user(self, username, password=None, display_name=None):
206 """Add a user to this controller.
207
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
212 """
213 if not display_name:
214 display_name = username
215 user_facade = client.UserManagerFacade.from_connection(self.connection)
216 users = [client.AddUser(display_name=display_name,
217 username=username,
218 password=password)]
219 await user_facade.AddUser(users)
220 return await self.get_user(username)
221
222 async def remove_user(self, username):
223 """Remove a user from this controller.
224 """
225 client_facade = client.UserManagerFacade.from_connection(
226 self.connection)
227 user = tag.user(username)
228 await client_facade.RemoveUser([client.Entity(user)])
229
230 async def change_user_password(self, username, password):
231 """Change the password for a user in this controller.
232
233 :param str username: Username
234 :param str password: New password
235
236 """
237 user_facade = client.UserManagerFacade.from_connection(self.connection)
238 entity = client.EntityPassword(password, tag.user(username))
239 return await user_facade.SetPassword([entity])
240
241 async def destroy(self, destroy_all_models=False):
242 """Destroy this controller.
243
244 :param bool destroy_all_models: Destroy all hosted models in the
245 controller.
246
247 """
248 controller_facade = client.ControllerFacade.from_connection(
249 self.connection)
250 return await controller_facade.DestroyController(destroy_all_models)
251
252 async def disable_user(self, username):
253 """Disable a user.
254
255 :param str username: Username
256
257 """
258 user_facade = client.UserManagerFacade.from_connection(self.connection)
259 entity = client.Entity(tag.user(username))
260 return await user_facade.DisableUser([entity])
261
262 async def enable_user(self, username):
263 """Re-enable a previously disabled user.
264
265 """
266 user_facade = client.UserManagerFacade.from_connection(self.connection)
267 entity = client.Entity(tag.user(username))
268 return await user_facade.EnableUser([entity])
269
270 def kill(self):
271 """Forcibly terminate all machines and other associated resources for
272 this controller.
273
274 """
275 raise NotImplementedError()
276
277 async def get_cloud(self):
278 """
279 Get the name of the cloud that this controller lives on.
280 """
281 cloud_facade = client.CloudFacade.from_connection(self.connection)
282
283 result = await cloud_facade.Clouds()
284 cloud = list(result.clouds.keys())[0] # only lives on one cloud
285 return tag.untag('cloud-', cloud)
286
287 async def _model_uuids(self, all_=False, username=None):
288 controller_facade = client.ControllerFacade.from_connection(
289 self.connection)
290 for attempt in (1, 2, 3):
291 try:
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:
299 raise
300 await asyncio.sleep(attempt, loop=self.loop)
301
302 async def list_models(self, all_=False, username=None):
303 """Return list of names of the available models on this controller.
304
305 :param bool all_: List all models, regardless of user accessibilty
306 (admin use only)
307 :param str username: User for which to list models (admin use only)
308
309 """
310 uuids = await self._model_uuids(all_, username)
311 return sorted(uuids.keys())
312
313 def get_payloads(self, *patterns):
314 """Return list of known payloads.
315
316 :param str \*patterns: Patterns to match against
317
318 Each pattern will be checked against the following info in Juju::
319
320 - unit name
321 - machine id
322 - payload type
323 - payload class
324 - payload id
325 - payload tag
326 - payload status
327
328 """
329 raise NotImplementedError()
330
331 def login(self):
332 """Log in to this controller.
333
334 """
335 raise NotImplementedError()
336
337 def logout(self, force=False):
338 """Log out of this controller.
339
340 :param bool force: Don't fail even if user not previously logged in
341 with a password
342
343 """
344 raise NotImplementedError()
345
346 async def get_model(self, model):
347 """Get a model by name or UUID.
348
349 :param str model: Model name or UUID
350
351 """
352 uuids = await self._model_uuids()
353 if model in uuids:
354 name_or_uuid = uuids[model]
355 else:
356 name_or_uuid = model
357
358 model = Model()
359 await model.connect(
360 self.connection.endpoint,
361 name_or_uuid,
362 self.connection.username,
363 self.connection.password,
364 self.connection.cacert,
365 self.connection.macaroons,
366 loop=self.loop,
367 )
368 return model
369
370 async def get_user(self, username):
371 """Get a user by name.
372
373 :param str username: Username
374 :returns: A :class:`~juju.user.User` instance
375 """
376 client_facade = client.UserManagerFacade.from_connection(
377 self.connection)
378 user = tag.user(username)
379 args = [client.Entity(user)]
380 try:
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
386 return None
387 raise
388 if response.results and response.results[0].result:
389 return User(self, response.results[0].result)
390 return None
391
392 async def get_users(self, include_disabled=False):
393 """Return list of users that can connect to this controller.
394
395 :param bool include_disabled: Include disabled users
396 :returns: A list of :class:`~juju.user.User` instances
397 """
398 client_facade = client.UserManagerFacade.from_connection(
399 self.connection)
400 response = await client_facade.UserInfo(None, include_disabled)
401 return [User(self, r.result) for r in response.results]
402
403 async def grant(self, username, acl='login'):
404 """Set access level of the given user on the controller
405
406 :param str username: Username
407 :param str acl: Access control ('login', 'add-model' or 'superuser')
408
409 """
410 controller_facade = client.ControllerFacade.from_connection(
411 self.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])
416
417 async def revoke(self, username):
418 """Removes all access from a controller
419
420 :param str username: username
421
422 """
423 controller_facade = client.ControllerFacade.from_connection(
424 self.connection)
425 user = tag.user(username)
426 changes = client.ModifyControllerAccess('login', 'revoke', user)
427 return await controller_facade.ModifyControllerAccess([changes])