blob: 1c1208f430a3fd8ca6d4a6c93b32d5017dda3219 [file] [log] [blame]
Adam Israel5e08a0e2018-09-06 19:22:47 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import logging
3import os
4import os.path
5import re
Adam Israelfa329072018-09-14 11:26:13 -04006import shlex
Adam Israelc3e6c2e2018-03-01 09:31:50 -05007import ssl
Adam Israelfa329072018-09-14 11:26:13 -04008import subprocess
Adam Israelc3e6c2e2018-03-01 09:31:50 -05009import sys
Adam Israel5e08a0e2018-09-06 19:22:47 -040010# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -050011
12# FIXME: this should load the juju inside or modules without having to
13# explicitly install it. Check why it's not working.
14# Load our subtree of the juju library
15path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
16path = os.path.join(path, "modules/libjuju/")
17if path not in sys.path:
18 sys.path.insert(1, path)
19
20from juju.controller import Controller
Adam Israel5e08a0e2018-09-06 19:22:47 -040021from juju.model import ModelObserver
Adam Israel136186e2018-09-14 12:01:12 -040022from juju.errors import JujuAPIError
Adam Israelc3e6c2e2018-03-01 09:31:50 -050023
24# We might need this to connect to the websocket securely, but test and verify.
25try:
26 ssl._create_default_https_context = ssl._create_unverified_context
27except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
30 pass
31
32
33# Custom exceptions
34class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
36
37
38class JujuApplicationExists(Exception):
39 """The Application already exists."""
40
Adam Israelb5214512018-05-03 10:00:04 -040041
Adam Israel88a49632018-04-10 13:04:57 -060042class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
44
Adam Israelc3e6c2e2018-03-01 09:31:50 -050045
46# Quiet the debug logging
47logging.getLogger('websockets.protocol').setLevel(logging.INFO)
48logging.getLogger('juju.client.connection').setLevel(logging.WARN)
49logging.getLogger('juju.model').setLevel(logging.WARN)
50logging.getLogger('juju.machine').setLevel(logging.WARN)
51
Adam Israelb5214512018-05-03 10:00:04 -040052
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053class VCAMonitor(ModelObserver):
54 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050055 log = None
56 ns_name = None
Adam Israel28a43c02018-04-23 16:04:54 -040057 applications = {}
Adam Israelc3e6c2e2018-03-01 09:31:50 -050058
Adam Israel28a43c02018-04-23 16:04:54 -040059 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060 self.log = logging.getLogger(__name__)
61
62 self.ns_name = ns_name
Adam Israel28a43c02018-04-23 16:04:54 -040063
64 def AddApplication(self, application_name, callback, *callback_args):
65 if application_name not in self.applications:
66 self.applications[application_name] = {
67 'callback': callback,
68 'callback_args': callback_args
69 }
70
71 def RemoveApplication(self, application_name):
72 if application_name in self.applications:
73 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050074
75 async def on_change(self, delta, old, new, model):
76 """React to changes in the Juju model."""
77
78 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040079 # Ignore change events from other applications
80 if delta.data['application'] not in self.applications.keys():
81 return
82
Adam Israelc3e6c2e2018-03-01 09:31:50 -050083 try:
Adam Israel28a43c02018-04-23 16:04:54 -040084
85 application_name = delta.data['application']
86
87 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040088 callback_args = \
89 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040090
Adam Israelc3e6c2e2018-03-01 09:31:50 -050091 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020092 # Fire off a callback with the application state
93 if callback:
94 callback(
95 self.ns_name,
96 delta.data['application'],
97 new.workload_status,
98 new.workload_status_message,
99 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400100
101 if old and not new:
102 # This is a charm being removed
103 if callback:
104 callback(
105 self.ns_name,
106 delta.data['application'],
107 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400108 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400109 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500110 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400111 self.log.debug("[1] notify_callback exception: {}".format(e))
112
Adam Israel88a49632018-04-10 13:04:57 -0600113 elif delta.entity == "action":
114 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500115
Adam Israel88a49632018-04-10 13:04:57 -0600116 # uuid = delta.data['id'] # The Action's unique id
117 # msg = delta.data['message'] # The output of the action
118 #
119 # if delta.data['status'] == "pending":
120 # # The action is queued
121 # pass
122 # elif delta.data['status'] == "completed""
123 # # The action was successful
124 # pass
125 # elif delta.data['status'] == "failed":
126 # # The action failed.
127 # pass
128
129 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500130
131########
132# TODO
133#
134# Create unique models per network service
135# Document all public functions
136
Adam Israelb5214512018-05-03 10:00:04 -0400137
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500138class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500139 def __init__(self,
140 log=None,
141 server='127.0.0.1',
142 port=17070,
143 user='admin',
144 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400145 artifacts=None,
146 loop=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500147 ):
148 """Initialize N2VC
149
150 :param vcaconfig dict A dictionary containing the VCA configuration
151
152 :param artifacts str The directory where charms required by a vnfd are
153 stored.
154
155 :Example:
156 n2vc = N2VC(vcaconfig={
157 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
158 'user': 'admin',
159 'ip-address': '10.44.127.137',
160 'port': 17070,
161 'artifacts': '/path/to/charms'
162 })
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500163 """
164
Adam Israel5e08a0e2018-09-06 19:22:47 -0400165 # Initialize instance-level variables
166 self.api = None
167 self.log = None
168 self.controller = None
169 self.connecting = False
170 self.authenticated = False
171
Adam Israelfc511ed2018-09-21 14:20:55 +0200172 # For debugging
173 self.refcount = {
174 'controller': 0,
175 'model': 0,
176 }
177
Adam Israel5e08a0e2018-09-06 19:22:47 -0400178 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400179
180 # Model Observers
181 self.monitors = {}
182
183 # VCA config
184 self.hostname = ""
185 self.port = 17070
186 self.username = ""
187 self.secret = ""
188
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500189 if log:
190 self.log = log
191 else:
192 self.log = logging.getLogger(__name__)
193
194 # Quiet websocket traffic
195 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
196 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
197 logging.getLogger('model').setLevel(logging.WARN)
198 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
199
200 self.log.debug('JujuApi: instantiated')
201
202 self.server = server
203 self.port = port
204
205 self.secret = secret
206 if user.startswith('user-'):
207 self.user = user
208 else:
209 self.user = 'user-{}'.format(user)
210
211 self.endpoint = '%s:%d' % (server, int(port))
212
213 self.artifacts = artifacts
214
Adam Israel5e08a0e2018-09-06 19:22:47 -0400215 self.loop = loop or asyncio.get_event_loop()
216
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500217 def __del__(self):
218 """Close any open connections."""
219 yield self.logout()
220
Adam Israel5e08a0e2018-09-06 19:22:47 -0400221 def notify_callback(self, model_name, application_name, status, message,
222 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500223 try:
224 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400225 callback(
226 model_name,
227 application_name,
228 status, message,
229 *callback_args,
230 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500231 except Exception as e:
232 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600233 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500234 return True
235
236 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500237 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400238 """Create a relation between the charm-enabled VDUs in a VNF.
239
240 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
241
242 vdu:
243 ...
244 relation:
245 - provides: dataVM:db
246 requires: mgmtVM:app
247
248 This tells N2VC that the charm referred to by the dataVM vdu offers a relation named 'db', and the mgmtVM vdu has an 'app' endpoint that should be connected to a database.
249
250 :param str ns_name: The name of the network service.
251 :param dict vnfd: The parsed yaml VNF descriptor.
252 """
253
254 # Currently, the call to Relate() is made automatically after the
255 # deployment of each charm; if the relation depends on a charm that
256 # hasn't been deployed yet, the call will fail silently. This will
257 # prevent an API breakage, with the intent of making this an explicitly
258 # required call in a more object-oriented refactor of the N2VC API.
259
260 configs = []
261 vnf_config = vnfd.get("vnf-configuration")
262 if vnf_config:
263 juju = vnf_config['juju']
264 if juju:
265 configs.append(vnf_config)
266
267 for vdu in vnfd['vdu']:
268 vdu_config = vdu.get('vdu-configuration')
269 if vdu_config:
270 juju = vdu_config['juju']
271 if juju:
272 configs.append(vdu_config)
273
274 def _get_application_name(name):
275 """Get the application name that's mapped to a vnf/vdu."""
276 vnf_member_index = 0
277 vnf_name = vnfd['name']
278
279 for vdu in vnfd.get('vdu'):
280 # Compare the named portion of the relation to the vdu's id
281 if vdu['id'] == name:
282 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500283 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400284 vnf_name,
285 str(vnf_member_index),
286 )
287 return application_name
288 else:
289 vnf_member_index += 1
290
291 return None
292
293 # Loop through relations
294 for cfg in configs:
295 if 'juju' in cfg:
296 if 'relation' in juju:
297 for rel in juju['relation']:
298 try:
299
300 # get the application name for the provides
301 (name, endpoint) = rel['provides'].split(':')
302 application_name = _get_application_name(name)
303
304 provides = "{}:{}".format(
305 application_name,
306 endpoint
307 )
308
309 # get the application name for thr requires
310 (name, endpoint) = rel['requires'].split(':')
311 application_name = _get_application_name(name)
312
313 requires = "{}:{}".format(
314 application_name,
315 endpoint
316 )
317 self.log.debug("Relation: {} <-> {}".format(
318 provides,
319 requires
320 ))
321 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500322 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400323 provides,
324 requires,
325 )
326 except Exception as e:
327 self.log.debug("Exception: {}".format(e))
328
329 return
330
Adam Israel5e08a0e2018-09-06 19:22:47 -0400331 async def DeployCharms(self, model_name, application_name, vnfd,
332 charm_path, params={}, machine_spec={},
333 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500334 """Deploy one or more charms associated with a VNF.
335
336 Deploy the charm(s) referenced in a VNF Descriptor.
337
Adam Israel85a4b212018-11-29 20:30:24 -0500338 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500339 :param str application_name: The name of the application
340 :param dict vnfd: The name of the application
341 :param str charm_path: The path to the Juju charm
342 :param dict params: A dictionary of runtime parameters
343 Examples::
344 {
Adam Israel88a49632018-04-10 13:04:57 -0600345 'rw_mgmt_ip': '1.2.3.4',
346 # Pass the initial-config-primitives section of the vnf or vdu
347 'initial-config-primitives': {...}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500348 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400349 :param dict machine_spec: A dictionary describing the machine to
350 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500351 Examples::
352 {
353 'hostname': '1.2.3.4',
354 'username': 'ubuntu',
355 }
356 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400357 :param tuple callback_args: A list of arguments to be passed to the
358 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500359 """
360
361 ########################################################
362 # Verify the path to the charm exists and is readable. #
363 ########################################################
364 if not os.path.exists(charm_path):
365 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400366 self.notify_callback(
367 model_name,
368 application_name,
369 "failed",
370 callback,
371 *callback_args,
372 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500373 raise JujuCharmNotFound("No artifacts configured.")
374
375 ################################
376 # Login to the Juju controller #
377 ################################
378 if not self.authenticated:
379 self.log.debug("Authenticating with Juju")
380 await self.login()
381
382 ##########################################
383 # Get the model for this network service #
384 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500385 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500386
387 ########################################
388 # Verify the application doesn't exist #
389 ########################################
390 app = await self.get_application(model, application_name)
391 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400392 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500393
Adam Israel28a43c02018-04-23 16:04:54 -0400394 ################################################################
395 # Register this application with the model-level event monitor #
396 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500397 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400398 self.monitors[model_name].AddApplication(
399 application_name,
400 callback,
401 *callback_args
402 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500403
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500404 ########################################################
405 # Check for specific machine placement (native charms) #
406 ########################################################
407 to = ""
408 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400409 if all(k in machine_spec for k in ['host', 'user']):
410 # Enlist an existing machine as a Juju unit
411 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
412 machine_spec['user'],
413 machine_spec['host'],
414 self.GetPrivateKeyPath(),
415 ))
Adam Israelfa329072018-09-14 11:26:13 -0400416 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500417
418 #######################################
419 # Get the initial charm configuration #
420 #######################################
421
422 rw_mgmt_ip = None
423 if 'rw_mgmt_ip' in params:
424 rw_mgmt_ip = params['rw_mgmt_ip']
425
Adam Israel5afe0542018-08-08 12:54:55 -0400426 if 'initial-config-primitive' not in params:
427 params['initial-config-primitive'] = {}
428
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500429 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600430 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500431 {'<rw_mgmt_ip>': rw_mgmt_ip}
432 )
433
Adam Israel85a4b212018-11-29 20:30:24 -0500434 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
435 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500436 application_name,
437 charm_path,
438 to=to,
439 ))
440
441 ########################################################
442 # Deploy the charm and apply the initial configuration #
443 ########################################################
444 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600445 # We expect charm_path to be either the path to the charm on disk
446 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500447 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600448 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500449 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600450 # Proxy charms should use the current LTS. This will need to be
451 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600453 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500454 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400455 # Where to deploy the charm to.
456 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500457 )
458
Adam Israel136186e2018-09-14 12:01:12 -0400459 # Map the vdu id<->app name,
460 #
461 await self.Relate(model_name, vnfd)
462
Adam Israel88a49632018-04-10 13:04:57 -0600463 # #######################################
464 # # Execute initial config primitive(s) #
465 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700466 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400467 model_name,
468 application_name,
469 params,
470 )
Adam Israelcf253202018-10-31 16:29:09 -0700471 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400472
473 # primitives = {}
474 #
475 # # Build a sequential list of the primitives to execute
476 # for primitive in params['initial-config-primitive']:
477 # try:
478 # if primitive['name'] == 'config':
479 # # This is applied when the Application is deployed
480 # pass
481 # else:
482 # seq = primitive['seq']
483 #
484 # params = {}
485 # if 'parameter' in primitive:
486 # params = primitive['parameter']
487 #
488 # primitives[seq] = {
489 # 'name': primitive['name'],
490 # 'parameters': self._map_primitive_parameters(
491 # params,
492 # {'<rw_mgmt_ip>': rw_mgmt_ip}
493 # ),
494 # }
495 #
496 # for primitive in sorted(primitives):
497 # await self.ExecutePrimitive(
498 # model_name,
499 # application_name,
500 # primitives[primitive]['name'],
501 # callback,
502 # callback_args,
503 # **primitives[primitive]['parameters'],
504 # )
505 # except N2VCPrimitiveExecutionFailed as e:
506 # self.log.debug(
507 # "[N2VC] Exception executing primitive: {}".format(e)
508 # )
509 # raise
510
511 async def GetPrimitiveStatus(self, model_name, uuid):
512 """Get the status of an executed Primitive.
513
514 The status of an executed Primitive will be one of three values:
515 - completed
516 - failed
517 - running
518 """
519 status = None
520 try:
521 if not self.authenticated:
522 await self.login()
523
Adam Israel5e08a0e2018-09-06 19:22:47 -0400524 model = await self.get_model(model_name)
525
526 results = await model.get_action_status(uuid)
527
528 if uuid in results:
529 status = results[uuid]
530
531 except Exception as e:
532 self.log.debug(
533 "Caught exception while getting primitive status: {}".format(e)
534 )
535 raise N2VCPrimitiveExecutionFailed(e)
536
537 return status
538
539 async def GetPrimitiveOutput(self, model_name, uuid):
540 """Get the output of an executed Primitive.
541
542 Note: this only returns output for a successfully executed primitive.
543 """
544 results = None
545 try:
546 if not self.authenticated:
547 await self.login()
548
Adam Israel5e08a0e2018-09-06 19:22:47 -0400549 model = await self.get_model(model_name)
550 results = await model.get_action_output(uuid, 60)
551 except Exception as e:
552 self.log.debug(
553 "Caught exception while getting primitive status: {}".format(e)
554 )
555 raise N2VCPrimitiveExecutionFailed(e)
556
557 return results
558
Adam Israelfa329072018-09-14 11:26:13 -0400559 # async def ProvisionMachine(self, model_name, hostname, username):
560 # """Provision machine for usage with Juju.
561 #
562 # Provisions a previously instantiated machine for use with Juju.
563 # """
564 # try:
565 # if not self.authenticated:
566 # await self.login()
567 #
568 # # FIXME: This is hard-coded until model-per-ns is added
569 # model_name = 'default'
570 #
571 # model = await self.get_model(model_name)
572 # model.add_machine(spec={})
573 #
574 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
575 # "ubuntu",
576 # host['address'],
577 # private_key_path,
578 # ))
579 # return machine.id
580 #
581 # except Exception as e:
582 # self.log.debug(
583 # "Caught exception while getting primitive status: {}".format(e)
584 # )
585 # raise N2VCPrimitiveExecutionFailed(e)
586
587 def GetPrivateKeyPath(self):
588 homedir = os.environ['HOME']
589 sshdir = "{}/.ssh".format(homedir)
590 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
591 return private_key_path
592
593 async def GetPublicKey(self):
594 """Get the N2VC SSH public key.abs
595
596 Returns the SSH public key, to be injected into virtual machines to
597 be managed by the VCA.
598
599 The first time this is run, a ssh keypair will be created. The public
600 key is injected into a VM so that we can provision the machine with
601 Juju, after which Juju will communicate with the VM directly via the
602 juju agent.
603 """
604 public_key = ""
605
606 # Find the path to where we expect our key to live.
607 homedir = os.environ['HOME']
608 sshdir = "{}/.ssh".format(homedir)
609 if not os.path.exists(sshdir):
610 os.mkdir(sshdir)
611
612 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
613 public_key_path = "{}.pub".format(private_key_path)
614
615 # If we don't have a key generated, generate it.
616 if not os.path.exists(private_key_path):
617 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
618 "rsa",
619 "4096",
620 private_key_path
621 )
622 subprocess.check_output(shlex.split(cmd))
623
624 # Read the public key
625 with open(public_key_path, "r") as f:
626 public_key = f.readline()
627
628 return public_key
629
Adam Israel5e08a0e2018-09-06 19:22:47 -0400630 async def ExecuteInitialPrimitives(self, model_name, application_name,
631 params, callback=None, *callback_args):
632 """Execute multiple primitives.
633
634 Execute multiple primitives as declared in initial-config-primitive.
635 This is useful in cases where the primitives initially failed -- for
636 example, if the charm is a proxy but the proxy hasn't been configured
637 yet.
638 """
639 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600640 primitives = {}
641
642 # Build a sequential list of the primitives to execute
643 for primitive in params['initial-config-primitive']:
644 try:
645 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600646 pass
647 else:
Adam Israel88a49632018-04-10 13:04:57 -0600648 seq = primitive['seq']
649
Adam Israel42d88e62018-07-16 14:18:41 -0400650 params = {}
651 if 'parameter' in primitive:
652 params = primitive['parameter']
653
Adam Israel88a49632018-04-10 13:04:57 -0600654 primitives[seq] = {
655 'name': primitive['name'],
656 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400657 params,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400658 {'<rw_mgmt_ip>': None}
Adam Israel88a49632018-04-10 13:04:57 -0600659 ),
660 }
661
662 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400663 uuids.append(
664 await self.ExecutePrimitive(
665 model_name,
666 application_name,
667 primitives[primitive]['name'],
668 callback,
669 callback_args,
670 **primitives[primitive]['parameters'],
671 )
Adam Israel88a49632018-04-10 13:04:57 -0600672 )
673 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400674 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600675 "[N2VC] Exception executing primitive: {}".format(e)
676 )
677 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400678 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600679
Adam Israel5e08a0e2018-09-06 19:22:47 -0400680 async def ExecutePrimitive(self, model_name, application_name, primitive,
681 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400682 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600683
Adam Israelc9df96f2018-05-03 14:49:56 -0400684 Execute a primitive defined in the VNF descriptor.
685
Adam Israel85a4b212018-11-29 20:30:24 -0500686 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400687 :param str application_name: The name of the application
688 :param str primitive: The name of the primitive to execute.
689 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400690 :param tuple callback_args: A list of arguments to be passed to the
691 callback function.
692 :param dict params: A dictionary of key=value pairs representing the
693 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400694 Examples::
695 {
696 'rw_mgmt_ip': '1.2.3.4',
697 # Pass the initial-config-primitives section of the vnf or vdu
698 'initial-config-primitives': {...}
699 }
Adam Israel6817f612018-04-13 08:41:43 -0600700 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400701 self.log.debug("Executing {}".format(primitive))
Adam Israel6817f612018-04-13 08:41:43 -0600702 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500703 try:
704 if not self.authenticated:
705 await self.login()
706
Adam Israel5e08a0e2018-09-06 19:22:47 -0400707 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400708
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500709 if primitive == 'config':
710 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400711 await self.set_config(
712 model,
713 application_name,
714 params['params'],
715 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500716 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500717 app = await self.get_application(model, application_name)
718 if app:
719 # Run against the first (and probably only) unit in the app
720 unit = app.units[0]
721 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500722 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600723 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500724 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400725 self.log.debug(
726 "Caught exception while executing primitive: {}".format(e)
727 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400728 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600729 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500730
Adam Israel5e08a0e2018-09-06 19:22:47 -0400731 async def RemoveCharms(self, model_name, application_name, callback=None,
732 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400733 """Remove a charm from the VCA.
734
735 Remove a charm referenced in a VNF Descriptor.
736
737 :param str model_name: The name of the network service.
738 :param str application_name: The name of the application
739 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400740 :param tuple callback_args: A list of arguments to be passed to the
741 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400742 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500743 try:
744 if not self.authenticated:
745 await self.login()
746
747 model = await self.get_model(model_name)
748 app = await self.get_application(model, application_name)
749 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400750 # Remove this application from event monitoring
751 self.monitors[model_name].RemoveApplication(application_name)
752
753 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400754 self.log.debug(
755 "Removing the application {}".format(application_name)
756 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500757 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400758
Adam Israel85a4b212018-11-29 20:30:24 -0500759 await self.disconnect_model(self.monitors[model_name])
760
Adam Israel28a43c02018-04-23 16:04:54 -0400761 # Notify the callback that this charm has been removed.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400762 self.notify_callback(
763 model_name,
764 application_name,
765 "removed",
766 callback,
767 *callback_args,
768 )
Adam Israel28a43c02018-04-23 16:04:54 -0400769
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500770 except Exception as e:
771 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600772 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500773 raise e
774
775 async def DestroyNetworkService(self, nsd):
776 raise NotImplementedError()
777
Adam Israelb5214512018-05-03 10:00:04 -0400778 async def GetMetrics(self, model_name, application_name):
779 """Get the metrics collected by the VCA.
780
Adam Israel85a4b212018-11-29 20:30:24 -0500781 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400782 :param application_name The name of the application
783 """
784 metrics = {}
785 model = await self.get_model(model_name)
786 app = await self.get_application(model, application_name)
787 if app:
788 metrics = await app.get_metrics()
789
790 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500791
Adam Israelfa329072018-09-14 11:26:13 -0400792 async def HasApplication(self, model_name, application_name):
793 model = await self.get_model(model_name)
794 app = await self.get_application(model, application_name)
795 if app:
796 return True
797 return False
798
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500799 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400800 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500801 """
802 Add a relation between two application endpoints.
803
Adam Israel85a4b212018-11-29 20:30:24 -0500804 :param str model_name: The name or unique id of the network service
805 :param str relation1: '<application>[:<relation_name>]'
806 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500807 """
Adam Israel136186e2018-09-14 12:01:12 -0400808
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500809 if not self.authenticated:
810 await self.login()
811
Adam Israel136186e2018-09-14 12:01:12 -0400812 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500813 try:
Adam Israel136186e2018-09-14 12:01:12 -0400814 await m.add_relation(relation1, relation2)
815 except JujuAPIError as e:
816 # If one of the applications in the relationship doesn't exist,
817 # or the relation has already been added, let the operation fail
818 # silently.
819 if 'not found' in e.message:
820 return
821 if 'already exists' in e.message:
822 return
823
824 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500825
Adam Israelb5214512018-05-03 10:00:04 -0400826 # async def apply_config(self, config, application):
827 # """Apply a configuration to the application."""
828 # print("JujuApi: Applying configuration to {}.".format(
829 # application
830 # ))
831 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500832
833 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600834 """Transform the yang config primitive to dict.
835
836 Expected result:
837
838 config = {
839 'config':
840 }
841 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500842 config = {}
843 for primitive in config_primitive:
844 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600845 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500846 for parameter in primitive['parameter']:
847 param = str(parameter['name'])
848 if parameter['value'] == "<rw_mgmt_ip>":
849 config[param] = str(values[parameter['value']])
850 else:
851 config[param] = str(parameter['value'])
852
853 return config
854
Adam Israel88a49632018-04-10 13:04:57 -0600855 def _map_primitive_parameters(self, parameters, values):
856 params = {}
857 for parameter in parameters:
858 param = str(parameter['name'])
tierno40047482018-10-26 14:54:26 +0200859 value = None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400860
Adam Israelbf793522018-11-20 13:54:13 -0500861 # If there's no value, use the default-value (if set)
862 if parameter['value'] is None and 'default-value' in parameter:
863 value = parameter['default-value']
864
Adam Israel5e08a0e2018-09-06 19:22:47 -0400865 # Typecast parameter value, if present
866 if 'data-type' in parameter:
867 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400868
869 if paramtype == "integer":
870 value = int(parameter['value'])
871 elif paramtype == "boolean":
872 value = bool(parameter['value'])
873 else:
874 value = str(parameter['value'])
Adam Israelbf793522018-11-20 13:54:13 -0500875 else:
876 # If there's no data-type, assume the value is a string
877 value = str(parameter['value'])
Adam Israel5e08a0e2018-09-06 19:22:47 -0400878
Adam Israel88a49632018-04-10 13:04:57 -0600879 if parameter['value'] == "<rw_mgmt_ip>":
880 params[param] = str(values[parameter['value']])
881 else:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400882 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600883 return params
884
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500885 def _get_config_from_yang(self, config_primitive, values):
886 """Transform the yang config primitive to dict."""
887 config = {}
888 for primitive in config_primitive.values():
889 if primitive['name'] == 'config':
890 for parameter in primitive['parameter'].values():
891 param = str(parameter['name'])
892 if parameter['value'] == "<rw_mgmt_ip>":
893 config[param] = str(values[parameter['value']])
894 else:
895 config[param] = str(parameter['value'])
896
897 return config
898
899 def FormatApplicationName(self, *args):
900 """
901 Generate a Juju-compatible Application name
902
903 :param args tuple: Positional arguments to be used to construct the
904 application name.
905
906 Limitations::
907 - Only accepts characters a-z and non-consequitive dashes (-)
908 - Application name should not exceed 50 characters
909
910 Examples::
911
912 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
913 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500914 appname = ""
915 for c in "-".join(list(args)):
916 if c.isdigit():
917 c = chr(97 + int(c))
918 elif not c.isalpha():
919 c = "-"
920 appname += c
921 return re.sub('\-+', '-', appname.lower())
922
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500923 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
924 # """Format the name of the application
925 #
926 # Limitations:
927 # - Only accepts characters a-z and non-consequitive dashes (-)
928 # - Application name should not exceed 50 characters
929 # """
930 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
931 # new_name = ''
932 # for c in name:
933 # if c.isdigit():
934 # c = chr(97 + int(c))
935 # elif not c.isalpha():
936 # c = "-"
937 # new_name += c
938 # return re.sub('\-+', '-', new_name.lower())
939
940 def format_model_name(self, name):
941 """Format the name of model.
942
943 Model names may only contain lowercase letters, digits and hyphens
944 """
945
946 return name.replace('_', '-').lower()
947
948 async def get_application(self, model, application):
949 """Get the deployed application."""
950 if not self.authenticated:
951 await self.login()
952
953 app = None
954 if application and model:
955 if model.applications:
956 if application in model.applications:
957 app = model.applications[application]
958
959 return app
960
Adam Israel85a4b212018-11-29 20:30:24 -0500961 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500962 """Get a model from the Juju Controller.
963
964 Note: Model objects returned must call disconnected() before it goes
965 out of scope."""
966 if not self.authenticated:
967 await self.login()
968
969 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -0500970 # Get the models in the controller
971 models = await self.controller.list_models()
972
973 if model_name not in models:
974 self.models[model_name] = await self.controller.add_model(
975 model_name
976 )
977 else:
978 self.models[model_name] = await self.controller.get_model(
979 model_name
980 )
981
Adam Israelfc511ed2018-09-21 14:20:55 +0200982 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500983
Adam Israel28a43c02018-04-23 16:04:54 -0400984 # Create an observer for this model
985 self.monitors[model_name] = VCAMonitor(model_name)
986 self.models[model_name].add_observer(self.monitors[model_name])
987
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500988 return self.models[model_name]
989
990 async def login(self):
991 """Login to the Juju controller."""
992
993 if self.authenticated:
994 return
995
996 self.connecting = True
997
998 self.log.debug("JujuApi: Logging into controller")
999
1000 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -04001001 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001002
1003 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001004 self.log.debug(
1005 "Connecting to controller... ws://{}:{} as {}/{}".format(
1006 self.endpoint,
1007 self.port,
1008 self.user,
1009 self.secret,
1010 )
1011 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001012 await self.controller.connect(
1013 endpoint=self.endpoint,
1014 username=self.user,
1015 password=self.secret,
1016 cacert=cacert,
1017 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001018 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001019 else:
1020 # current_controller no longer exists
1021 # self.log.debug("Connecting to current controller...")
1022 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001023 # await self.controller.connect(
1024 # endpoint=self.endpoint,
1025 # username=self.user,
1026 # cacert=cacert,
1027 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001028 self.log.fatal("VCA credentials not configured.")
1029
1030 self.authenticated = True
1031 self.log.debug("JujuApi: Logged into controller")
1032
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001033 async def logout(self):
1034 """Logout of the Juju controller."""
1035 if not self.authenticated:
1036 return
1037
1038 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001039 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001040 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001041
1042 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001043 self.log.debug("Disconnecting controller {}".format(
1044 self.controller
1045 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001046 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001047 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001048 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001049
1050 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001051
1052 self.log.debug(self.refcount)
1053
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001054 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001055 self.log.fatal(
1056 "Fatal error logging out of Juju Controller: {}".format(e)
1057 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001058 raise e
1059
Adam Israel85a4b212018-11-29 20:30:24 -05001060 async def disconnect_model(self, model):
1061 self.log.debug("Disconnecting model {}".format(model))
1062 if model in self.models:
1063 print(self.models[model].applications)
1064 if len(self.models[model].applications) == 0:
1065 print("Destroying empty model")
1066 await self.controller.destroy_models(model)
1067
1068 print("Disconnecting model")
1069 await self.models[model].disconnect()
1070 self.refcount['model'] -= 1
1071 self.models[model] = None
1072
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001073 # async def remove_application(self, name):
1074 # """Remove the application."""
1075 # if not self.authenticated:
1076 # await self.login()
1077 #
1078 # app = await self.get_application(name)
1079 # if app:
1080 # self.log.debug("JujuApi: Destroying application {}".format(
1081 # name,
1082 # ))
1083 #
1084 # await app.destroy()
1085
1086 async def remove_relation(self, a, b):
1087 """
1088 Remove a relation between two application endpoints
1089
1090 :param a An application endpoint
1091 :param b An application endpoint
1092 """
1093 if not self.authenticated:
1094 await self.login()
1095
1096 m = await self.get_model()
1097 try:
1098 m.remove_relation(a, b)
1099 finally:
1100 await m.disconnect()
1101
Adam Israel85a4b212018-11-29 20:30:24 -05001102 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001103 """Resolve units in error state."""
1104 if not self.authenticated:
1105 await self.login()
1106
Adam Israel85a4b212018-11-29 20:30:24 -05001107 model = await self.get_model(model_name)
1108
1109 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001110 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001111 self.log.debug(
1112 "JujuApi: Resolving errors for application {}".format(
1113 application,
1114 )
1115 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001116
1117 for unit in app.units:
1118 app.resolved(retry=True)
1119
Adam Israel85a4b212018-11-29 20:30:24 -05001120 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001121 """Execute an action and return an Action object."""
1122 if not self.authenticated:
1123 await self.login()
1124 result = {
1125 'status': '',
1126 'action': {
1127 'tag': None,
1128 'results': None,
1129 }
1130 }
Adam Israel85a4b212018-11-29 20:30:24 -05001131
1132 model = await self.get_model(model_name)
1133
1134 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001135 if app:
1136 # We currently only have one unit per application
1137 # so use the first unit available.
1138 unit = app.units[0]
1139
Adam Israel5e08a0e2018-09-06 19:22:47 -04001140 self.log.debug(
1141 "JujuApi: Running Action {} against Application {}".format(
1142 action_name,
1143 application,
1144 )
1145 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001146
1147 action = await unit.run_action(action_name, **params)
1148
1149 # Wait for the action to complete
1150 await action.wait()
1151
1152 result['status'] = action.status
1153 result['action']['tag'] = action.data['id']
1154 result['action']['results'] = action.results
1155
1156 return result
1157
Adam Israelb5214512018-05-03 10:00:04 -04001158 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001159 """Apply a configuration to the application."""
1160 if not self.authenticated:
1161 await self.login()
1162
Adam Israelb5214512018-05-03 10:00:04 -04001163 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001164 if app:
1165 self.log.debug("JujuApi: Setting config for Application {}".format(
1166 application,
1167 ))
1168 await app.set_config(config)
1169
1170 # Verify the config is set
1171 newconf = await app.get_config()
1172 for key in config:
1173 if config[key] != newconf[key]['value']:
1174 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1175
Adam Israelb5214512018-05-03 10:00:04 -04001176 # async def set_parameter(self, parameter, value, application=None):
1177 # """Set a config parameter for a service."""
1178 # if not self.authenticated:
1179 # await self.login()
1180 #
1181 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1182 # parameter,
1183 # value,
1184 # application,
1185 # ))
1186 # return await self.apply_config(
1187 # {parameter: value},
1188 # application=application,
1189 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001190
Adam Israel5e08a0e2018-09-06 19:22:47 -04001191 async def wait_for_application(self, model_name, application_name,
1192 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001193 """Wait for an application to become active."""
1194 if not self.authenticated:
1195 await self.login()
1196
Adam Israel5e08a0e2018-09-06 19:22:47 -04001197 model = await self.get_model(model_name)
1198
1199 app = await self.get_application(model, application_name)
1200 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001201 if app:
1202 self.log.debug(
1203 "JujuApi: Waiting {} seconds for Application {}".format(
1204 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001205 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001206 )
1207 )
1208
Adam Israel5e08a0e2018-09-06 19:22:47 -04001209 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001210 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001211 unit.agent_status == 'idle' and unit.workload_status in
1212 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001213 ),
1214 timeout=timeout
1215 )