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': {...}
348 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
349 inside < >. rw_mgmt_ip will be included here also
351 :param dict machine_spec: A dictionary describing the machine to
355 'hostname': '1.2.3.4',
356 'username': 'ubuntu',
358 :param obj callback: A callback function to receive status changes.
359 :param tuple callback_args: A list of arguments to be passed to the
363 ########################################################
364 # Verify the path to the charm exists and is readable. #
365 ########################################################
366 if not os
.path
.exists(charm_path
):
367 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
368 self
.notify_callback(
375 raise JujuCharmNotFound("No artifacts configured.")
377 ################################
378 # Login to the Juju controller #
379 ################################
380 if not self
.authenticated
:
381 self
.log
.debug("Authenticating with Juju")
384 ##########################################
385 # Get the model for this network service #
386 ##########################################
387 model
= await self
.get_model(model_name
)
389 ########################################
390 # Verify the application doesn't exist #
391 ########################################
392 app
= await self
.get_application(model
, application_name
)
394 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
396 ################################################################
397 # Register this application with the model-level event monitor #
398 ################################################################
400 self
.monitors
[model_name
].AddApplication(
406 ########################################################
407 # Check for specific machine placement (native charms) #
408 ########################################################
410 if machine_spec
.keys():
411 if all(k
in machine_spec
for k
in ['host', 'user']):
412 # Enlist an existing machine as a Juju unit
413 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
414 machine_spec
['user'],
415 machine_spec
['host'],
416 self
.GetPrivateKeyPath(),
420 #######################################
421 # Get the initial charm configuration #
422 #######################################
425 if 'rw_mgmt_ip' in params
:
426 rw_mgmt_ip
= params
['rw_mgmt_ip']
428 if 'initial-config-primitive' not in params
:
429 params
['initial-config-primitive'] = {}
431 initial_config
= self
._get
_config
_from
_dict
(
432 params
['initial-config-primitive'],
433 {'<rw_mgmt_ip>': rw_mgmt_ip
}
436 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
443 ########################################################
444 # Deploy the charm and apply the initial configuration #
445 ########################################################
446 app
= await model
.deploy(
447 # We expect charm_path to be either the path to the charm on disk
448 # or in the format of cs:series/name
450 # This is the formatted, unique name for this charm
451 application_name
=application_name
,
452 # Proxy charms should use the current LTS. This will need to be
453 # changed for native charms.
455 # Apply the initial 'config' primitive during deployment
456 config
=initial_config
,
457 # Where to deploy the charm to.
461 # Map the vdu id<->app name,
463 await self
.Relate(model_name
, vnfd
)
465 # #######################################
466 # # Execute initial config primitive(s) #
467 # #######################################
468 uuids
= await self
.ExecuteInitialPrimitives(
477 # # Build a sequential list of the primitives to execute
478 # for primitive in params['initial-config-primitive']:
480 # if primitive['name'] == 'config':
481 # # This is applied when the Application is deployed
484 # seq = primitive['seq']
487 # if 'parameter' in primitive:
488 # params = primitive['parameter']
490 # primitives[seq] = {
491 # 'name': primitive['name'],
492 # 'parameters': self._map_primitive_parameters(
494 # {'<rw_mgmt_ip>': rw_mgmt_ip}
498 # for primitive in sorted(primitives):
499 # await self.ExecutePrimitive(
502 # primitives[primitive]['name'],
505 # **primitives[primitive]['parameters'],
507 # except N2VCPrimitiveExecutionFailed as e:
509 # "[N2VC] Exception executing primitive: {}".format(e)
513 async def GetPrimitiveStatus(self
, model_name
, uuid
):
514 """Get the status of an executed Primitive.
516 The status of an executed Primitive will be one of three values:
523 if not self
.authenticated
:
526 model
= await self
.get_model(model_name
)
528 results
= await model
.get_action_status(uuid
)
531 status
= results
[uuid
]
533 except Exception as e
:
535 "Caught exception while getting primitive status: {}".format(e
)
537 raise N2VCPrimitiveExecutionFailed(e
)
541 async def GetPrimitiveOutput(self
, model_name
, uuid
):
542 """Get the output of an executed Primitive.
544 Note: this only returns output for a successfully executed primitive.
548 if not self
.authenticated
:
551 model
= await self
.get_model(model_name
)
552 results
= await model
.get_action_output(uuid
, 60)
553 except Exception as e
:
555 "Caught exception while getting primitive status: {}".format(e
)
557 raise N2VCPrimitiveExecutionFailed(e
)
561 # async def ProvisionMachine(self, model_name, hostname, username):
562 # """Provision machine for usage with Juju.
564 # Provisions a previously instantiated machine for use with Juju.
567 # if not self.authenticated:
570 # # FIXME: This is hard-coded until model-per-ns is added
571 # model_name = 'default'
573 # model = await self.get_model(model_name)
574 # model.add_machine(spec={})
576 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
583 # except Exception as e:
585 # "Caught exception while getting primitive status: {}".format(e)
587 # raise N2VCPrimitiveExecutionFailed(e)
589 def GetPrivateKeyPath(self
):
590 homedir
= os
.environ
['HOME']
591 sshdir
= "{}/.ssh".format(homedir
)
592 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
593 return private_key_path
595 async def GetPublicKey(self
):
596 """Get the N2VC SSH public key.abs
598 Returns the SSH public key, to be injected into virtual machines to
599 be managed by the VCA.
601 The first time this is run, a ssh keypair will be created. The public
602 key is injected into a VM so that we can provision the machine with
603 Juju, after which Juju will communicate with the VM directly via the
608 # Find the path to where we expect our key to live.
609 homedir
= os
.environ
['HOME']
610 sshdir
= "{}/.ssh".format(homedir
)
611 if not os
.path
.exists(sshdir
):
614 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
615 public_key_path
= "{}.pub".format(private_key_path
)
617 # If we don't have a key generated, generate it.
618 if not os
.path
.exists(private_key_path
):
619 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
624 subprocess
.check_output(shlex
.split(cmd
))
626 # Read the public key
627 with
open(public_key_path
, "r") as f
:
628 public_key
= f
.readline()
632 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
633 params
, callback
=None, *callback_args
):
634 """Execute multiple primitives.
636 Execute multiple primitives as declared in initial-config-primitive.
637 This is useful in cases where the primitives initially failed -- for
638 example, if the charm is a proxy but the proxy hasn't been configured
644 # Build a sequential list of the primitives to execute
645 for primitive
in params
['initial-config-primitive']:
647 if primitive
['name'] == 'config':
650 seq
= primitive
['seq']
653 if 'parameter' in primitive
:
654 params_
= primitive
['parameter']
656 user_values
= params
.get("user_values", {})
657 if 'rw_mgmt_ip' not in user_values
:
658 user_values
['rw_mgmt_ip'] = None
659 # just for backward compatibility, because it will be provided always by modern version of LCM
662 'name': primitive
['name'],
663 'parameters': self
._map
_primitive
_parameters
(
669 for primitive
in sorted(primitives
):
671 await self
.ExecutePrimitive(
674 primitives
[primitive
]['name'],
677 **primitives
[primitive
]['parameters'],
680 except N2VCPrimitiveExecutionFailed
as e
:
682 "[N2VC] Exception executing primitive: {}".format(e
)
687 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
688 callback
, *callback_args
, **params
):
689 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
691 Execute a primitive defined in the VNF descriptor.
693 :param str model_name: The name or unique id of the network service.
694 :param str application_name: The name of the application
695 :param str primitive: The name of the primitive to execute.
696 :param obj callback: A callback function to receive status changes.
697 :param tuple callback_args: A list of arguments to be passed to the
699 :param dict params: A dictionary of key=value pairs representing the
700 primitive's parameters
703 'rw_mgmt_ip': '1.2.3.4',
704 # Pass the initial-config-primitives section of the vnf or vdu
705 'initial-config-primitives': {...}
708 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
711 if not self
.authenticated
:
714 model
= await self
.get_model(model_name
)
716 if primitive
== 'config':
717 # config is special, and expecting params to be a dictionary
718 await self
.set_config(
724 app
= await self
.get_application(model
, application_name
)
726 # Run against the first (and probably only) unit in the app
729 action
= await unit
.run_action(primitive
, **params
)
731 except Exception as e
:
733 "Caught exception while executing primitive: {}".format(e
)
735 raise N2VCPrimitiveExecutionFailed(e
)
738 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
740 """Remove a charm from the VCA.
742 Remove a charm referenced in a VNF Descriptor.
744 :param str model_name: The name of the network service.
745 :param str application_name: The name of the application
746 :param obj callback: A callback function to receive status changes.
747 :param tuple callback_args: A list of arguments to be passed to the
751 if not self
.authenticated
:
754 model
= await self
.get_model(model_name
)
755 app
= await self
.get_application(model
, application_name
)
757 # Remove this application from event monitoring
758 self
.monitors
[model_name
].RemoveApplication(application_name
)
760 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
762 "Removing the application {}".format(application_name
)
766 await self
.disconnect_model(self
.monitors
[model_name
])
768 # Notify the callback that this charm has been removed.
769 self
.notify_callback(
777 except Exception as e
:
778 print("Caught exception: {}".format(e
))
782 async def DestroyNetworkService(self
, nsd
):
783 raise NotImplementedError()
785 async def GetMetrics(self
, model_name
, application_name
):
786 """Get the metrics collected by the VCA.
788 :param model_name The name or unique id of the network service
789 :param application_name The name of the application
792 model
= await self
.get_model(model_name
)
793 app
= await self
.get_application(model
, application_name
)
795 metrics
= await app
.get_metrics()
799 async def HasApplication(self
, model_name
, application_name
):
800 model
= await self
.get_model(model_name
)
801 app
= await self
.get_application(model
, application_name
)
807 async def add_relation(self
, model_name
, relation1
, relation2
):
809 Add a relation between two application endpoints.
811 :param str model_name: The name or unique id of the network service
812 :param str relation1: '<application>[:<relation_name>]'
813 :param str relation2: '<application>[:<relation_name>]'
816 if not self
.authenticated
:
819 m
= await self
.get_model(model_name
)
821 await m
.add_relation(relation1
, relation2
)
822 except JujuAPIError
as e
:
823 # If one of the applications in the relationship doesn't exist,
824 # or the relation has already been added, let the operation fail
826 if 'not found' in e
.message
:
828 if 'already exists' in e
.message
:
833 # async def apply_config(self, config, application):
834 # """Apply a configuration to the application."""
835 # print("JujuApi: Applying configuration to {}.".format(
838 # return await self.set_config(application=application, config=config)
840 def _get_config_from_dict(self
, config_primitive
, values
):
841 """Transform the yang config primitive to dict.
850 for primitive
in config_primitive
:
851 if primitive
['name'] == 'config':
852 # config = self._map_primitive_parameters()
853 for parameter
in primitive
['parameter']:
854 param
= str(parameter
['name'])
855 if parameter
['value'] == "<rw_mgmt_ip>":
856 config
[param
] = str(values
[parameter
['value']])
858 config
[param
] = str(parameter
['value'])
862 def _map_primitive_parameters(self
, parameters
, user_values
):
864 for parameter
in parameters
:
865 param
= str(parameter
['name'])
866 value
= parameter
.get('value')
868 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
869 # Must exist at user_values except if there is a default value
870 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
871 if parameter
['value'][1:-1] in user_values
:
872 value
= user_values
[parameter
['value'][1:-1]]
873 elif 'default-value' in parameter
:
874 value
= parameter
['default-value']
876 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
878 # If there's no value, use the default-value (if set)
879 if value
is None and 'default-value' in parameter
:
880 value
= parameter
['default-value']
882 # Typecast parameter value, if present
885 if 'data-type' in parameter
:
886 paramtype
= str(parameter
['data-type']).lower()
888 if paramtype
== "integer":
890 elif paramtype
== "boolean":
895 # If there's no data-type, assume the value is a string
898 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
900 params
[param
] = value
903 def _get_config_from_yang(self
, config_primitive
, values
):
904 """Transform the yang config primitive to dict."""
906 for primitive
in config_primitive
.values():
907 if primitive
['name'] == 'config':
908 for parameter
in primitive
['parameter'].values():
909 param
= str(parameter
['name'])
910 if parameter
['value'] == "<rw_mgmt_ip>":
911 config
[param
] = str(values
[parameter
['value']])
913 config
[param
] = str(parameter
['value'])
917 def FormatApplicationName(self
, *args
):
919 Generate a Juju-compatible Application name
921 :param args tuple: Positional arguments to be used to construct the
925 - Only accepts characters a-z and non-consequitive dashes (-)
926 - Application name should not exceed 50 characters
930 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
933 for c
in "-".join(list(args
)):
936 elif not c
.isalpha():
939 return re
.sub('\-+', '-', appname
.lower())
941 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
942 # """Format the name of the application
945 # - Only accepts characters a-z and non-consequitive dashes (-)
946 # - Application name should not exceed 50 characters
948 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
952 # c = chr(97 + int(c))
953 # elif not c.isalpha():
956 # return re.sub('\-+', '-', new_name.lower())
958 def format_model_name(self
, name
):
959 """Format the name of model.
961 Model names may only contain lowercase letters, digits and hyphens
964 return name
.replace('_', '-').lower()
966 async def get_application(self
, model
, application
):
967 """Get the deployed application."""
968 if not self
.authenticated
:
972 if application
and model
:
973 if model
.applications
:
974 if application
in model
.applications
:
975 app
= model
.applications
[application
]
979 async def get_model(self
, model_name
):
980 """Get a model from the Juju Controller.
982 Note: Model objects returned must call disconnected() before it goes
984 if not self
.authenticated
:
987 if model_name
not in self
.models
:
988 # Get the models in the controller
989 models
= await self
.controller
.list_models()
991 if model_name
not in models
:
992 self
.models
[model_name
] = await self
.controller
.add_model(
996 self
.models
[model_name
] = await self
.controller
.get_model(
1000 self
.refcount
['model'] += 1
1002 # Create an observer for this model
1003 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1004 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1006 return self
.models
[model_name
]
1008 async def login(self
):
1009 """Login to the Juju controller."""
1011 if self
.authenticated
:
1014 self
.connecting
= True
1016 self
.log
.debug("JujuApi: Logging into controller")
1019 self
.controller
= Controller(loop
=self
.loop
)
1023 "Connecting to controller... ws://{}:{} as {}/{}".format(
1030 await self
.controller
.connect(
1031 endpoint
=self
.endpoint
,
1033 password
=self
.secret
,
1036 self
.refcount
['controller'] += 1
1038 # current_controller no longer exists
1039 # self.log.debug("Connecting to current controller...")
1040 # await self.controller.connect_current()
1041 # await self.controller.connect(
1042 # endpoint=self.endpoint,
1043 # username=self.user,
1046 self
.log
.fatal("VCA credentials not configured.")
1048 self
.authenticated
= True
1049 self
.log
.debug("JujuApi: Logged into controller")
1051 async def logout(self
):
1052 """Logout of the Juju controller."""
1053 if not self
.authenticated
:
1057 for model
in self
.models
:
1058 await self
.disconnect_model(model
)
1061 self
.log
.debug("Disconnecting controller {}".format(
1064 await self
.controller
.disconnect()
1065 self
.refcount
['controller'] -= 1
1066 self
.controller
= None
1068 self
.authenticated
= False
1070 self
.log
.debug(self
.refcount
)
1072 except Exception as e
:
1074 "Fatal error logging out of Juju Controller: {}".format(e
)
1078 async def disconnect_model(self
, model
):
1079 self
.log
.debug("Disconnecting model {}".format(model
))
1080 if model
in self
.models
:
1081 print(self
.models
[model
].applications
)
1082 if len(self
.models
[model
].applications
) == 0:
1083 print("Destroying empty model")
1084 await self
.controller
.destroy_models(model
)
1086 print("Disconnecting model")
1087 await self
.models
[model
].disconnect()
1088 self
.refcount
['model'] -= 1
1089 self
.models
[model
] = None
1091 # async def remove_application(self, name):
1092 # """Remove the application."""
1093 # if not self.authenticated:
1094 # await self.login()
1096 # app = await self.get_application(name)
1098 # self.log.debug("JujuApi: Destroying application {}".format(
1102 # await app.destroy()
1104 async def remove_relation(self
, a
, b
):
1106 Remove a relation between two application endpoints
1108 :param a An application endpoint
1109 :param b An application endpoint
1111 if not self
.authenticated
:
1114 m
= await self
.get_model()
1116 m
.remove_relation(a
, b
)
1118 await m
.disconnect()
1120 async def resolve_error(self
, model_name
, application
=None):
1121 """Resolve units in error state."""
1122 if not self
.authenticated
:
1125 model
= await self
.get_model(model_name
)
1127 app
= await self
.get_application(model
, application
)
1130 "JujuApi: Resolving errors for application {}".format(
1135 for unit
in app
.units
:
1136 app
.resolved(retry
=True)
1138 async def run_action(self
, model_name
, application
, action_name
, **params
):
1139 """Execute an action and return an Action object."""
1140 if not self
.authenticated
:
1150 model
= await self
.get_model(model_name
)
1152 app
= await self
.get_application(model
, application
)
1154 # We currently only have one unit per application
1155 # so use the first unit available.
1159 "JujuApi: Running Action {} against Application {}".format(
1165 action
= await unit
.run_action(action_name
, **params
)
1167 # Wait for the action to complete
1170 result
['status'] = action
.status
1171 result
['action']['tag'] = action
.data
['id']
1172 result
['action']['results'] = action
.results
1176 async def set_config(self
, model_name
, application
, config
):
1177 """Apply a configuration to the application."""
1178 if not self
.authenticated
:
1181 app
= await self
.get_application(model_name
, application
)
1183 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1186 await app
.set_config(config
)
1188 # Verify the config is set
1189 newconf
= await app
.get_config()
1191 if config
[key
] != newconf
[key
]['value']:
1192 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1194 # async def set_parameter(self, parameter, value, application=None):
1195 # """Set a config parameter for a service."""
1196 # if not self.authenticated:
1197 # await self.login()
1199 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1204 # return await self.apply_config(
1205 # {parameter: value},
1206 # application=application,
1209 async def wait_for_application(self
, model_name
, application_name
,
1211 """Wait for an application to become active."""
1212 if not self
.authenticated
:
1215 model
= await self
.get_model(model_name
)
1217 app
= await self
.get_application(model
, application_name
)
1218 self
.log
.debug("Application: {}".format(app
))
1221 "JujuApi: Waiting {} seconds for Application {}".format(
1227 await model
.block_until(
1229 unit
.agent_status
== 'idle' and unit
.workload_status
in
1230 ['active', 'unknown'] for unit
in app
.units