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
15 path
= os
.path
.abspath(os
.path
.join(os
.path
.dirname(__file__
), '..'))
16 path
= os
.path
.join(path
, "modules/libjuju/")
17 if path
not in sys
.path
:
18 sys
.path
.insert(1, path
)
20 from juju
.controller
import Controller
21 from juju
.model
import ModelObserver
22 from juju
.errors
import JujuAPIError
24 # We might need this to connect to the websocket securely, but test and verify.
26 ssl
._create
_default
_https
_context
= ssl
._create
_unverified
_context
27 except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
34 class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
38 class JujuApplicationExists(Exception):
39 """The Application already exists."""
42 class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
46 # Quiet the debug logging
47 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
48 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
49 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
50 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
53 class VCAMonitor(ModelObserver
):
54 """Monitor state changes within the Juju Model."""
59 def __init__(self
, ns_name
):
60 self
.log
= logging
.getLogger(__name__
)
62 self
.ns_name
= ns_name
64 def AddApplication(self
, application_name
, callback
, *callback_args
):
65 if application_name
not in self
.applications
:
66 self
.applications
[application_name
] = {
68 'callback_args': callback_args
71 def RemoveApplication(self
, application_name
):
72 if application_name
in self
.applications
:
73 del self
.applications
[application_name
]
75 async def on_change(self
, delta
, old
, new
, model
):
76 """React to changes in the Juju model."""
78 if delta
.entity
== "unit":
79 # Ignore change events from other applications
80 if delta
.data
['application'] not in self
.applications
.keys():
85 application_name
= delta
.data
['application']
87 callback
= self
.applications
[application_name
]['callback']
89 self
.applications
[application_name
]['callback_args']
92 # Fire off a callback with the application state
96 delta
.data
['application'],
98 new
.workload_status_message
,
102 # This is a charm being removed
106 delta
.data
['application'],
110 except Exception as e
:
111 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
113 elif delta
.entity
== "action":
114 # TODO: Decide how we want to notify the user of actions
116 # uuid = delta.data['id'] # The Action's unique id
117 # msg = delta.data['message'] # The output of the action
119 # if delta.data['status'] == "pending":
120 # # The action is queued
122 # elif delta.data['status'] == "completed""
123 # # The action was successful
125 # elif delta.data['status'] == "failed":
126 # # The action failed.
134 # Create unique models per network service
135 # Document all public functions
150 :param vcaconfig dict A dictionary containing the VCA configuration
152 :param artifacts str The directory where charms required by a vnfd are
156 n2vc = N2VC(vcaconfig={
157 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
159 'ip-address': '10.44.127.137',
161 'artifacts': '/path/to/charms'
165 # Initialize instance-level variables
168 self
.controller
= None
169 self
.connecting
= False
170 self
.authenticated
= False
192 self
.log
= logging
.getLogger(__name__
)
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)
200 self
.log
.debug('JujuApi: instantiated')
206 if user
.startswith('user-'):
209 self
.user
= 'user-{}'.format(user
)
211 self
.endpoint
= '%s:%d' % (server
, int(port
))
213 self
.artifacts
= artifacts
215 self
.loop
= loop
or asyncio
.get_event_loop()
218 """Close any open connections."""
221 def notify_callback(self
, model_name
, application_name
, status
, message
,
222 callback
=None, *callback_args
):
231 except Exception as e
:
232 self
.log
.error("[0] notify_callback exception {}".format(e
))
237 async def Relate(self
, model_name
, vnfd
):
238 """Create a relation between the charm-enabled VDUs in a VNF.
240 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
245 - provides: dataVM:db
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.
250 :param str ns_name: The name of the network service.
251 :param dict vnfd: The parsed yaml VNF descriptor.
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.
261 vnf_config
= vnfd
.get("vnf-configuration")
263 juju
= vnf_config
['juju']
265 configs
.append(vnf_config
)
267 for vdu
in vnfd
['vdu']:
268 vdu_config
= vdu
.get('vdu-configuration')
270 juju
= vdu_config
['juju']
272 configs
.append(vdu_config
)
274 def _get_application_name(name
):
275 """Get the application name that's mapped to a vnf/vdu."""
277 vnf_name
= vnfd
['name']
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(
285 str(vnf_member_index
),
287 return application_name
289 vnf_member_index
+= 1
293 # Loop through relations
296 if 'relation' in juju
:
297 for rel
in juju
['relation']:
300 # get the application name for the provides
301 (name
, endpoint
) = rel
['provides'].split(':')
302 application_name
= _get_application_name(name
)
304 provides
= "{}:{}".format(
309 # get the application name for thr requires
310 (name
, endpoint
) = rel
['requires'].split(':')
311 application_name
= _get_application_name(name
)
313 requires
= "{}:{}".format(
317 self
.log
.debug("Relation: {} <-> {}".format(
321 await self
.add_relation(
326 except Exception as e
:
327 self
.log
.debug("Exception: {}".format(e
))
331 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
332 charm_path
, params
={}, machine_spec
={},
333 callback
=None, *callback_args
):
334 """Deploy one or more charms associated with a VNF.
336 Deploy the charm(s) referenced in a VNF Descriptor.
338 :param str model_name: The name or unique id of the network service.
339 :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
345 'rw_mgmt_ip': '1.2.3.4',
346 # Pass the initial-config-primitives section of the vnf or vdu
347 'initial-config-primitives': {...}
349 :param dict machine_spec: A dictionary describing the machine to
353 'hostname': '1.2.3.4',
354 'username': 'ubuntu',
356 :param obj callback: A callback function to receive status changes.
357 :param tuple callback_args: A list of arguments to be passed to the
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
))
366 self
.notify_callback(
373 raise JujuCharmNotFound("No artifacts configured.")
375 ################################
376 # Login to the Juju controller #
377 ################################
378 if not self
.authenticated
:
379 self
.log
.debug("Authenticating with Juju")
382 ##########################################
383 # Get the model for this network service #
384 ##########################################
385 model
= await self
.get_model(model_name
)
387 ########################################
388 # Verify the application doesn't exist #
389 ########################################
390 app
= await self
.get_application(model
, application_name
)
392 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
394 ################################################################
395 # Register this application with the model-level event monitor #
396 ################################################################
398 self
.monitors
[model_name
].AddApplication(
404 ########################################################
405 # Check for specific machine placement (native charms) #
406 ########################################################
408 if machine_spec
.keys():
409 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(),
418 #######################################
419 # Get the initial charm configuration #
420 #######################################
423 if 'rw_mgmt_ip' in params
:
424 rw_mgmt_ip
= params
['rw_mgmt_ip']
426 if 'initial-config-primitive' not in params
:
427 params
['initial-config-primitive'] = {}
429 initial_config
= self
._get
_config
_from
_dict
(
430 params
['initial-config-primitive'],
431 {'<rw_mgmt_ip>': rw_mgmt_ip
}
434 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
441 ########################################################
442 # Deploy the charm and apply the initial configuration #
443 ########################################################
444 app
= await model
.deploy(
445 # We expect charm_path to be either the path to the charm on disk
446 # or in the format of cs:series/name
448 # This is the formatted, unique name for this charm
449 application_name
=application_name
,
450 # Proxy charms should use the current LTS. This will need to be
451 # changed for native charms.
453 # Apply the initial 'config' primitive during deployment
454 config
=initial_config
,
455 # Where to deploy the charm to.
459 # Map the vdu id<->app name,
461 await self
.Relate(model_name
, vnfd
)
463 # #######################################
464 # # Execute initial config primitive(s) #
465 # #######################################
466 uuids
= await self
.ExecuteInitialPrimitives(
475 # # Build a sequential list of the primitives to execute
476 # for primitive in params['initial-config-primitive']:
478 # if primitive['name'] == 'config':
479 # # This is applied when the Application is deployed
482 # seq = primitive['seq']
485 # if 'parameter' in primitive:
486 # params = primitive['parameter']
488 # primitives[seq] = {
489 # 'name': primitive['name'],
490 # 'parameters': self._map_primitive_parameters(
492 # {'<rw_mgmt_ip>': rw_mgmt_ip}
496 # for primitive in sorted(primitives):
497 # await self.ExecutePrimitive(
500 # primitives[primitive]['name'],
503 # **primitives[primitive]['parameters'],
505 # except N2VCPrimitiveExecutionFailed as e:
507 # "[N2VC] Exception executing primitive: {}".format(e)
511 async def GetPrimitiveStatus(self
, model_name
, uuid
):
512 """Get the status of an executed Primitive.
514 The status of an executed Primitive will be one of three values:
521 if not self
.authenticated
:
524 model
= await self
.get_model(model_name
)
526 results
= await model
.get_action_status(uuid
)
529 status
= results
[uuid
]
531 except Exception as e
:
533 "Caught exception while getting primitive status: {}".format(e
)
535 raise N2VCPrimitiveExecutionFailed(e
)
539 async def GetPrimitiveOutput(self
, model_name
, uuid
):
540 """Get the output of an executed Primitive.
542 Note: this only returns output for a successfully executed primitive.
546 if not self
.authenticated
:
549 model
= await self
.get_model(model_name
)
550 results
= await model
.get_action_output(uuid
, 60)
551 except Exception as e
:
553 "Caught exception while getting primitive status: {}".format(e
)
555 raise N2VCPrimitiveExecutionFailed(e
)
559 # async def ProvisionMachine(self, model_name, hostname, username):
560 # """Provision machine for usage with Juju.
562 # Provisions a previously instantiated machine for use with Juju.
565 # if not self.authenticated:
568 # # FIXME: This is hard-coded until model-per-ns is added
569 # model_name = 'default'
571 # model = await self.get_model(model_name)
572 # model.add_machine(spec={})
574 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
581 # except Exception as e:
583 # "Caught exception while getting primitive status: {}".format(e)
585 # raise N2VCPrimitiveExecutionFailed(e)
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
593 async def GetPublicKey(self
):
594 """Get the N2VC SSH public key.abs
596 Returns the SSH public key, to be injected into virtual machines to
597 be managed by the VCA.
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
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
):
612 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
613 public_key_path
= "{}.pub".format(private_key_path
)
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(
622 subprocess
.check_output(shlex
.split(cmd
))
624 # Read the public key
625 with
open(public_key_path
, "r") as f
:
626 public_key
= f
.readline()
630 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
631 params
, callback
=None, *callback_args
):
632 """Execute multiple primitives.
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
642 # Build a sequential list of the primitives to execute
643 for primitive
in params
['initial-config-primitive']:
645 if primitive
['name'] == 'config':
648 seq
= primitive
['seq']
651 if 'parameter' in primitive
:
652 params
= primitive
['parameter']
655 'name': primitive
['name'],
656 'parameters': self
._map
_primitive
_parameters
(
658 {'<rw_mgmt_ip>': None}
662 for primitive
in sorted(primitives
):
664 await self
.ExecutePrimitive(
667 primitives
[primitive
]['name'],
670 **primitives
[primitive
]['parameters'],
673 except N2VCPrimitiveExecutionFailed
as e
:
675 "[N2VC] Exception executing primitive: {}".format(e
)
680 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
681 callback
, *callback_args
, **params
):
682 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
684 Execute a primitive defined in the VNF descriptor.
686 :param str model_name: The name or unique id of the network service.
687 :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.
690 :param tuple callback_args: A list of arguments to be passed to the
692 :param dict params: A dictionary of key=value pairs representing the
693 primitive's parameters
696 'rw_mgmt_ip': '1.2.3.4',
697 # Pass the initial-config-primitives section of the vnf or vdu
698 'initial-config-primitives': {...}
701 self
.log
.debug("Executing {}".format(primitive
))
704 if not self
.authenticated
:
707 model
= await self
.get_model(model_name
)
709 if primitive
== 'config':
710 # config is special, and expecting params to be a dictionary
711 await self
.set_config(
717 app
= await self
.get_application(model
, application_name
)
719 # Run against the first (and probably only) unit in the app
722 action
= await unit
.run_action(primitive
, **params
)
724 except Exception as e
:
726 "Caught exception while executing primitive: {}".format(e
)
728 raise N2VCPrimitiveExecutionFailed(e
)
731 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
733 """Remove a charm from the VCA.
735 Remove a charm referenced in a VNF Descriptor.
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.
740 :param tuple callback_args: A list of arguments to be passed to the
744 if not self
.authenticated
:
747 model
= await self
.get_model(model_name
)
748 app
= await self
.get_application(model
, application_name
)
750 # Remove this application from event monitoring
751 self
.monitors
[model_name
].RemoveApplication(application_name
)
753 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
755 "Removing the application {}".format(application_name
)
759 await self
.disconnect_model(self
.monitors
[model_name
])
761 # Notify the callback that this charm has been removed.
762 self
.notify_callback(
770 except Exception as e
:
771 print("Caught exception: {}".format(e
))
775 async def DestroyNetworkService(self
, nsd
):
776 raise NotImplementedError()
778 async def GetMetrics(self
, model_name
, application_name
):
779 """Get the metrics collected by the VCA.
781 :param model_name The name or unique id of the network service
782 :param application_name The name of the application
785 model
= await self
.get_model(model_name
)
786 app
= await self
.get_application(model
, application_name
)
788 metrics
= await app
.get_metrics()
792 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
)
800 async def add_relation(self
, model_name
, relation1
, relation2
):
802 Add a relation between two application endpoints.
804 :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>]'
809 if not self
.authenticated
:
812 m
= await self
.get_model(model_name
)
814 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
819 if 'not found' in e
.message
:
821 if 'already exists' in e
.message
:
826 # async def apply_config(self, config, application):
827 # """Apply a configuration to the application."""
828 # print("JujuApi: Applying configuration to {}.".format(
831 # return await self.set_config(application=application, config=config)
833 def _get_config_from_dict(self
, config_primitive
, values
):
834 """Transform the yang config primitive to dict.
843 for primitive
in config_primitive
:
844 if primitive
['name'] == 'config':
845 # config = self._map_primitive_parameters()
846 for parameter
in primitive
['parameter']:
847 param
= str(parameter
['name'])
848 if parameter
['value'] == "<rw_mgmt_ip>":
849 config
[param
] = str(values
[parameter
['value']])
851 config
[param
] = str(parameter
['value'])
855 def _map_primitive_parameters(self
, parameters
, values
):
857 for parameter
in parameters
:
858 param
= str(parameter
['name'])
861 # 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']
865 # Typecast parameter value, if present
866 if 'data-type' in parameter
:
867 paramtype
= str(parameter
['data-type']).lower()
869 if paramtype
== "integer":
870 value
= int(parameter
['value'])
871 elif paramtype
== "boolean":
872 value
= bool(parameter
['value'])
874 value
= str(parameter
['value'])
876 # If there's no data-type, assume the value is a string
877 value
= str(parameter
['value'])
879 if parameter
['value'] == "<rw_mgmt_ip>":
880 params
[param
] = str(values
[parameter
['value']])
882 params
[param
] = value
885 def _get_config_from_yang(self
, config_primitive
, values
):
886 """Transform the yang config primitive to dict."""
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']])
895 config
[param
] = str(parameter
['value'])
899 def FormatApplicationName(self
, *args
):
901 Generate a Juju-compatible Application name
903 :param args tuple: Positional arguments to be used to construct the
907 - Only accepts characters a-z and non-consequitive dashes (-)
908 - Application name should not exceed 50 characters
912 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
915 for c
in "-".join(list(args
)):
918 elif not c
.isalpha():
921 return re
.sub('\-+', '-', appname
.lower())
923 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
924 # """Format the name of the application
927 # - Only accepts characters a-z and non-consequitive dashes (-)
928 # - Application name should not exceed 50 characters
930 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
934 # c = chr(97 + int(c))
935 # elif not c.isalpha():
938 # return re.sub('\-+', '-', new_name.lower())
940 def format_model_name(self
, name
):
941 """Format the name of model.
943 Model names may only contain lowercase letters, digits and hyphens
946 return name
.replace('_', '-').lower()
948 async def get_application(self
, model
, application
):
949 """Get the deployed application."""
950 if not self
.authenticated
:
954 if application
and model
:
955 if model
.applications
:
956 if application
in model
.applications
:
957 app
= model
.applications
[application
]
961 async def get_model(self
, model_name
):
962 """Get a model from the Juju Controller.
964 Note: Model objects returned must call disconnected() before it goes
966 if not self
.authenticated
:
969 if model_name
not in self
.models
:
970 # Get the models in the controller
971 models
= await self
.controller
.list_models()
973 if model_name
not in models
:
974 self
.models
[model_name
] = await self
.controller
.add_model(
978 self
.models
[model_name
] = await self
.controller
.get_model(
982 self
.refcount
['model'] += 1
984 # 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
])
988 return self
.models
[model_name
]
990 async def login(self
):
991 """Login to the Juju controller."""
993 if self
.authenticated
:
996 self
.connecting
= True
998 self
.log
.debug("JujuApi: Logging into controller")
1001 self
.controller
= Controller(loop
=self
.loop
)
1005 "Connecting to controller... ws://{}:{} as {}/{}".format(
1012 await self
.controller
.connect(
1013 endpoint
=self
.endpoint
,
1015 password
=self
.secret
,
1018 self
.refcount
['controller'] += 1
1020 # current_controller no longer exists
1021 # self.log.debug("Connecting to current controller...")
1022 # await self.controller.connect_current()
1023 # await self.controller.connect(
1024 # endpoint=self.endpoint,
1025 # username=self.user,
1028 self
.log
.fatal("VCA credentials not configured.")
1030 self
.authenticated
= True
1031 self
.log
.debug("JujuApi: Logged into controller")
1033 async def logout(self
):
1034 """Logout of the Juju controller."""
1035 if not self
.authenticated
:
1039 for model
in self
.models
:
1040 await self
.disconnect_model(model
)
1043 self
.log
.debug("Disconnecting controller {}".format(
1046 await self
.controller
.disconnect()
1047 self
.refcount
['controller'] -= 1
1048 self
.controller
= None
1050 self
.authenticated
= False
1052 self
.log
.debug(self
.refcount
)
1054 except Exception as e
:
1056 "Fatal error logging out of Juju Controller: {}".format(e
)
1060 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
)
1068 print("Disconnecting model")
1069 await self
.models
[model
].disconnect()
1070 self
.refcount
['model'] -= 1
1071 self
.models
[model
] = None
1073 # async def remove_application(self, name):
1074 # """Remove the application."""
1075 # if not self.authenticated:
1076 # await self.login()
1078 # app = await self.get_application(name)
1080 # self.log.debug("JujuApi: Destroying application {}".format(
1084 # await app.destroy()
1086 async def remove_relation(self
, a
, b
):
1088 Remove a relation between two application endpoints
1090 :param a An application endpoint
1091 :param b An application endpoint
1093 if not self
.authenticated
:
1096 m
= await self
.get_model()
1098 m
.remove_relation(a
, b
)
1100 await m
.disconnect()
1102 async def resolve_error(self
, model_name
, application
=None):
1103 """Resolve units in error state."""
1104 if not self
.authenticated
:
1107 model
= await self
.get_model(model_name
)
1109 app
= await self
.get_application(model
, application
)
1112 "JujuApi: Resolving errors for application {}".format(
1117 for unit
in app
.units
:
1118 app
.resolved(retry
=True)
1120 async def run_action(self
, model_name
, application
, action_name
, **params
):
1121 """Execute an action and return an Action object."""
1122 if not self
.authenticated
:
1132 model
= await self
.get_model(model_name
)
1134 app
= await self
.get_application(model
, application
)
1136 # We currently only have one unit per application
1137 # so use the first unit available.
1141 "JujuApi: Running Action {} against Application {}".format(
1147 action
= await unit
.run_action(action_name
, **params
)
1149 # Wait for the action to complete
1152 result
['status'] = action
.status
1153 result
['action']['tag'] = action
.data
['id']
1154 result
['action']['results'] = action
.results
1158 async def set_config(self
, model_name
, application
, config
):
1159 """Apply a configuration to the application."""
1160 if not self
.authenticated
:
1163 app
= await self
.get_application(model_name
, application
)
1165 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1168 await app
.set_config(config
)
1170 # Verify the config is set
1171 newconf
= await app
.get_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
]))
1176 # 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()
1181 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1186 # return await self.apply_config(
1187 # {parameter: value},
1188 # application=application,
1191 async def wait_for_application(self
, model_name
, application_name
,
1193 """Wait for an application to become active."""
1194 if not self
.authenticated
:
1197 model
= await self
.get_model(model_name
)
1199 app
= await self
.get_application(model
, application_name
)
1200 self
.log
.debug("Application: {}".format(app
))
1203 "JujuApi: Waiting {} seconds for Application {}".format(
1209 await model
.block_until(
1211 unit
.agent_status
== 'idle' and unit
.workload_status
in
1212 ['active', 'unknown'] for unit
in app
.units