Merge "Fix bug #502"
[osm/N2VC.git] / n2vc / vnf.py
1
2 import logging
3 import os
4 import os.path
5 import re
6 import ssl
7 import sys
8 import time
9
10 # FIXME: this should load the juju inside or modules without having to
11 # explicitly install it. Check why it's not working.
12 # Load our subtree of the juju library
13 path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
14 path = os.path.join(path, "modules/libjuju/")
15 if path not in sys.path:
16 sys.path.insert(1, path)
17
18 from juju.controller import Controller
19 from juju.model import Model, ModelObserver
20
21
22 # We might need this to connect to the websocket securely, but test and verify.
23 try:
24 ssl._create_default_https_context = ssl._create_unverified_context
25 except AttributeError:
26 # Legacy Python doesn't verify by default (see pep-0476)
27 # https://www.python.org/dev/peps/pep-0476/
28 pass
29
30
31 # Custom exceptions
32 class JujuCharmNotFound(Exception):
33 """The Charm can't be found or is not readable."""
34
35
36 class JujuApplicationExists(Exception):
37 """The Application already exists."""
38
39
40 class N2VCPrimitiveExecutionFailed(Exception):
41 """Something failed while attempting to execute a primitive."""
42
43
44 # Quiet the debug logging
45 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
46 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
47 logging.getLogger('juju.model').setLevel(logging.WARN)
48 logging.getLogger('juju.machine').setLevel(logging.WARN)
49
50
51 class VCAMonitor(ModelObserver):
52 """Monitor state changes within the Juju Model."""
53 log = None
54 ns_name = None
55 applications = {}
56
57 def __init__(self, ns_name):
58 self.log = logging.getLogger(__name__)
59
60 self.ns_name = ns_name
61
62 def AddApplication(self, application_name, callback, *callback_args):
63 if application_name not in self.applications:
64 self.applications[application_name] = {
65 'callback': callback,
66 'callback_args': callback_args
67 }
68
69 def RemoveApplication(self, application_name):
70 if application_name in self.applications:
71 del self.applications[application_name]
72
73 async def on_change(self, delta, old, new, model):
74 """React to changes in the Juju model."""
75
76 if delta.entity == "unit":
77 # Ignore change events from other applications
78 if delta.data['application'] not in self.applications.keys():
79 return
80
81 try:
82
83 application_name = delta.data['application']
84
85 callback = self.applications[application_name]['callback']
86 callback_args = self.applications[application_name]['callback_args']
87
88 if old and new:
89 old_status = old.workload_status
90 new_status = new.workload_status
91
92 if old_status == new_status:
93 """The workload status may fluctuate around certain events,
94 so wait until the status has stabilized before triggering
95 the callback."""
96 if callback:
97 callback(
98 self.ns_name,
99 delta.data['application'],
100 new_status,
101 new.workload_status_message,
102 *callback_args)
103
104 if old and not new:
105 # This is a charm being removed
106 if callback:
107 callback(
108 self.ns_name,
109 delta.data['application'],
110 "removed",
111 "",
112 *callback_args)
113 except Exception as e:
114 self.log.debug("[1] notify_callback exception {}".format(e))
115 elif delta.entity == "action":
116 # TODO: Decide how we want to notify the user of actions
117
118 # uuid = delta.data['id'] # The Action's unique id
119 # msg = delta.data['message'] # The output of the action
120 #
121 # if delta.data['status'] == "pending":
122 # # The action is queued
123 # pass
124 # elif delta.data['status'] == "completed""
125 # # The action was successful
126 # pass
127 # elif delta.data['status'] == "failed":
128 # # The action failed.
129 # pass
130
131 pass
132
133 ########
134 # TODO
135 #
136 # Create unique models per network service
137 # Document all public functions
138
139
140 class N2VC:
141
142 # Juju API
143 api = None
144 log = None
145 controller = None
146 connecting = False
147 authenticated = False
148
149 models = {}
150 default_model = None
151
152 # Model Observers
153 monitors = {}
154
155 # VCA config
156 hostname = ""
157 port = 17070
158 username = ""
159 secret = ""
160
161 def __init__(self,
162 log=None,
163 server='127.0.0.1',
164 port=17070,
165 user='admin',
166 secret=None,
167 artifacts=None
168 ):
169 """Initialize N2VC
170
171 :param vcaconfig dict A dictionary containing the VCA configuration
172
173 :param artifacts str The directory where charms required by a vnfd are
174 stored.
175
176 :Example:
177 n2vc = N2VC(vcaconfig={
178 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
179 'user': 'admin',
180 'ip-address': '10.44.127.137',
181 'port': 17070,
182 'artifacts': '/path/to/charms'
183 })
184
185 """
186
187 if log:
188 self.log = log
189 else:
190 self.log = logging.getLogger(__name__)
191
192 # Quiet websocket traffic
193 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
194 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
195 logging.getLogger('model').setLevel(logging.WARN)
196 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
197
198 self.log.debug('JujuApi: instantiated')
199
200 self.server = server
201 self.port = port
202
203 self.secret = secret
204 if user.startswith('user-'):
205 self.user = user
206 else:
207 self.user = 'user-{}'.format(user)
208
209 self.endpoint = '%s:%d' % (server, int(port))
210
211 self.artifacts = artifacts
212
213 def __del__(self):
214 """Close any open connections."""
215 yield self.logout()
216
217 def notify_callback(self, model_name, application_name, status, message, callback=None, *callback_args):
218 try:
219 if callback:
220 callback(model_name, application_name, status, message, *callback_args)
221 except Exception as e:
222 self.log.error("[0] notify_callback exception {}".format(e))
223 raise e
224 return True
225
226 # Public methods
227 async def CreateNetworkService(self, nsd):
228 """Create a new model to encapsulate this network service.
229
230 Create a new model in the Juju controller to encapsulate the
231 charms associated with a network service.
232
233 You can pass either the nsd record or the id of the network
234 service, but this method will fail without one of them.
235 """
236 if not self.authenticated:
237 await self.login()
238
239 # Ideally, we will create a unique model per network service.
240 # This change will require all components, i.e., LCM and SO, to use
241 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
242 # services deployed via LCM would't be manageable via SO and vice versa
243
244 return self.default_model
245
246 async def DeployCharms(self, model_name, application_name, vnfd, charm_path, params={}, machine_spec={}, callback=None, *callback_args):
247 """Deploy one or more charms associated with a VNF.
248
249 Deploy the charm(s) referenced in a VNF Descriptor.
250
251 :param str model_name: The name of the network service.
252 :param str application_name: The name of the application
253 :param dict vnfd: The name of the application
254 :param str charm_path: The path to the Juju charm
255 :param dict params: A dictionary of runtime parameters
256 Examples::
257 {
258 'rw_mgmt_ip': '1.2.3.4',
259 # Pass the initial-config-primitives section of the vnf or vdu
260 'initial-config-primitives': {...}
261 }
262 :param dict machine_spec: A dictionary describing the machine to install to
263 Examples::
264 {
265 'hostname': '1.2.3.4',
266 'username': 'ubuntu',
267 }
268 :param obj callback: A callback function to receive status changes.
269 :param tuple callback_args: A list of arguments to be passed to the callback
270 """
271
272 ########################################################
273 # Verify the path to the charm exists and is readable. #
274 ########################################################
275 if not os.path.exists(charm_path):
276 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
277 self.notify_callback(model_name, application_name, "failed", callback, *callback_args)
278 raise JujuCharmNotFound("No artifacts configured.")
279
280 ################################
281 # Login to the Juju controller #
282 ################################
283 if not self.authenticated:
284 self.log.debug("Authenticating with Juju")
285 await self.login()
286
287 ##########################################
288 # Get the model for this network service #
289 ##########################################
290 # TODO: In a point release, we will use a model per deployed network
291 # service. In the meantime, we will always use the 'default' model.
292 model_name = 'default'
293 model = await self.get_model(model_name)
294
295 ########################################
296 # Verify the application doesn't exist #
297 ########################################
298 app = await self.get_application(model, application_name)
299 if app:
300 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
301
302 ################################################################
303 # Register this application with the model-level event monitor #
304 ################################################################
305 if callback:
306 self.monitors[model_name].AddApplication(
307 application_name,
308 callback,
309 *callback_args
310 )
311
312 ########################################################
313 # Check for specific machine placement (native charms) #
314 ########################################################
315 to = ""
316 if machine_spec.keys():
317 # TODO: This needs to be tested.
318 # if all(k in machine_spec for k in ['hostname', 'username']):
319 # # Enlist the existing machine in Juju
320 # machine = await self.model.add_machine(spec='ssh:%@%'.format(
321 # specs['host'],
322 # specs['user'],
323 # ))
324 # to = machine.id
325 pass
326
327 #######################################
328 # Get the initial charm configuration #
329 #######################################
330
331 rw_mgmt_ip = None
332 if 'rw_mgmt_ip' in params:
333 rw_mgmt_ip = params['rw_mgmt_ip']
334
335 initial_config = self._get_config_from_dict(
336 params['initial-config-primitive'],
337 {'<rw_mgmt_ip>': rw_mgmt_ip}
338 )
339
340 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
341 application_name,
342 charm_path,
343 to=to,
344 ))
345
346 ########################################################
347 # Deploy the charm and apply the initial configuration #
348 ########################################################
349 app = await model.deploy(
350 # We expect charm_path to be either the path to the charm on disk
351 # or in the format of cs:series/name
352 charm_path,
353 # This is the formatted, unique name for this charm
354 application_name=application_name,
355 # Proxy charms should use the current LTS. This will need to be
356 # changed for native charms.
357 series='xenial',
358 # Apply the initial 'config' primitive during deployment
359 config=initial_config,
360 # TBD: Where to deploy the charm to.
361 to=None,
362 )
363
364 # #######################################
365 # # Execute initial config primitive(s) #
366 # #######################################
367 primitives = {}
368
369 # Build a sequential list of the primitives to execute
370 for primitive in params['initial-config-primitive']:
371 try:
372 if primitive['name'] == 'config':
373 # This is applied when the Application is deployed
374 pass
375 else:
376 seq = primitive['seq']
377
378 params = {}
379 if 'parameter' in primitive:
380 params = primitive['parameter']
381
382 primitives[seq] = {
383 'name': primitive['name'],
384 'parameters': self._map_primitive_parameters(
385 params,
386 {'<rw_mgmt_ip>': rw_mgmt_ip}
387 ),
388 }
389
390 for primitive in sorted(primitives):
391 await self.ExecutePrimitive(
392 model_name,
393 application_name,
394 primitives[primitive]['name'],
395 callback,
396 callback_args,
397 **primitives[primitive]['parameters'],
398 )
399 except N2VCPrimitiveExecutionFailed as e:
400 self.log.debug(
401 "[N2VC] Exception executing primitive: {}".format(e)
402 )
403 raise
404
405 async def GetPrimitiveStatus(self, model_name, uuid):
406 results = None
407 try:
408 if not self.authenticated:
409 await self.login()
410
411 # FIXME: This is hard-coded until model-per-ns is added
412 model_name = 'default'
413
414 model = await self.controller.get_model(model_name)
415
416 results = await model.get_action_output(uuid)
417
418 await model.disconnect()
419 except Exception as e:
420 self.log.debug(
421 "Caught exception while getting primitive status: {}".format(e)
422 )
423 raise N2VCPrimitiveExecutionFailed(e)
424
425 return results
426
427
428 async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
429 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
430
431 Execute a primitive defined in the VNF descriptor.
432
433 :param str model_name: The name of the network service.
434 :param str application_name: The name of the application
435 :param str primitive: The name of the primitive to execute.
436 :param obj callback: A callback function to receive status changes.
437 :param tuple callback_args: A list of arguments to be passed to the callback function.
438 :param dict params: A dictionary of key=value pairs representing the primitive's parameters
439 Examples::
440 {
441 'rw_mgmt_ip': '1.2.3.4',
442 # Pass the initial-config-primitives section of the vnf or vdu
443 'initial-config-primitives': {...}
444 }
445 """
446 uuid = None
447 try:
448 if not self.authenticated:
449 await self.login()
450
451 # FIXME: This is hard-coded until model-per-ns is added
452 model_name = 'default'
453
454 model = await self.controller.get_model(model_name)
455
456 if primitive == 'config':
457 # config is special, and expecting params to be a dictionary
458 await self.set_config(
459 model,
460 application_name,
461 params['params'],
462 )
463 else:
464 app = await self.get_application(model, application_name)
465 if app:
466 # Run against the first (and probably only) unit in the app
467 unit = app.units[0]
468 if unit:
469 self.log.debug(
470 "Executing primitive {}".format(primitive)
471 )
472 action = await unit.run_action(primitive, **params)
473 uuid = action.id
474 await model.disconnect()
475 except Exception as e:
476 self.log.debug(
477 "Caught exception while executing primitive: {}".format(e)
478 )
479 raise N2VCPrimitiveExecutionFailed(e)
480 return uuid
481
482 async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
483 """Remove a charm from the VCA.
484
485 Remove a charm referenced in a VNF Descriptor.
486
487 :param str model_name: The name of the network service.
488 :param str application_name: The name of the application
489 :param obj callback: A callback function to receive status changes.
490 :param tuple callback_args: A list of arguments to be passed to the callback function.
491 """
492 try:
493 if not self.authenticated:
494 await self.login()
495
496 model = await self.get_model(model_name)
497 app = await self.get_application(model, application_name)
498 if app:
499 # Remove this application from event monitoring
500 self.monitors[model_name].RemoveApplication(application_name)
501
502 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
503 self.log.debug("Removing the application {}".format(application_name))
504 await app.remove()
505
506 # Notify the callback that this charm has been removed.
507 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
508
509 except Exception as e:
510 print("Caught exception: {}".format(e))
511 self.log.debug(e)
512 raise e
513
514 async def DestroyNetworkService(self, nsd):
515 raise NotImplementedError()
516
517 async def GetMetrics(self, model_name, application_name):
518 """Get the metrics collected by the VCA.
519
520 :param model_name The name of the model
521 :param application_name The name of the application
522 """
523 metrics = {}
524 model = await self.get_model(model_name)
525 app = await self.get_application(model, application_name)
526 if app:
527 metrics = await app.get_metrics()
528
529 return metrics
530
531 # Non-public methods
532 async def add_relation(self, a, b, via=None):
533 """
534 Add a relation between two application endpoints.
535
536 :param a An application endpoint
537 :param b An application endpoint
538 :param via The egress subnet(s) for outbound traffic, e.g.,
539 (192.168.0.0/16,10.0.0.0/8)
540 """
541 if not self.authenticated:
542 await self.login()
543
544 m = await self.get_model()
545 try:
546 m.add_relation(a, b, via)
547 finally:
548 await m.disconnect()
549
550 # async def apply_config(self, config, application):
551 # """Apply a configuration to the application."""
552 # print("JujuApi: Applying configuration to {}.".format(
553 # application
554 # ))
555 # return await self.set_config(application=application, config=config)
556
557 def _get_config_from_dict(self, config_primitive, values):
558 """Transform the yang config primitive to dict.
559
560 Expected result:
561
562 config = {
563 'config':
564 }
565 """
566 config = {}
567 for primitive in config_primitive:
568 if primitive['name'] == 'config':
569 # config = self._map_primitive_parameters()
570 for parameter in primitive['parameter']:
571 param = str(parameter['name'])
572 if parameter['value'] == "<rw_mgmt_ip>":
573 config[param] = str(values[parameter['value']])
574 else:
575 config[param] = str(parameter['value'])
576
577 return config
578
579 def _map_primitive_parameters(self, parameters, values):
580 params = {}
581 for parameter in parameters:
582 param = str(parameter['name'])
583 if parameter['value'] == "<rw_mgmt_ip>":
584 params[param] = str(values[parameter['value']])
585 else:
586 """
587 The Juju API uses strictly typed data-types, so we must make
588 sure the parameters from the VNFD match the appropriate type.
589
590 The honus will still be on the operator, to make sure the
591 data-type in the VNFD matches the one in the charm. N2VC will
592 raise N2VCPrimitiveExecutionFailed when there is a mismatch.
593
594 There are three data types supported by the YANG model:
595 # - STRING
596 # - INTEGER
597 # - BOOLEAN
598
599 Each parameter will look like this:
600 {
601 'seq': '3',
602 'name': 'testint',
603 'parameter': [
604 {
605 'name': 'interval',
606 'data-type': 'INTEGER',
607 'value': 20
608 }
609 ]
610 }
611 """
612
613 if 'value' in parameter:
614 # String is the default format
615 val = str(parameter['value'])
616
617 # If the data-type is explicitly set, cast to that type.
618 if 'data-type' in parameter:
619 dt = parameter['data-type'].upper()
620 if dt == "INTEGER":
621 val = int(val)
622
623 elif dt == "BOOLEAN":
624 if val in ['true', 'false', '0', '1']:
625 val = True
626 else:
627 val = False
628
629 params[param] = val
630 return params
631
632 def _get_config_from_yang(self, config_primitive, values):
633 """Transform the yang config primitive to dict."""
634 config = {}
635 for primitive in config_primitive.values():
636 if primitive['name'] == 'config':
637 for parameter in primitive['parameter'].values():
638 param = str(parameter['name'])
639 if parameter['value'] == "<rw_mgmt_ip>":
640 config[param] = str(values[parameter['value']])
641 else:
642 config[param] = str(parameter['value'])
643
644 return config
645
646 def FormatApplicationName(self, *args):
647 """
648 Generate a Juju-compatible Application name
649
650 :param args tuple: Positional arguments to be used to construct the
651 application name.
652
653 Limitations::
654 - Only accepts characters a-z and non-consequitive dashes (-)
655 - Application name should not exceed 50 characters
656
657 Examples::
658
659 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
660 """
661
662 appname = ""
663 for c in "-".join(list(args)):
664 if c.isdigit():
665 c = chr(97 + int(c))
666 elif not c.isalpha():
667 c = "-"
668 appname += c
669 return re.sub('\-+', '-', appname.lower())
670
671
672 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
673 # """Format the name of the application
674 #
675 # Limitations:
676 # - Only accepts characters a-z and non-consequitive dashes (-)
677 # - Application name should not exceed 50 characters
678 # """
679 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
680 # new_name = ''
681 # for c in name:
682 # if c.isdigit():
683 # c = chr(97 + int(c))
684 # elif not c.isalpha():
685 # c = "-"
686 # new_name += c
687 # return re.sub('\-+', '-', new_name.lower())
688
689 def format_model_name(self, name):
690 """Format the name of model.
691
692 Model names may only contain lowercase letters, digits and hyphens
693 """
694
695 return name.replace('_', '-').lower()
696
697 async def get_application(self, model, application):
698 """Get the deployed application."""
699 if not self.authenticated:
700 await self.login()
701
702 app = None
703 if application and model:
704 if model.applications:
705 if application in model.applications:
706 app = model.applications[application]
707
708 return app
709
710 async def get_model(self, model_name='default'):
711 """Get a model from the Juju Controller.
712
713 Note: Model objects returned must call disconnected() before it goes
714 out of scope."""
715 if not self.authenticated:
716 await self.login()
717
718 if model_name not in self.models:
719 print("connecting to model {}".format(model_name))
720 self.models[model_name] = await self.controller.get_model(model_name)
721
722 # Create an observer for this model
723 self.monitors[model_name] = VCAMonitor(model_name)
724 self.models[model_name].add_observer(self.monitors[model_name])
725
726 return self.models[model_name]
727
728 async def login(self):
729 """Login to the Juju controller."""
730
731 if self.authenticated:
732 return
733
734 self.connecting = True
735
736 self.log.debug("JujuApi: Logging into controller")
737
738 cacert = None
739 self.controller = Controller()
740
741 if self.secret:
742 self.log.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self.endpoint, self.port, self.user, self.secret))
743 await self.controller.connect(
744 endpoint=self.endpoint,
745 username=self.user,
746 password=self.secret,
747 cacert=cacert,
748 )
749 else:
750 # current_controller no longer exists
751 # self.log.debug("Connecting to current controller...")
752 # await self.controller.connect_current()
753 # await self.controller.connect(
754 # endpoint=self.endpoint,
755 # username=self.user,
756 # cacert=cacert,
757 # )
758 self.log.fatal("VCA credentials not configured.")
759
760 self.authenticated = True
761 self.log.debug("JujuApi: Logged into controller")
762
763 # self.default_model = await self.controller.get_model("default")
764
765 async def logout(self):
766 """Logout of the Juju controller."""
767 if not self.authenticated:
768 return
769
770 try:
771 if self.default_model:
772 self.log.debug("Disconnecting model {}".format(self.default_model))
773 await self.default_model.disconnect()
774 self.default_model = None
775
776 for model in self.models:
777 await self.models[model].disconnect()
778
779 if self.controller:
780 self.log.debug("Disconnecting controller {}".format(self.controller))
781 await self.controller.disconnect()
782 # self.controller = None
783
784 self.authenticated = False
785 except Exception as e:
786 self.log.fail("Fatal error logging out of Juju Controller: {}".format(e))
787 raise e
788
789
790 # async def remove_application(self, name):
791 # """Remove the application."""
792 # if not self.authenticated:
793 # await self.login()
794 #
795 # app = await self.get_application(name)
796 # if app:
797 # self.log.debug("JujuApi: Destroying application {}".format(
798 # name,
799 # ))
800 #
801 # await app.destroy()
802
803 async def remove_relation(self, a, b):
804 """
805 Remove a relation between two application endpoints
806
807 :param a An application endpoint
808 :param b An application endpoint
809 """
810 if not self.authenticated:
811 await self.login()
812
813 m = await self.get_model()
814 try:
815 m.remove_relation(a, b)
816 finally:
817 await m.disconnect()
818
819 async def resolve_error(self, application=None):
820 """Resolve units in error state."""
821 if not self.authenticated:
822 await self.login()
823
824 app = await self.get_application(self.default_model, application)
825 if app:
826 self.log.debug("JujuApi: Resolving errors for application {}".format(
827 application,
828 ))
829
830 for unit in app.units:
831 app.resolved(retry=True)
832
833 async def run_action(self, application, action_name, **params):
834 """Execute an action and return an Action object."""
835 if not self.authenticated:
836 await self.login()
837 result = {
838 'status': '',
839 'action': {
840 'tag': None,
841 'results': None,
842 }
843 }
844 app = await self.get_application(self.default_model, application)
845 if app:
846 # We currently only have one unit per application
847 # so use the first unit available.
848 unit = app.units[0]
849
850 self.log.debug("JujuApi: Running Action {} against Application {}".format(
851 action_name,
852 application,
853 ))
854
855 action = await unit.run_action(action_name, **params)
856
857 # Wait for the action to complete
858 await action.wait()
859
860 result['status'] = action.status
861 result['action']['tag'] = action.data['id']
862 result['action']['results'] = action.results
863
864 return result
865
866 async def set_config(self, model_name, application, config):
867 """Apply a configuration to the application."""
868 if not self.authenticated:
869 await self.login()
870
871 app = await self.get_application(model_name, application)
872 if app:
873 self.log.debug("JujuApi: Setting config for Application {}".format(
874 application,
875 ))
876 await app.set_config(config)
877
878 # Verify the config is set
879 newconf = await app.get_config()
880 for key in config:
881 if config[key] != newconf[key]['value']:
882 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
883
884 # async def set_parameter(self, parameter, value, application=None):
885 # """Set a config parameter for a service."""
886 # if not self.authenticated:
887 # await self.login()
888 #
889 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
890 # parameter,
891 # value,
892 # application,
893 # ))
894 # return await self.apply_config(
895 # {parameter: value},
896 # application=application,
897 # )
898
899 async def wait_for_application(self, name, timeout=300):
900 """Wait for an application to become active."""
901 if not self.authenticated:
902 await self.login()
903
904 app = await self.get_application(self.default_model, name)
905 if app:
906 self.log.debug(
907 "JujuApi: Waiting {} seconds for Application {}".format(
908 timeout,
909 name,
910 )
911 )
912
913 await self.default_model.block_until(
914 lambda: all(
915 unit.agent_status == 'idle'
916 and unit.workload_status
917 in ['active', 'unknown'] for unit in app.units
918 ),
919 timeout=timeout
920 )