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
:
809 # Create an observer for this model
810 await self
.create_model_monitor(ns_uuid
)
814 async def DestroyNetworkService(self
, ns_uuid
):
815 """Destroy a Network Service.
817 Destroy the Network Service and any deployed charms.
819 :param ns_uuid The unique id of the Network Service
821 :returns: True if the model was created. Raises JujuError on failure.
824 # Do not delete the default model. The default model was used by all
825 # Network Services, prior to the implementation of a model per NS.
826 if ns_uuid
.lower() == "default":
829 if not self
.authenticated
:
830 self
.log
.debug("Authenticating with Juju")
833 # Disconnect from the Model
834 if ns_uuid
in self
.models
:
835 await self
.disconnect_model(self
.models
[ns_uuid
])
838 await self
.controller
.destroy_models(ns_uuid
)
840 raise NetworkServiceDoesNotExist(
841 "The Network Service '{}' does not exist".format(ns_uuid
)
846 async def GetMetrics(self
, model_name
, application_name
):
847 """Get the metrics collected by the VCA.
849 :param model_name The name or unique id of the network service
850 :param application_name The name of the application
853 model
= await self
.get_model(model_name
)
854 app
= await self
.get_application(model
, application_name
)
856 metrics
= await app
.get_metrics()
860 async def HasApplication(self
, model_name
, application_name
):
861 model
= await self
.get_model(model_name
)
862 app
= await self
.get_application(model
, application_name
)
868 async def add_relation(self
, model_name
, relation1
, relation2
):
870 Add a relation between two application endpoints.
872 :param str model_name: The name or unique id of the network service
873 :param str relation1: '<application>[:<relation_name>]'
874 :param str relation2: '<application>[:<relation_name>]'
877 if not self
.authenticated
:
880 m
= await self
.get_model(model_name
)
882 await m
.add_relation(relation1
, relation2
)
883 except JujuAPIError
as e
:
884 # If one of the applications in the relationship doesn't exist,
885 # or the relation has already been added, let the operation fail
887 if 'not found' in e
.message
:
889 if 'already exists' in e
.message
:
894 # async def apply_config(self, config, application):
895 # """Apply a configuration to the application."""
896 # print("JujuApi: Applying configuration to {}.".format(
899 # return await self.set_config(application=application, config=config)
901 def _get_config_from_dict(self
, config_primitive
, values
):
902 """Transform the yang config primitive to dict.
911 for primitive
in config_primitive
:
912 if primitive
['name'] == 'config':
913 # config = self._map_primitive_parameters()
914 for parameter
in primitive
['parameter']:
915 param
= str(parameter
['name'])
916 if parameter
['value'] == "<rw_mgmt_ip>":
917 config
[param
] = str(values
[parameter
['value']])
919 config
[param
] = str(parameter
['value'])
923 def _map_primitive_parameters(self
, parameters
, user_values
):
925 for parameter
in parameters
:
926 param
= str(parameter
['name'])
927 value
= parameter
.get('value')
929 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
930 # Must exist at user_values except if there is a default value
931 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
932 if parameter
['value'][1:-1] in user_values
:
933 value
= user_values
[parameter
['value'][1:-1]]
934 elif 'default-value' in parameter
:
935 value
= parameter
['default-value']
937 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
939 # If there's no value, use the default-value (if set)
940 if value
is None and 'default-value' in parameter
:
941 value
= parameter
['default-value']
943 # Typecast parameter value, if present
946 if 'data-type' in parameter
:
947 paramtype
= str(parameter
['data-type']).lower()
949 if paramtype
== "integer":
951 elif paramtype
== "boolean":
956 # If there's no data-type, assume the value is a string
959 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
961 params
[param
] = value
964 def _get_config_from_yang(self
, config_primitive
, values
):
965 """Transform the yang config primitive to dict."""
967 for primitive
in config_primitive
.values():
968 if primitive
['name'] == 'config':
969 for parameter
in primitive
['parameter'].values():
970 param
= str(parameter
['name'])
971 if parameter
['value'] == "<rw_mgmt_ip>":
972 config
[param
] = str(values
[parameter
['value']])
974 config
[param
] = str(parameter
['value'])
978 def FormatApplicationName(self
, *args
):
980 Generate a Juju-compatible Application name
982 :param args tuple: Positional arguments to be used to construct the
986 - Only accepts characters a-z and non-consequitive dashes (-)
987 - Application name should not exceed 50 characters
991 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
994 for c
in "-".join(list(args
)):
997 elif not c
.isalpha():
1000 return re
.sub('-+', '-', appname
.lower())
1002 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1003 # """Format the name of the application
1006 # - Only accepts characters a-z and non-consequitive dashes (-)
1007 # - Application name should not exceed 50 characters
1009 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1013 # c = chr(97 + int(c))
1014 # elif not c.isalpha():
1017 # return re.sub('\-+', '-', new_name.lower())
1019 def format_model_name(self
, name
):
1020 """Format the name of model.
1022 Model names may only contain lowercase letters, digits and hyphens
1025 return name
.replace('_', '-').lower()
1027 async def get_application(self
, model
, application
):
1028 """Get the deployed application."""
1029 if not self
.authenticated
:
1033 if application
and model
:
1034 if model
.applications
:
1035 if application
in model
.applications
:
1036 app
= model
.applications
[application
]
1040 async def get_model(self
, model_name
):
1041 """Get a model from the Juju Controller.
1043 Note: Model objects returned must call disconnected() before it goes
1045 if not self
.authenticated
:
1048 if model_name
not in self
.models
:
1049 # Get the models in the controller
1050 models
= await self
.controller
.list_models()
1052 if model_name
not in models
:
1054 self
.models
[model_name
] = await self
.controller
.add_model(
1057 except JujuError
as e
:
1058 if "already exists" not in e
.message
:
1061 self
.models
[model_name
] = await self
.controller
.get_model(
1065 self
.refcount
['model'] += 1
1067 # Create an observer for this model
1068 await self
.create_model_monitor(model_name
)
1070 return self
.models
[model_name
]
1072 async def create_model_monitor(self
, model_name
):
1073 """Create a monitor for the model, if none exists."""
1074 if not self
.authenticated
:
1077 if model_name
not in self
.monitors
:
1078 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1079 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1083 async def login(self
):
1084 """Login to the Juju controller."""
1086 if self
.authenticated
:
1089 self
.connecting
= True
1091 self
.log
.debug("JujuApi: Logging into controller")
1094 self
.controller
= Controller(loop
=self
.loop
)
1098 "Connecting to controller... ws://{}:{} as {}/{}".format(
1105 await self
.controller
.connect(
1106 endpoint
=self
.endpoint
,
1108 password
=self
.secret
,
1111 self
.refcount
['controller'] += 1
1113 # current_controller no longer exists
1114 # self.log.debug("Connecting to current controller...")
1115 # await self.controller.connect_current()
1116 # await self.controller.connect(
1117 # endpoint=self.endpoint,
1118 # username=self.user,
1121 self
.log
.fatal("VCA credentials not configured.")
1123 self
.authenticated
= True
1124 self
.log
.debug("JujuApi: Logged into controller")
1126 async def logout(self
):
1127 """Logout of the Juju controller."""
1128 if not self
.authenticated
:
1132 for model
in self
.models
:
1133 await self
.disconnect_model(model
)
1136 self
.log
.debug("Disconnecting controller {}".format(
1139 await self
.controller
.disconnect()
1140 self
.refcount
['controller'] -= 1
1141 self
.controller
= None
1143 self
.authenticated
= False
1145 self
.log
.debug(self
.refcount
)
1147 except Exception as e
:
1149 "Fatal error logging out of Juju Controller: {}".format(e
)
1154 async def disconnect_model(self
, model
):
1155 self
.log
.debug("Disconnecting model {}".format(model
))
1156 if model
in self
.models
:
1157 print("Disconnecting model")
1158 await self
.models
[model
].disconnect()
1159 self
.refcount
['model'] -= 1
1160 self
.models
[model
] = None
1162 # async def remove_application(self, name):
1163 # """Remove the application."""
1164 # if not self.authenticated:
1165 # await self.login()
1167 # app = await self.get_application(name)
1169 # self.log.debug("JujuApi: Destroying application {}".format(
1173 # await app.destroy()
1175 async def remove_relation(self
, a
, b
):
1177 Remove a relation between two application endpoints
1179 :param a An application endpoint
1180 :param b An application endpoint
1182 if not self
.authenticated
:
1185 m
= await self
.get_model()
1187 m
.remove_relation(a
, b
)
1189 await m
.disconnect()
1191 async def resolve_error(self
, model_name
, application
=None):
1192 """Resolve units in error state."""
1193 if not self
.authenticated
:
1196 model
= await self
.get_model(model_name
)
1198 app
= await self
.get_application(model
, application
)
1201 "JujuApi: Resolving errors for application {}".format(
1206 for unit
in app
.units
:
1207 app
.resolved(retry
=True)
1209 async def run_action(self
, model_name
, application
, action_name
, **params
):
1210 """Execute an action and return an Action object."""
1211 if not self
.authenticated
:
1221 model
= await self
.get_model(model_name
)
1223 app
= await self
.get_application(model
, application
)
1225 # We currently only have one unit per application
1226 # so use the first unit available.
1230 "JujuApi: Running Action {} against Application {}".format(
1236 action
= await unit
.run_action(action_name
, **params
)
1238 # Wait for the action to complete
1241 result
['status'] = action
.status
1242 result
['action']['tag'] = action
.data
['id']
1243 result
['action']['results'] = action
.results
1247 async def set_config(self
, model_name
, application
, config
):
1248 """Apply a configuration to the application."""
1249 if not self
.authenticated
:
1252 app
= await self
.get_application(model_name
, application
)
1254 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1257 await app
.set_config(config
)
1259 # Verify the config is set
1260 newconf
= await app
.get_config()
1262 if config
[key
] != newconf
[key
]['value']:
1263 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1265 # async def set_parameter(self, parameter, value, application=None):
1266 # """Set a config parameter for a service."""
1267 # if not self.authenticated:
1268 # await self.login()
1270 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1275 # return await self.apply_config(
1276 # {parameter: value},
1277 # application=application,
1280 async def wait_for_application(self
, model_name
, application_name
,
1282 """Wait for an application to become active."""
1283 if not self
.authenticated
:
1286 model
= await self
.get_model(model_name
)
1288 app
= await self
.get_application(model
, application_name
)
1289 self
.log
.debug("Application: {}".format(app
))
1292 "JujuApi: Waiting {} seconds for Application {}".format(
1298 await model
.block_until(
1300 unit
.agent_status
== 'idle' and unit
.workload_status
in
1301 ['active', 'unknown'] for unit
in app
.units