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
, JujuError
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 class NetworkServiceDoesNotExist(Exception):
47 """The Network Service being acted against does not exist."""
50 # Quiet the debug logging
51 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
52 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
53 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
54 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
57 class VCAMonitor(ModelObserver
):
58 """Monitor state changes within the Juju Model."""
63 def __init__(self
, ns_name
):
64 self
.log
= logging
.getLogger(__name__
)
66 self
.ns_name
= ns_name
68 def AddApplication(self
, application_name
, callback
, *callback_args
):
69 if application_name
not in self
.applications
:
70 self
.applications
[application_name
] = {
72 'callback_args': callback_args
75 def RemoveApplication(self
, application_name
):
76 if application_name
in self
.applications
:
77 del self
.applications
[application_name
]
79 async def on_change(self
, delta
, old
, new
, model
):
80 """React to changes in the Juju model."""
82 if delta
.entity
== "unit":
83 # Ignore change events from other applications
84 if delta
.data
['application'] not in self
.applications
.keys():
89 application_name
= delta
.data
['application']
91 callback
= self
.applications
[application_name
]['callback']
93 self
.applications
[application_name
]['callback_args']
96 # Fire off a callback with the application state
100 delta
.data
['application'],
102 new
.workload_status_message
,
106 # This is a charm being removed
110 delta
.data
['application'],
114 except Exception as e
:
115 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
117 elif delta
.entity
== "action":
118 # TODO: Decide how we want to notify the user of actions
120 # uuid = delta.data['id'] # The Action's unique id
121 # msg = delta.data['message'] # The output of the action
123 # if delta.data['status'] == "pending":
124 # # The action is queued
126 # elif delta.data['status'] == "completed""
127 # # The action was successful
129 # elif delta.data['status'] == "failed":
130 # # The action failed.
138 # Create unique models per network service
139 # Document all public functions
154 :param vcaconfig dict A dictionary containing the VCA configuration
156 :param artifacts str The directory where charms required by a vnfd are
160 n2vc = N2VC(vcaconfig={
161 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
163 'ip-address': '10.44.127.137',
165 'artifacts': '/path/to/charms'
169 # Initialize instance-level variables
172 self
.controller
= None
173 self
.connecting
= False
174 self
.authenticated
= False
196 self
.log
= logging
.getLogger(__name__
)
198 # Quiet websocket traffic
199 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
200 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
201 logging
.getLogger('model').setLevel(logging
.WARN
)
202 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
204 self
.log
.debug('JujuApi: instantiated')
210 if user
.startswith('user-'):
213 self
.user
= 'user-{}'.format(user
)
215 self
.endpoint
= '%s:%d' % (server
, int(port
))
217 self
.artifacts
= artifacts
219 self
.loop
= loop
or asyncio
.get_event_loop()
222 """Close any open connections."""
225 def notify_callback(self
, model_name
, application_name
, status
, message
,
226 callback
=None, *callback_args
):
235 except Exception as e
:
236 self
.log
.error("[0] notify_callback exception {}".format(e
))
241 async def Relate(self
, model_name
, vnfd
):
242 """Create a relation between the charm-enabled VDUs in a VNF.
244 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
249 - provides: dataVM:db
252 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.
254 :param str ns_name: The name of the network service.
255 :param dict vnfd: The parsed yaml VNF descriptor.
258 # Currently, the call to Relate() is made automatically after the
259 # deployment of each charm; if the relation depends on a charm that
260 # hasn't been deployed yet, the call will fail silently. This will
261 # prevent an API breakage, with the intent of making this an explicitly
262 # required call in a more object-oriented refactor of the N2VC API.
265 vnf_config
= vnfd
.get("vnf-configuration")
267 juju
= vnf_config
['juju']
269 configs
.append(vnf_config
)
271 for vdu
in vnfd
['vdu']:
272 vdu_config
= vdu
.get('vdu-configuration')
274 juju
= vdu_config
['juju']
276 configs
.append(vdu_config
)
278 def _get_application_name(name
):
279 """Get the application name that's mapped to a vnf/vdu."""
281 vnf_name
= vnfd
['name']
283 for vdu
in vnfd
.get('vdu'):
284 # Compare the named portion of the relation to the vdu's id
285 if vdu
['id'] == name
:
286 application_name
= self
.FormatApplicationName(
289 str(vnf_member_index
),
291 return application_name
293 vnf_member_index
+= 1
297 # Loop through relations
300 if 'relation' in juju
:
301 for rel
in juju
['relation']:
304 # get the application name for the provides
305 (name
, endpoint
) = rel
['provides'].split(':')
306 application_name
= _get_application_name(name
)
308 provides
= "{}:{}".format(
313 # get the application name for thr requires
314 (name
, endpoint
) = rel
['requires'].split(':')
315 application_name
= _get_application_name(name
)
317 requires
= "{}:{}".format(
321 self
.log
.debug("Relation: {} <-> {}".format(
325 await self
.add_relation(
330 except Exception as e
:
331 self
.log
.debug("Exception: {}".format(e
))
335 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
336 charm_path
, params
={}, machine_spec
={},
337 callback
=None, *callback_args
):
338 """Deploy one or more charms associated with a VNF.
340 Deploy the charm(s) referenced in a VNF Descriptor.
342 :param str model_name: The name or unique id of the network service.
343 :param str application_name: The name of the application
344 :param dict vnfd: The name of the application
345 :param str charm_path: The path to the Juju charm
346 :param dict params: A dictionary of runtime parameters
349 'rw_mgmt_ip': '1.2.3.4',
350 # Pass the initial-config-primitives section of the vnf or vdu
351 'initial-config-primitives': {...}
352 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
353 inside < >. rw_mgmt_ip will be included here also
355 :param dict machine_spec: A dictionary describing the machine to
359 'hostname': '1.2.3.4',
360 'username': 'ubuntu',
362 :param obj callback: A callback function to receive status changes.
363 :param tuple callback_args: A list of arguments to be passed to the
367 ########################################################
368 # Verify the path to the charm exists and is readable. #
369 ########################################################
370 if not os
.path
.exists(charm_path
):
371 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
372 self
.notify_callback(
379 raise JujuCharmNotFound("No artifacts configured.")
381 ################################
382 # Login to the Juju controller #
383 ################################
384 if not self
.authenticated
:
385 self
.log
.debug("Authenticating with Juju")
388 ##########################################
389 # Get the model for this network service #
390 ##########################################
391 model
= await self
.get_model(model_name
)
393 ########################################
394 # Verify the application doesn't exist #
395 ########################################
396 app
= await self
.get_application(model
, application_name
)
398 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
400 ################################################################
401 # Register this application with the model-level event monitor #
402 ################################################################
404 self
.monitors
[model_name
].AddApplication(
410 ########################################################
411 # Check for specific machine placement (native charms) #
412 ########################################################
414 if machine_spec
.keys():
415 if all(k
in machine_spec
for k
in ['host', 'user']):
416 # Enlist an existing machine as a Juju unit
417 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
418 machine_spec
['user'],
419 machine_spec
['host'],
420 self
.GetPrivateKeyPath(),
424 #######################################
425 # Get the initial charm configuration #
426 #######################################
429 if 'rw_mgmt_ip' in params
:
430 rw_mgmt_ip
= params
['rw_mgmt_ip']
432 if 'initial-config-primitive' not in params
:
433 params
['initial-config-primitive'] = {}
435 initial_config
= self
._get
_config
_from
_dict
(
436 params
['initial-config-primitive'],
437 {'<rw_mgmt_ip>': rw_mgmt_ip
}
440 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
447 ########################################################
448 # Deploy the charm and apply the initial configuration #
449 ########################################################
450 app
= await model
.deploy(
451 # We expect charm_path to be either the path to the charm on disk
452 # or in the format of cs:series/name
454 # This is the formatted, unique name for this charm
455 application_name
=application_name
,
456 # Proxy charms should use the current LTS. This will need to be
457 # changed for native charms.
459 # Apply the initial 'config' primitive during deployment
460 config
=initial_config
,
461 # Where to deploy the charm to.
465 # Map the vdu id<->app name,
467 await self
.Relate(model_name
, vnfd
)
469 # #######################################
470 # # Execute initial config primitive(s) #
471 # #######################################
472 uuids
= await self
.ExecuteInitialPrimitives(
481 # # Build a sequential list of the primitives to execute
482 # for primitive in params['initial-config-primitive']:
484 # if primitive['name'] == 'config':
485 # # This is applied when the Application is deployed
488 # seq = primitive['seq']
491 # if 'parameter' in primitive:
492 # params = primitive['parameter']
494 # primitives[seq] = {
495 # 'name': primitive['name'],
496 # 'parameters': self._map_primitive_parameters(
498 # {'<rw_mgmt_ip>': rw_mgmt_ip}
502 # for primitive in sorted(primitives):
503 # await self.ExecutePrimitive(
506 # primitives[primitive]['name'],
509 # **primitives[primitive]['parameters'],
511 # except N2VCPrimitiveExecutionFailed as e:
513 # "[N2VC] Exception executing primitive: {}".format(e)
517 async def GetPrimitiveStatus(self
, model_name
, uuid
):
518 """Get the status of an executed Primitive.
520 The status of an executed Primitive will be one of three values:
527 if not self
.authenticated
:
530 model
= await self
.get_model(model_name
)
532 results
= await model
.get_action_status(uuid
)
535 status
= results
[uuid
]
537 except Exception as e
:
539 "Caught exception while getting primitive status: {}".format(e
)
541 raise N2VCPrimitiveExecutionFailed(e
)
545 async def GetPrimitiveOutput(self
, model_name
, uuid
):
546 """Get the output of an executed Primitive.
548 Note: this only returns output for a successfully executed primitive.
552 if not self
.authenticated
:
555 model
= await self
.get_model(model_name
)
556 results
= await model
.get_action_output(uuid
, 60)
557 except Exception as e
:
559 "Caught exception while getting primitive status: {}".format(e
)
561 raise N2VCPrimitiveExecutionFailed(e
)
565 # async def ProvisionMachine(self, model_name, hostname, username):
566 # """Provision machine for usage with Juju.
568 # Provisions a previously instantiated machine for use with Juju.
571 # if not self.authenticated:
574 # # FIXME: This is hard-coded until model-per-ns is added
575 # model_name = 'default'
577 # model = await self.get_model(model_name)
578 # model.add_machine(spec={})
580 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
587 # except Exception as e:
589 # "Caught exception while getting primitive status: {}".format(e)
591 # raise N2VCPrimitiveExecutionFailed(e)
593 def GetPrivateKeyPath(self
):
594 homedir
= os
.environ
['HOME']
595 sshdir
= "{}/.ssh".format(homedir
)
596 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
597 return private_key_path
599 async def GetPublicKey(self
):
600 """Get the N2VC SSH public key.abs
602 Returns the SSH public key, to be injected into virtual machines to
603 be managed by the VCA.
605 The first time this is run, a ssh keypair will be created. The public
606 key is injected into a VM so that we can provision the machine with
607 Juju, after which Juju will communicate with the VM directly via the
612 # Find the path to where we expect our key to live.
613 homedir
= os
.environ
['HOME']
614 sshdir
= "{}/.ssh".format(homedir
)
615 if not os
.path
.exists(sshdir
):
618 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
619 public_key_path
= "{}.pub".format(private_key_path
)
621 # If we don't have a key generated, generate it.
622 if not os
.path
.exists(private_key_path
):
623 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
628 subprocess
.check_output(shlex
.split(cmd
))
630 # Read the public key
631 with
open(public_key_path
, "r") as f
:
632 public_key
= f
.readline()
636 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
637 params
, callback
=None, *callback_args
):
638 """Execute multiple primitives.
640 Execute multiple primitives as declared in initial-config-primitive.
641 This is useful in cases where the primitives initially failed -- for
642 example, if the charm is a proxy but the proxy hasn't been configured
648 # Build a sequential list of the primitives to execute
649 for primitive
in params
['initial-config-primitive']:
651 if primitive
['name'] == 'config':
654 seq
= primitive
['seq']
657 if 'parameter' in primitive
:
658 params_
= primitive
['parameter']
660 user_values
= params
.get("user_values", {})
661 if 'rw_mgmt_ip' not in user_values
:
662 user_values
['rw_mgmt_ip'] = None
663 # just for backward compatibility, because it will be provided always by modern version of LCM
666 'name': primitive
['name'],
667 'parameters': self
._map
_primitive
_parameters
(
673 for primitive
in sorted(primitives
):
675 await self
.ExecutePrimitive(
678 primitives
[primitive
]['name'],
681 **primitives
[primitive
]['parameters'],
684 except N2VCPrimitiveExecutionFailed
as e
:
686 "[N2VC] Exception executing primitive: {}".format(e
)
691 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
692 callback
, *callback_args
, **params
):
693 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
695 Execute a primitive defined in the VNF descriptor.
697 :param str model_name: The name or unique id of the network service.
698 :param str application_name: The name of the application
699 :param str primitive: The name of the primitive to execute.
700 :param obj callback: A callback function to receive status changes.
701 :param tuple callback_args: A list of arguments to be passed to the
703 :param dict params: A dictionary of key=value pairs representing the
704 primitive's parameters
707 'rw_mgmt_ip': '1.2.3.4',
708 # Pass the initial-config-primitives section of the vnf or vdu
709 'initial-config-primitives': {...}
712 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
715 if not self
.authenticated
:
718 model
= await self
.get_model(model_name
)
720 if primitive
== 'config':
721 # config is special, and expecting params to be a dictionary
722 await self
.set_config(
728 app
= await self
.get_application(model
, application_name
)
730 # Run against the first (and probably only) unit in the app
733 action
= await unit
.run_action(primitive
, **params
)
735 except Exception as e
:
737 "Caught exception while executing primitive: {}".format(e
)
739 raise N2VCPrimitiveExecutionFailed(e
)
742 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
744 """Remove a charm from the VCA.
746 Remove a charm referenced in a VNF Descriptor.
748 :param str model_name: The name of the network service.
749 :param str application_name: The name of the application
750 :param obj callback: A callback function to receive status changes.
751 :param tuple callback_args: A list of arguments to be passed to the
755 if not self
.authenticated
:
758 model
= await self
.get_model(model_name
)
759 app
= await self
.get_application(model
, application_name
)
761 # Remove this application from event monitoring
762 self
.monitors
[model_name
].RemoveApplication(application_name
)
764 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
766 "Removing the application {}".format(application_name
)
770 await self
.disconnect_model(self
.monitors
[model_name
])
772 self
.notify_callback(
776 "Removing charm {}".format(application_name
),
781 except Exception as e
:
782 print("Caught exception: {}".format(e
))
786 async def CreateNetworkService(self
, ns_uuid
):
787 """Create a new Juju model for the Network Service.
789 Creates a new Model in the Juju Controller.
791 :param str ns_uuid: A unique id representing an instaance of a
794 :returns: True if the model was created. Raises JujuError on failure.
796 if not self
.authenticated
:
799 models
= await self
.controller
.list_models()
800 if ns_uuid
not in models
:
802 self
.models
[ns_uuid
] = await self
.controller
.add_model(
805 except JujuError
as e
:
806 if "already exists" not in e
.message
:
810 async def DestroyNetworkService(self
, ns_uuid
):
811 """Destroy a Network Service.
813 Destroy the Network Service and any deployed charms.
815 :param ns_uuid The unique id of the Network Service
817 :returns: True if the model was created. Raises JujuError on failure.
820 # Do not delete the default model. The default model was used by all
821 # Network Services, prior to the implementation of a model per NS.
822 if ns_uuid
.lower() == "default":
825 if not self
.authenticated
:
826 self
.log
.debug("Authenticating with Juju")
829 # Disconnect from the Model
830 if ns_uuid
in self
.models
:
831 await self
.disconnect_model(self
.models
[ns_uuid
])
834 await self
.controller
.destroy_models(ns_uuid
)
836 raise NetworkServiceDoesNotExist(
837 "The Network Service '{}' does not exist".format(ns_uuid
)
842 async def GetMetrics(self
, model_name
, application_name
):
843 """Get the metrics collected by the VCA.
845 :param model_name The name or unique id of the network service
846 :param application_name The name of the application
849 model
= await self
.get_model(model_name
)
850 app
= await self
.get_application(model
, application_name
)
852 metrics
= await app
.get_metrics()
856 async def HasApplication(self
, model_name
, application_name
):
857 model
= await self
.get_model(model_name
)
858 app
= await self
.get_application(model
, application_name
)
864 async def add_relation(self
, model_name
, relation1
, relation2
):
866 Add a relation between two application endpoints.
868 :param str model_name: The name or unique id of the network service
869 :param str relation1: '<application>[:<relation_name>]'
870 :param str relation2: '<application>[:<relation_name>]'
873 if not self
.authenticated
:
876 m
= await self
.get_model(model_name
)
878 await m
.add_relation(relation1
, relation2
)
879 except JujuAPIError
as e
:
880 # If one of the applications in the relationship doesn't exist,
881 # or the relation has already been added, let the operation fail
883 if 'not found' in e
.message
:
885 if 'already exists' in e
.message
:
890 # async def apply_config(self, config, application):
891 # """Apply a configuration to the application."""
892 # print("JujuApi: Applying configuration to {}.".format(
895 # return await self.set_config(application=application, config=config)
897 def _get_config_from_dict(self
, config_primitive
, values
):
898 """Transform the yang config primitive to dict.
907 for primitive
in config_primitive
:
908 if primitive
['name'] == 'config':
909 # config = self._map_primitive_parameters()
910 for parameter
in primitive
['parameter']:
911 param
= str(parameter
['name'])
912 if parameter
['value'] == "<rw_mgmt_ip>":
913 config
[param
] = str(values
[parameter
['value']])
915 config
[param
] = str(parameter
['value'])
919 def _map_primitive_parameters(self
, parameters
, user_values
):
921 for parameter
in parameters
:
922 param
= str(parameter
['name'])
923 value
= parameter
.get('value')
925 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
926 # Must exist at user_values except if there is a default value
927 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
928 if parameter
['value'][1:-1] in user_values
:
929 value
= user_values
[parameter
['value'][1:-1]]
930 elif 'default-value' in parameter
:
931 value
= parameter
['default-value']
933 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
935 # If there's no value, use the default-value (if set)
936 if value
is None and 'default-value' in parameter
:
937 value
= parameter
['default-value']
939 # Typecast parameter value, if present
942 if 'data-type' in parameter
:
943 paramtype
= str(parameter
['data-type']).lower()
945 if paramtype
== "integer":
947 elif paramtype
== "boolean":
952 # If there's no data-type, assume the value is a string
955 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
957 params
[param
] = value
960 def _get_config_from_yang(self
, config_primitive
, values
):
961 """Transform the yang config primitive to dict."""
963 for primitive
in config_primitive
.values():
964 if primitive
['name'] == 'config':
965 for parameter
in primitive
['parameter'].values():
966 param
= str(parameter
['name'])
967 if parameter
['value'] == "<rw_mgmt_ip>":
968 config
[param
] = str(values
[parameter
['value']])
970 config
[param
] = str(parameter
['value'])
974 def FormatApplicationName(self
, *args
):
976 Generate a Juju-compatible Application name
978 :param args tuple: Positional arguments to be used to construct the
982 - Only accepts characters a-z and non-consequitive dashes (-)
983 - Application name should not exceed 50 characters
987 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
990 for c
in "-".join(list(args
)):
993 elif not c
.isalpha():
996 return re
.sub('-+', '-', appname
.lower())
998 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
999 # """Format the name of the application
1002 # - Only accepts characters a-z and non-consequitive dashes (-)
1003 # - Application name should not exceed 50 characters
1005 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1009 # c = chr(97 + int(c))
1010 # elif not c.isalpha():
1013 # return re.sub('\-+', '-', new_name.lower())
1015 def format_model_name(self
, name
):
1016 """Format the name of model.
1018 Model names may only contain lowercase letters, digits and hyphens
1021 return name
.replace('_', '-').lower()
1023 async def get_application(self
, model
, application
):
1024 """Get the deployed application."""
1025 if not self
.authenticated
:
1029 if application
and model
:
1030 if model
.applications
:
1031 if application
in model
.applications
:
1032 app
= model
.applications
[application
]
1036 async def get_model(self
, model_name
):
1037 """Get a model from the Juju Controller.
1039 Note: Model objects returned must call disconnected() before it goes
1041 if not self
.authenticated
:
1044 if model_name
not in self
.models
:
1045 # Get the models in the controller
1046 models
= await self
.controller
.list_models()
1048 if model_name
not in models
:
1050 self
.models
[model_name
] = await self
.controller
.add_model(
1053 except JujuError
as e
:
1054 if "already exists" not in e
.message
:
1057 self
.models
[model_name
] = await self
.controller
.get_model(
1061 self
.refcount
['model'] += 1
1063 # Create an observer for this model
1064 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1065 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1067 return self
.models
[model_name
]
1069 async def login(self
):
1070 """Login to the Juju controller."""
1072 if self
.authenticated
:
1075 self
.connecting
= True
1077 self
.log
.debug("JujuApi: Logging into controller")
1080 self
.controller
= Controller(loop
=self
.loop
)
1084 "Connecting to controller... ws://{}:{} as {}/{}".format(
1091 await self
.controller
.connect(
1092 endpoint
=self
.endpoint
,
1094 password
=self
.secret
,
1097 self
.refcount
['controller'] += 1
1099 # current_controller no longer exists
1100 # self.log.debug("Connecting to current controller...")
1101 # await self.controller.connect_current()
1102 # await self.controller.connect(
1103 # endpoint=self.endpoint,
1104 # username=self.user,
1107 self
.log
.fatal("VCA credentials not configured.")
1109 self
.authenticated
= True
1110 self
.log
.debug("JujuApi: Logged into controller")
1112 async def logout(self
):
1113 """Logout of the Juju controller."""
1114 if not self
.authenticated
:
1118 for model
in self
.models
:
1119 await self
.disconnect_model(model
)
1122 self
.log
.debug("Disconnecting controller {}".format(
1125 await self
.controller
.disconnect()
1126 self
.refcount
['controller'] -= 1
1127 self
.controller
= None
1129 self
.authenticated
= False
1131 self
.log
.debug(self
.refcount
)
1133 except Exception as e
:
1135 "Fatal error logging out of Juju Controller: {}".format(e
)
1140 async def disconnect_model(self
, model
):
1141 self
.log
.debug("Disconnecting model {}".format(model
))
1142 if model
in self
.models
:
1143 print("Disconnecting model")
1144 await self
.models
[model
].disconnect()
1145 self
.refcount
['model'] -= 1
1146 self
.models
[model
] = None
1148 # async def remove_application(self, name):
1149 # """Remove the application."""
1150 # if not self.authenticated:
1151 # await self.login()
1153 # app = await self.get_application(name)
1155 # self.log.debug("JujuApi: Destroying application {}".format(
1159 # await app.destroy()
1161 async def remove_relation(self
, a
, b
):
1163 Remove a relation between two application endpoints
1165 :param a An application endpoint
1166 :param b An application endpoint
1168 if not self
.authenticated
:
1171 m
= await self
.get_model()
1173 m
.remove_relation(a
, b
)
1175 await m
.disconnect()
1177 async def resolve_error(self
, model_name
, application
=None):
1178 """Resolve units in error state."""
1179 if not self
.authenticated
:
1182 model
= await self
.get_model(model_name
)
1184 app
= await self
.get_application(model
, application
)
1187 "JujuApi: Resolving errors for application {}".format(
1192 for unit
in app
.units
:
1193 app
.resolved(retry
=True)
1195 async def run_action(self
, model_name
, application
, action_name
, **params
):
1196 """Execute an action and return an Action object."""
1197 if not self
.authenticated
:
1207 model
= await self
.get_model(model_name
)
1209 app
= await self
.get_application(model
, application
)
1211 # We currently only have one unit per application
1212 # so use the first unit available.
1216 "JujuApi: Running Action {} against Application {}".format(
1222 action
= await unit
.run_action(action_name
, **params
)
1224 # Wait for the action to complete
1227 result
['status'] = action
.status
1228 result
['action']['tag'] = action
.data
['id']
1229 result
['action']['results'] = action
.results
1233 async def set_config(self
, model_name
, application
, config
):
1234 """Apply a configuration to the application."""
1235 if not self
.authenticated
:
1238 app
= await self
.get_application(model_name
, application
)
1240 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1243 await app
.set_config(config
)
1245 # Verify the config is set
1246 newconf
= await app
.get_config()
1248 if config
[key
] != newconf
[key
]['value']:
1249 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1251 # async def set_parameter(self, parameter, value, application=None):
1252 # """Set a config parameter for a service."""
1253 # if not self.authenticated:
1254 # await self.login()
1256 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1261 # return await self.apply_config(
1262 # {parameter: value},
1263 # application=application,
1266 async def wait_for_application(self
, model_name
, application_name
,
1268 """Wait for an application to become active."""
1269 if not self
.authenticated
:
1272 model
= await self
.get_model(model_name
)
1274 app
= await self
.get_application(model
, application_name
)
1275 self
.log
.debug("Application: {}".format(app
))
1278 "JujuApi: Waiting {} seconds for Application {}".format(
1284 await model
.block_until(
1286 unit
.agent_status
== 'idle' and unit
.workload_status
in
1287 ['active', 'unknown'] for unit
in app
.units