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."""
61 def __init__(self
, ns_name
):
62 self
.log
= logging
.getLogger(__name__
)
64 self
.ns_name
= ns_name
65 self
.applications
= {}
67 def AddApplication(self
, application_name
, callback
, *callback_args
):
68 if application_name
not in self
.applications
:
69 self
.applications
[application_name
] = {
71 'callback_args': callback_args
74 def RemoveApplication(self
, application_name
):
75 if application_name
in self
.applications
:
76 del self
.applications
[application_name
]
78 async def on_change(self
, delta
, old
, new
, model
):
79 """React to changes in the Juju model."""
81 if delta
.entity
== "unit":
82 # Ignore change events from other applications
83 if delta
.data
['application'] not in self
.applications
.keys():
88 application_name
= delta
.data
['application']
90 callback
= self
.applications
[application_name
]['callback']
92 self
.applications
[application_name
]['callback_args']
95 # Fire off a callback with the application state
99 delta
.data
['application'],
101 new
.workload_status_message
,
105 # This is a charm being removed
109 delta
.data
['application'],
113 except Exception as e
:
114 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
116 elif delta
.entity
== "action":
117 # TODO: Decide how we want to notify the user of actions
119 # uuid = delta.data['id'] # The Action's unique id
120 # msg = delta.data['message'] # The output of the action
122 # if delta.data['status'] == "pending":
123 # # The action is queued
125 # elif delta.data['status'] == "completed""
126 # # The action was successful
128 # elif delta.data['status'] == "failed":
129 # # The action failed.
137 # Create unique models per network service
138 # Document all public functions
153 :param vcaconfig dict A dictionary containing the VCA configuration
155 :param artifacts str The directory where charms required by a vnfd are
159 n2vc = N2VC(vcaconfig={
160 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
162 'ip-address': '10.44.127.137',
164 'artifacts': '/path/to/charms'
168 # Initialize instance-level variables
171 self
.controller
= None
172 self
.connecting
= False
173 self
.authenticated
= False
195 self
.log
= logging
.getLogger(__name__
)
197 # Quiet websocket traffic
198 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
199 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
200 logging
.getLogger('model').setLevel(logging
.WARN
)
201 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
203 self
.log
.debug('JujuApi: instantiated')
209 if user
.startswith('user-'):
212 self
.user
= 'user-{}'.format(user
)
214 self
.endpoint
= '%s:%d' % (server
, int(port
))
216 self
.artifacts
= artifacts
218 self
.loop
= loop
or asyncio
.get_event_loop()
221 """Close any open connections."""
224 def notify_callback(self
, model_name
, application_name
, status
, message
,
225 callback
=None, *callback_args
):
234 except Exception as e
:
235 self
.log
.error("[0] notify_callback exception {}".format(e
))
240 async def Relate(self
, model_name
, vnfd
):
241 """Create a relation between the charm-enabled VDUs in a VNF.
243 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
248 - provides: dataVM:db
251 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.
253 :param str ns_name: The name of the network service.
254 :param dict vnfd: The parsed yaml VNF descriptor.
257 # Currently, the call to Relate() is made automatically after the
258 # deployment of each charm; if the relation depends on a charm that
259 # hasn't been deployed yet, the call will fail silently. This will
260 # prevent an API breakage, with the intent of making this an explicitly
261 # required call in a more object-oriented refactor of the N2VC API.
264 vnf_config
= vnfd
.get("vnf-configuration")
266 juju
= vnf_config
['juju']
268 configs
.append(vnf_config
)
270 for vdu
in vnfd
['vdu']:
271 vdu_config
= vdu
.get('vdu-configuration')
273 juju
= vdu_config
['juju']
275 configs
.append(vdu_config
)
277 def _get_application_name(name
):
278 """Get the application name that's mapped to a vnf/vdu."""
280 vnf_name
= vnfd
['name']
282 for vdu
in vnfd
.get('vdu'):
283 # Compare the named portion of the relation to the vdu's id
284 if vdu
['id'] == name
:
285 application_name
= self
.FormatApplicationName(
288 str(vnf_member_index
),
290 return application_name
292 vnf_member_index
+= 1
296 # Loop through relations
299 if 'relation' in juju
:
300 for rel
in juju
['relation']:
303 # get the application name for the provides
304 (name
, endpoint
) = rel
['provides'].split(':')
305 application_name
= _get_application_name(name
)
307 provides
= "{}:{}".format(
312 # get the application name for thr requires
313 (name
, endpoint
) = rel
['requires'].split(':')
314 application_name
= _get_application_name(name
)
316 requires
= "{}:{}".format(
320 self
.log
.debug("Relation: {} <-> {}".format(
324 await self
.add_relation(
329 except Exception as e
:
330 self
.log
.debug("Exception: {}".format(e
))
334 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
335 charm_path
, params
={}, machine_spec
={},
336 callback
=None, *callback_args
):
337 """Deploy one or more charms associated with a VNF.
339 Deploy the charm(s) referenced in a VNF Descriptor.
341 :param str model_name: The name or unique id of the network service.
342 :param str application_name: The name of the application
343 :param dict vnfd: The name of the application
344 :param str charm_path: The path to the Juju charm
345 :param dict params: A dictionary of runtime parameters
348 'rw_mgmt_ip': '1.2.3.4',
349 # Pass the initial-config-primitives section of the vnf or vdu
350 'initial-config-primitives': {...}
351 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
352 inside < >. rw_mgmt_ip will be included here also
354 :param dict machine_spec: A dictionary describing the machine to
358 'hostname': '1.2.3.4',
359 'username': 'ubuntu',
361 :param obj callback: A callback function to receive status changes.
362 :param tuple callback_args: A list of arguments to be passed to the
366 ########################################################
367 # Verify the path to the charm exists and is readable. #
368 ########################################################
369 if not os
.path
.exists(charm_path
):
370 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
371 self
.notify_callback(
378 raise JujuCharmNotFound("No artifacts configured.")
380 ################################
381 # Login to the Juju controller #
382 ################################
383 if not self
.authenticated
:
384 self
.log
.debug("Authenticating with Juju")
387 ##########################################
388 # Get the model for this network service #
389 ##########################################
390 model
= await self
.get_model(model_name
)
392 ########################################
393 # Verify the application doesn't exist #
394 ########################################
395 app
= await self
.get_application(model
, application_name
)
397 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
399 ################################################################
400 # Register this application with the model-level event monitor #
401 ################################################################
403 self
.monitors
[model_name
].AddApplication(
409 ########################################################
410 # Check for specific machine placement (native charms) #
411 ########################################################
413 if machine_spec
.keys():
414 if all(k
in machine_spec
for k
in ['host', 'user']):
415 # Enlist an existing machine as a Juju unit
416 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
417 machine_spec
['user'],
418 machine_spec
['host'],
419 self
.GetPrivateKeyPath(),
423 #######################################
424 # Get the initial charm configuration #
425 #######################################
428 if 'rw_mgmt_ip' in params
:
429 rw_mgmt_ip
= params
['rw_mgmt_ip']
431 if 'initial-config-primitive' not in params
:
432 params
['initial-config-primitive'] = {}
434 initial_config
= self
._get
_config
_from
_dict
(
435 params
['initial-config-primitive'],
436 {'<rw_mgmt_ip>': rw_mgmt_ip
}
439 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
446 ########################################################
447 # Deploy the charm and apply the initial configuration #
448 ########################################################
449 app
= await model
.deploy(
450 # We expect charm_path to be either the path to the charm on disk
451 # or in the format of cs:series/name
453 # This is the formatted, unique name for this charm
454 application_name
=application_name
,
455 # Proxy charms should use the current LTS. This will need to be
456 # changed for native charms.
458 # Apply the initial 'config' primitive during deployment
459 config
=initial_config
,
460 # Where to deploy the charm to.
464 # Map the vdu id<->app name,
466 await self
.Relate(model_name
, vnfd
)
468 # #######################################
469 # # Execute initial config primitive(s) #
470 # #######################################
471 uuids
= await self
.ExecuteInitialPrimitives(
480 # # Build a sequential list of the primitives to execute
481 # for primitive in params['initial-config-primitive']:
483 # if primitive['name'] == 'config':
484 # # This is applied when the Application is deployed
487 # seq = primitive['seq']
490 # if 'parameter' in primitive:
491 # params = primitive['parameter']
493 # primitives[seq] = {
494 # 'name': primitive['name'],
495 # 'parameters': self._map_primitive_parameters(
497 # {'<rw_mgmt_ip>': rw_mgmt_ip}
501 # for primitive in sorted(primitives):
502 # await self.ExecutePrimitive(
505 # primitives[primitive]['name'],
508 # **primitives[primitive]['parameters'],
510 # except N2VCPrimitiveExecutionFailed as e:
512 # "[N2VC] Exception executing primitive: {}".format(e)
516 async def GetPrimitiveStatus(self
, model_name
, uuid
):
517 """Get the status of an executed Primitive.
519 The status of an executed Primitive will be one of three values:
526 if not self
.authenticated
:
529 model
= await self
.get_model(model_name
)
531 results
= await model
.get_action_status(uuid
)
534 status
= results
[uuid
]
536 except Exception as e
:
538 "Caught exception while getting primitive status: {}".format(e
)
540 raise N2VCPrimitiveExecutionFailed(e
)
544 async def GetPrimitiveOutput(self
, model_name
, uuid
):
545 """Get the output of an executed Primitive.
547 Note: this only returns output for a successfully executed primitive.
551 if not self
.authenticated
:
554 model
= await self
.get_model(model_name
)
555 results
= await model
.get_action_output(uuid
, 60)
556 except Exception as e
:
558 "Caught exception while getting primitive status: {}".format(e
)
560 raise N2VCPrimitiveExecutionFailed(e
)
564 # async def ProvisionMachine(self, model_name, hostname, username):
565 # """Provision machine for usage with Juju.
567 # Provisions a previously instantiated machine for use with Juju.
570 # if not self.authenticated:
573 # # FIXME: This is hard-coded until model-per-ns is added
574 # model_name = 'default'
576 # model = await self.get_model(model_name)
577 # model.add_machine(spec={})
579 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
586 # except Exception as e:
588 # "Caught exception while getting primitive status: {}".format(e)
590 # raise N2VCPrimitiveExecutionFailed(e)
592 def GetPrivateKeyPath(self
):
593 homedir
= os
.environ
['HOME']
594 sshdir
= "{}/.ssh".format(homedir
)
595 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
596 return private_key_path
598 async def GetPublicKey(self
):
599 """Get the N2VC SSH public key.abs
601 Returns the SSH public key, to be injected into virtual machines to
602 be managed by the VCA.
604 The first time this is run, a ssh keypair will be created. The public
605 key is injected into a VM so that we can provision the machine with
606 Juju, after which Juju will communicate with the VM directly via the
611 # Find the path to where we expect our key to live.
612 homedir
= os
.environ
['HOME']
613 sshdir
= "{}/.ssh".format(homedir
)
614 if not os
.path
.exists(sshdir
):
617 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
618 public_key_path
= "{}.pub".format(private_key_path
)
620 # If we don't have a key generated, generate it.
621 if not os
.path
.exists(private_key_path
):
622 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
627 subprocess
.check_output(shlex
.split(cmd
))
629 # Read the public key
630 with
open(public_key_path
, "r") as f
:
631 public_key
= f
.readline()
635 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
636 params
, callback
=None, *callback_args
):
637 """Execute multiple primitives.
639 Execute multiple primitives as declared in initial-config-primitive.
640 This is useful in cases where the primitives initially failed -- for
641 example, if the charm is a proxy but the proxy hasn't been configured
647 # Build a sequential list of the primitives to execute
648 for primitive
in params
['initial-config-primitive']:
650 if primitive
['name'] == 'config':
653 seq
= primitive
['seq']
656 if 'parameter' in primitive
:
657 params_
= primitive
['parameter']
659 user_values
= params
.get("user_values", {})
660 if 'rw_mgmt_ip' not in user_values
:
661 user_values
['rw_mgmt_ip'] = None
662 # just for backward compatibility, because it will be provided always by modern version of LCM
665 'name': primitive
['name'],
666 'parameters': self
._map
_primitive
_parameters
(
672 for primitive
in sorted(primitives
):
674 await self
.ExecutePrimitive(
677 primitives
[primitive
]['name'],
680 **primitives
[primitive
]['parameters'],
683 except N2VCPrimitiveExecutionFailed
as e
:
685 "[N2VC] Exception executing primitive: {}".format(e
)
690 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
691 callback
, *callback_args
, **params
):
692 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
694 Execute a primitive defined in the VNF descriptor.
696 :param str model_name: The name or unique id of the network service.
697 :param str application_name: The name of the application
698 :param str primitive: The name of the primitive to execute.
699 :param obj callback: A callback function to receive status changes.
700 :param tuple callback_args: A list of arguments to be passed to the
702 :param dict params: A dictionary of key=value pairs representing the
703 primitive's parameters
706 'rw_mgmt_ip': '1.2.3.4',
707 # Pass the initial-config-primitives section of the vnf or vdu
708 'initial-config-primitives': {...}
711 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
714 if not self
.authenticated
:
717 model
= await self
.get_model(model_name
)
719 if primitive
== 'config':
720 # config is special, and expecting params to be a dictionary
721 await self
.set_config(
727 app
= await self
.get_application(model
, application_name
)
729 # Run against the first (and probably only) unit in the app
732 action
= await unit
.run_action(primitive
, **params
)
734 except Exception as e
:
736 "Caught exception while executing primitive: {}".format(e
)
738 raise N2VCPrimitiveExecutionFailed(e
)
741 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
743 """Remove a charm from the VCA.
745 Remove a charm referenced in a VNF Descriptor.
747 :param str model_name: The name of the network service.
748 :param str application_name: The name of the application
749 :param obj callback: A callback function to receive status changes.
750 :param tuple callback_args: A list of arguments to be passed to the
754 if not self
.authenticated
:
757 model
= await self
.get_model(model_name
)
758 app
= await self
.get_application(model
, application_name
)
760 # Remove this application from event monitoring
761 self
.monitors
[model_name
].RemoveApplication(application_name
)
763 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
765 "Removing the application {}".format(application_name
)
769 await self
.disconnect_model(self
.monitors
[model_name
])
771 self
.notify_callback(
775 "Removing charm {}".format(application_name
),
780 except Exception as e
:
781 print("Caught exception: {}".format(e
))
785 async def CreateNetworkService(self
, ns_uuid
):
786 """Create a new Juju model for the Network Service.
788 Creates a new Model in the Juju Controller.
790 :param str ns_uuid: A unique id representing an instaance of a
793 :returns: True if the model was created. Raises JujuError on failure.
795 if not self
.authenticated
:
798 models
= await self
.controller
.list_models()
799 if ns_uuid
not in models
:
801 self
.models
[ns_uuid
] = await self
.controller
.add_model(
804 except JujuError
as e
:
805 if "already exists" not in e
.message
:
808 # Create an observer for this model
809 await self
.create_model_monitor(ns_uuid
)
813 async def DestroyNetworkService(self
, ns_uuid
):
814 """Destroy a Network Service.
816 Destroy the Network Service and any deployed charms.
818 :param ns_uuid The unique id of the Network Service
820 :returns: True if the model was created. Raises JujuError on failure.
823 # Do not delete the default model. The default model was used by all
824 # Network Services, prior to the implementation of a model per NS.
825 if ns_uuid
.lower() == "default":
828 if not self
.authenticated
:
829 self
.log
.debug("Authenticating with Juju")
832 # Disconnect from the Model
833 if ns_uuid
in self
.models
:
834 await self
.disconnect_model(self
.models
[ns_uuid
])
837 await self
.controller
.destroy_models(ns_uuid
)
839 raise NetworkServiceDoesNotExist(
840 "The Network Service '{}' does not exist".format(ns_uuid
)
845 async def GetMetrics(self
, model_name
, application_name
):
846 """Get the metrics collected by the VCA.
848 :param model_name The name or unique id of the network service
849 :param application_name The name of the application
852 model
= await self
.get_model(model_name
)
853 app
= await self
.get_application(model
, application_name
)
855 metrics
= await app
.get_metrics()
859 async def HasApplication(self
, model_name
, application_name
):
860 model
= await self
.get_model(model_name
)
861 app
= await self
.get_application(model
, application_name
)
867 async def add_relation(self
, model_name
, relation1
, relation2
):
869 Add a relation between two application endpoints.
871 :param str model_name: The name or unique id of the network service
872 :param str relation1: '<application>[:<relation_name>]'
873 :param str relation2: '<application>[:<relation_name>]'
876 if not self
.authenticated
:
879 m
= await self
.get_model(model_name
)
881 await m
.add_relation(relation1
, relation2
)
882 except JujuAPIError
as e
:
883 # If one of the applications in the relationship doesn't exist,
884 # or the relation has already been added, let the operation fail
886 if 'not found' in e
.message
:
888 if 'already exists' in e
.message
:
893 # async def apply_config(self, config, application):
894 # """Apply a configuration to the application."""
895 # print("JujuApi: Applying configuration to {}.".format(
898 # return await self.set_config(application=application, config=config)
900 def _get_config_from_dict(self
, config_primitive
, values
):
901 """Transform the yang config primitive to dict.
910 for primitive
in config_primitive
:
911 if primitive
['name'] == 'config':
912 # config = self._map_primitive_parameters()
913 for parameter
in primitive
['parameter']:
914 param
= str(parameter
['name'])
915 if parameter
['value'] == "<rw_mgmt_ip>":
916 config
[param
] = str(values
[parameter
['value']])
918 config
[param
] = str(parameter
['value'])
922 def _map_primitive_parameters(self
, parameters
, user_values
):
924 for parameter
in parameters
:
925 param
= str(parameter
['name'])
926 value
= parameter
.get('value')
928 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
929 # Must exist at user_values except if there is a default value
930 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
931 if parameter
['value'][1:-1] in user_values
:
932 value
= user_values
[parameter
['value'][1:-1]]
933 elif 'default-value' in parameter
:
934 value
= parameter
['default-value']
936 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
938 # If there's no value, use the default-value (if set)
939 if value
is None and 'default-value' in parameter
:
940 value
= parameter
['default-value']
942 # Typecast parameter value, if present
945 if 'data-type' in parameter
:
946 paramtype
= str(parameter
['data-type']).lower()
948 if paramtype
== "integer":
950 elif paramtype
== "boolean":
955 # If there's no data-type, assume the value is a string
958 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
960 params
[param
] = value
963 def _get_config_from_yang(self
, config_primitive
, values
):
964 """Transform the yang config primitive to dict."""
966 for primitive
in config_primitive
.values():
967 if primitive
['name'] == 'config':
968 for parameter
in primitive
['parameter'].values():
969 param
= str(parameter
['name'])
970 if parameter
['value'] == "<rw_mgmt_ip>":
971 config
[param
] = str(values
[parameter
['value']])
973 config
[param
] = str(parameter
['value'])
977 def FormatApplicationName(self
, *args
):
979 Generate a Juju-compatible Application name
981 :param args tuple: Positional arguments to be used to construct the
985 - Only accepts characters a-z and non-consequitive dashes (-)
986 - Application name should not exceed 50 characters
990 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
993 for c
in "-".join(list(args
)):
996 elif not c
.isalpha():
999 return re
.sub('-+', '-', appname
.lower())
1001 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1002 # """Format the name of the application
1005 # - Only accepts characters a-z and non-consequitive dashes (-)
1006 # - Application name should not exceed 50 characters
1008 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1012 # c = chr(97 + int(c))
1013 # elif not c.isalpha():
1016 # return re.sub('\-+', '-', new_name.lower())
1018 def format_model_name(self
, name
):
1019 """Format the name of model.
1021 Model names may only contain lowercase letters, digits and hyphens
1024 return name
.replace('_', '-').lower()
1026 async def get_application(self
, model
, application
):
1027 """Get the deployed application."""
1028 if not self
.authenticated
:
1032 if application
and model
:
1033 if model
.applications
:
1034 if application
in model
.applications
:
1035 app
= model
.applications
[application
]
1039 async def get_model(self
, model_name
):
1040 """Get a model from the Juju Controller.
1042 Note: Model objects returned must call disconnected() before it goes
1044 if not self
.authenticated
:
1047 if model_name
not in self
.models
:
1048 # Get the models in the controller
1049 models
= await self
.controller
.list_models()
1051 if model_name
not in models
:
1053 self
.models
[model_name
] = await self
.controller
.add_model(
1056 except JujuError
as e
:
1057 if "already exists" not in e
.message
:
1060 self
.models
[model_name
] = await self
.controller
.get_model(
1064 self
.refcount
['model'] += 1
1066 # Create an observer for this model
1067 await self
.create_model_monitor(model_name
)
1069 return self
.models
[model_name
]
1071 async def create_model_monitor(self
, model_name
):
1072 """Create a monitor for the model, if none exists."""
1073 if not self
.authenticated
:
1076 if model_name
not in self
.monitors
:
1077 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1078 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1082 async def login(self
):
1083 """Login to the Juju controller."""
1085 if self
.authenticated
:
1088 self
.connecting
= True
1090 self
.log
.debug("JujuApi: Logging into controller")
1093 self
.controller
= Controller(loop
=self
.loop
)
1097 "Connecting to controller... ws://{}:{} as {}/{}".format(
1104 await self
.controller
.connect(
1105 endpoint
=self
.endpoint
,
1107 password
=self
.secret
,
1110 self
.refcount
['controller'] += 1
1112 # current_controller no longer exists
1113 # self.log.debug("Connecting to current controller...")
1114 # await self.controller.connect_current()
1115 # await self.controller.connect(
1116 # endpoint=self.endpoint,
1117 # username=self.user,
1120 self
.log
.fatal("VCA credentials not configured.")
1122 self
.authenticated
= True
1123 self
.log
.debug("JujuApi: Logged into controller")
1125 async def logout(self
):
1126 """Logout of the Juju controller."""
1127 if not self
.authenticated
:
1131 for model
in self
.models
:
1132 await self
.disconnect_model(model
)
1135 self
.log
.debug("Disconnecting controller {}".format(
1138 await self
.controller
.disconnect()
1139 self
.refcount
['controller'] -= 1
1140 self
.controller
= None
1142 self
.authenticated
= False
1144 self
.log
.debug(self
.refcount
)
1146 except Exception as e
:
1148 "Fatal error logging out of Juju Controller: {}".format(e
)
1153 async def disconnect_model(self
, model
):
1154 self
.log
.debug("Disconnecting model {}".format(model
))
1155 if model
in self
.models
:
1156 print("Disconnecting model")
1157 await self
.models
[model
].disconnect()
1158 self
.refcount
['model'] -= 1
1159 self
.models
[model
] = None
1161 # async def remove_application(self, name):
1162 # """Remove the application."""
1163 # if not self.authenticated:
1164 # await self.login()
1166 # app = await self.get_application(name)
1168 # self.log.debug("JujuApi: Destroying application {}".format(
1172 # await app.destroy()
1174 async def remove_relation(self
, a
, b
):
1176 Remove a relation between two application endpoints
1178 :param a An application endpoint
1179 :param b An application endpoint
1181 if not self
.authenticated
:
1184 m
= await self
.get_model()
1186 m
.remove_relation(a
, b
)
1188 await m
.disconnect()
1190 async def resolve_error(self
, model_name
, application
=None):
1191 """Resolve units in error state."""
1192 if not self
.authenticated
:
1195 model
= await self
.get_model(model_name
)
1197 app
= await self
.get_application(model
, application
)
1200 "JujuApi: Resolving errors for application {}".format(
1205 for unit
in app
.units
:
1206 app
.resolved(retry
=True)
1208 async def run_action(self
, model_name
, application
, action_name
, **params
):
1209 """Execute an action and return an Action object."""
1210 if not self
.authenticated
:
1220 model
= await self
.get_model(model_name
)
1222 app
= await self
.get_application(model
, application
)
1224 # We currently only have one unit per application
1225 # so use the first unit available.
1229 "JujuApi: Running Action {} against Application {}".format(
1235 action
= await unit
.run_action(action_name
, **params
)
1237 # Wait for the action to complete
1240 result
['status'] = action
.status
1241 result
['action']['tag'] = action
.data
['id']
1242 result
['action']['results'] = action
.results
1246 async def set_config(self
, model_name
, application
, config
):
1247 """Apply a configuration to the application."""
1248 if not self
.authenticated
:
1251 app
= await self
.get_application(model_name
, application
)
1253 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1256 await app
.set_config(config
)
1258 # Verify the config is set
1259 newconf
= await app
.get_config()
1261 if config
[key
] != newconf
[key
]['value']:
1262 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1264 # async def set_parameter(self, parameter, value, application=None):
1265 # """Set a config parameter for a service."""
1266 # if not self.authenticated:
1267 # await self.login()
1269 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1274 # return await self.apply_config(
1275 # {parameter: value},
1276 # application=application,
1279 async def wait_for_application(self
, model_name
, application_name
,
1281 """Wait for an application to become active."""
1282 if not self
.authenticated
:
1285 model
= await self
.get_model(model_name
)
1287 app
= await self
.get_application(model
, application_name
)
1288 self
.log
.debug("Application: {}".format(app
))
1291 "JujuApi: Waiting {} seconds for Application {}".format(
1297 await model
.block_until(
1299 unit
.agent_status
== 'idle' and unit
.workload_status
in
1300 ['active', 'unknown'] for unit
in app
.units