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
150 juju_public_key
=None,
154 :param log obj: The logging object to log to
155 :param server str: The IP Address or Hostname of the Juju controller
156 :param port int: The port of the Juju Controller
157 :param user str: The Juju username to authenticate with
158 :param secret str: The Juju password to authenticate with
159 :param artifacts str: The directory where charms required by a vnfd are
161 :param loop obj: The loop to use.
162 :param juju_public_key str: The contents of the Juju public SSH key
163 :param ca_cert str: The CA certificate to use to authenticate
167 client = n2vc.vnf.N2VC(
173 artifacts='/app/storage/myvnf/charms',
175 juju_public_key='<contents of the juju public key>',
176 ca_cert='<contents of CA certificate>',
180 # Initialize instance-level variables
183 self
.controller
= None
184 self
.connecting
= False
185 self
.authenticated
= False
204 self
.juju_public_key
= juju_public_key
206 self
._create
_juju
_public
_key
(juju_public_key
)
208 self
.ca_cert
= ca_cert
213 self
.log
= logging
.getLogger(__name__
)
215 # Quiet websocket traffic
216 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
217 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
218 logging
.getLogger('model').setLevel(logging
.WARN
)
219 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
221 self
.log
.debug('JujuApi: instantiated')
227 if user
.startswith('user-'):
230 self
.user
= 'user-{}'.format(user
)
232 self
.endpoint
= '%s:%d' % (server
, int(port
))
234 self
.artifacts
= artifacts
236 self
.loop
= loop
or asyncio
.get_event_loop()
239 """Close any open connections."""
242 def _create_juju_public_key(self
, public_key
):
243 """Recreate the Juju public key on disk.
245 Certain libjuju commands expect to be run from the same machine as Juju
246 is bootstrapped to. This method will write the public key to disk in
247 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
249 if public_key
is None or len(public_key
) == 0:
252 path
= "{}/.local/share/juju/ssh".format(
253 os
.path
.expanduser('~'),
255 if not os
.path
.exists(path
):
258 with
open('{}/juju_id_rsa.pub'.format(path
), 'w') as f
:
261 def notify_callback(self
, model_name
, application_name
, status
, message
,
262 callback
=None, *callback_args
):
271 except Exception as e
:
272 self
.log
.error("[0] notify_callback exception {}".format(e
))
277 async def Relate(self
, model_name
, vnfd
):
278 """Create a relation between the charm-enabled VDUs in a VNF.
280 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
285 - provides: dataVM:db
288 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.
290 :param str ns_name: The name of the network service.
291 :param dict vnfd: The parsed yaml VNF descriptor.
294 # Currently, the call to Relate() is made automatically after the
295 # deployment of each charm; if the relation depends on a charm that
296 # hasn't been deployed yet, the call will fail silently. This will
297 # prevent an API breakage, with the intent of making this an explicitly
298 # required call in a more object-oriented refactor of the N2VC API.
301 vnf_config
= vnfd
.get("vnf-configuration")
303 juju
= vnf_config
['juju']
305 configs
.append(vnf_config
)
307 for vdu
in vnfd
['vdu']:
308 vdu_config
= vdu
.get('vdu-configuration')
310 juju
= vdu_config
['juju']
312 configs
.append(vdu_config
)
314 def _get_application_name(name
):
315 """Get the application name that's mapped to a vnf/vdu."""
317 vnf_name
= vnfd
['name']
319 for vdu
in vnfd
.get('vdu'):
320 # Compare the named portion of the relation to the vdu's id
321 if vdu
['id'] == name
:
322 application_name
= self
.FormatApplicationName(
325 str(vnf_member_index
),
327 return application_name
329 vnf_member_index
+= 1
333 # Loop through relations
336 if 'relation' in juju
:
337 for rel
in juju
['relation']:
340 # get the application name for the provides
341 (name
, endpoint
) = rel
['provides'].split(':')
342 application_name
= _get_application_name(name
)
344 provides
= "{}:{}".format(
349 # get the application name for thr requires
350 (name
, endpoint
) = rel
['requires'].split(':')
351 application_name
= _get_application_name(name
)
353 requires
= "{}:{}".format(
357 self
.log
.debug("Relation: {} <-> {}".format(
361 await self
.add_relation(
366 except Exception as e
:
367 self
.log
.debug("Exception: {}".format(e
))
371 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
372 charm_path
, params
={}, machine_spec
={},
373 callback
=None, *callback_args
):
374 """Deploy one or more charms associated with a VNF.
376 Deploy the charm(s) referenced in a VNF Descriptor.
378 :param str model_name: The name or unique id of the network service.
379 :param str application_name: The name of the application
380 :param dict vnfd: The name of the application
381 :param str charm_path: The path to the Juju charm
382 :param dict params: A dictionary of runtime parameters
385 'rw_mgmt_ip': '1.2.3.4',
386 # Pass the initial-config-primitives section of the vnf or vdu
387 'initial-config-primitives': {...}
388 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
389 inside < >. rw_mgmt_ip will be included here also
391 :param dict machine_spec: A dictionary describing the machine to
395 'hostname': '1.2.3.4',
396 'username': 'ubuntu',
398 :param obj callback: A callback function to receive status changes.
399 :param tuple callback_args: A list of arguments to be passed to the
403 ########################################################
404 # Verify the path to the charm exists and is readable. #
405 ########################################################
406 if not os
.path
.exists(charm_path
):
407 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
408 self
.notify_callback(
415 raise JujuCharmNotFound("No artifacts configured.")
417 ################################
418 # Login to the Juju controller #
419 ################################
420 if not self
.authenticated
:
421 self
.log
.debug("Authenticating with Juju")
424 ##########################################
425 # Get the model for this network service #
426 ##########################################
427 model
= await self
.get_model(model_name
)
429 ########################################
430 # Verify the application doesn't exist #
431 ########################################
432 app
= await self
.get_application(model
, application_name
)
434 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
436 ################################################################
437 # Register this application with the model-level event monitor #
438 ################################################################
440 self
.monitors
[model_name
].AddApplication(
446 ########################################################
447 # Check for specific machine placement (native charms) #
448 ########################################################
450 if machine_spec
.keys():
451 if all(k
in machine_spec
for k
in ['host', 'user']):
452 # Enlist an existing machine as a Juju unit
453 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
454 machine_spec
['user'],
455 machine_spec
['host'],
456 self
.GetPrivateKeyPath(),
460 #######################################
461 # Get the initial charm configuration #
462 #######################################
465 if 'rw_mgmt_ip' in params
:
466 rw_mgmt_ip
= params
['rw_mgmt_ip']
468 if 'initial-config-primitive' not in params
:
469 params
['initial-config-primitive'] = {}
471 initial_config
= self
._get
_config
_from
_dict
(
472 params
['initial-config-primitive'],
473 {'<rw_mgmt_ip>': rw_mgmt_ip
}
476 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
483 ########################################################
484 # Deploy the charm and apply the initial configuration #
485 ########################################################
486 app
= await model
.deploy(
487 # We expect charm_path to be either the path to the charm on disk
488 # or in the format of cs:series/name
490 # This is the formatted, unique name for this charm
491 application_name
=application_name
,
492 # Proxy charms should use the current LTS. This will need to be
493 # changed for native charms.
495 # Apply the initial 'config' primitive during deployment
496 config
=initial_config
,
497 # Where to deploy the charm to.
501 # Map the vdu id<->app name,
503 await self
.Relate(model_name
, vnfd
)
505 # #######################################
506 # # Execute initial config primitive(s) #
507 # #######################################
508 uuids
= await self
.ExecuteInitialPrimitives(
517 # # Build a sequential list of the primitives to execute
518 # for primitive in params['initial-config-primitive']:
520 # if primitive['name'] == 'config':
521 # # This is applied when the Application is deployed
524 # seq = primitive['seq']
527 # if 'parameter' in primitive:
528 # params = primitive['parameter']
530 # primitives[seq] = {
531 # 'name': primitive['name'],
532 # 'parameters': self._map_primitive_parameters(
534 # {'<rw_mgmt_ip>': rw_mgmt_ip}
538 # for primitive in sorted(primitives):
539 # await self.ExecutePrimitive(
542 # primitives[primitive]['name'],
545 # **primitives[primitive]['parameters'],
547 # except N2VCPrimitiveExecutionFailed as e:
549 # "[N2VC] Exception executing primitive: {}".format(e)
553 async def GetPrimitiveStatus(self
, model_name
, uuid
):
554 """Get the status of an executed Primitive.
556 The status of an executed Primitive will be one of three values:
563 if not self
.authenticated
:
566 model
= await self
.get_model(model_name
)
568 results
= await model
.get_action_status(uuid
)
571 status
= results
[uuid
]
573 except Exception as e
:
575 "Caught exception while getting primitive status: {}".format(e
)
577 raise N2VCPrimitiveExecutionFailed(e
)
581 async def GetPrimitiveOutput(self
, model_name
, uuid
):
582 """Get the output of an executed Primitive.
584 Note: this only returns output for a successfully executed primitive.
588 if not self
.authenticated
:
591 model
= await self
.get_model(model_name
)
592 results
= await model
.get_action_output(uuid
, 60)
593 except Exception as e
:
595 "Caught exception while getting primitive status: {}".format(e
)
597 raise N2VCPrimitiveExecutionFailed(e
)
601 # async def ProvisionMachine(self, model_name, hostname, username):
602 # """Provision machine for usage with Juju.
604 # Provisions a previously instantiated machine for use with Juju.
607 # if not self.authenticated:
610 # # FIXME: This is hard-coded until model-per-ns is added
611 # model_name = 'default'
613 # model = await self.get_model(model_name)
614 # model.add_machine(spec={})
616 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
623 # except Exception as e:
625 # "Caught exception while getting primitive status: {}".format(e)
627 # raise N2VCPrimitiveExecutionFailed(e)
629 def GetPrivateKeyPath(self
):
630 homedir
= os
.environ
['HOME']
631 sshdir
= "{}/.ssh".format(homedir
)
632 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
633 return private_key_path
635 async def GetPublicKey(self
):
636 """Get the N2VC SSH public key.abs
638 Returns the SSH public key, to be injected into virtual machines to
639 be managed by the VCA.
641 The first time this is run, a ssh keypair will be created. The public
642 key is injected into a VM so that we can provision the machine with
643 Juju, after which Juju will communicate with the VM directly via the
648 # Find the path to where we expect our key to live.
649 homedir
= os
.environ
['HOME']
650 sshdir
= "{}/.ssh".format(homedir
)
651 if not os
.path
.exists(sshdir
):
654 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
655 public_key_path
= "{}.pub".format(private_key_path
)
657 # If we don't have a key generated, generate it.
658 if not os
.path
.exists(private_key_path
):
659 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
664 subprocess
.check_output(shlex
.split(cmd
))
666 # Read the public key
667 with
open(public_key_path
, "r") as f
:
668 public_key
= f
.readline()
672 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
673 params
, callback
=None, *callback_args
):
674 """Execute multiple primitives.
676 Execute multiple primitives as declared in initial-config-primitive.
677 This is useful in cases where the primitives initially failed -- for
678 example, if the charm is a proxy but the proxy hasn't been configured
684 # Build a sequential list of the primitives to execute
685 for primitive
in params
['initial-config-primitive']:
687 if primitive
['name'] == 'config':
690 seq
= primitive
['seq']
693 if 'parameter' in primitive
:
694 params_
= primitive
['parameter']
696 user_values
= params
.get("user_values", {})
697 if 'rw_mgmt_ip' not in user_values
:
698 user_values
['rw_mgmt_ip'] = None
699 # just for backward compatibility, because it will be provided always by modern version of LCM
702 'name': primitive
['name'],
703 'parameters': self
._map
_primitive
_parameters
(
709 for primitive
in sorted(primitives
):
711 await self
.ExecutePrimitive(
714 primitives
[primitive
]['name'],
717 **primitives
[primitive
]['parameters'],
720 except N2VCPrimitiveExecutionFailed
as e
:
722 "[N2VC] Exception executing primitive: {}".format(e
)
727 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
728 callback
, *callback_args
, **params
):
729 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
731 Execute a primitive defined in the VNF descriptor.
733 :param str model_name: The name or unique id of the network service.
734 :param str application_name: The name of the application
735 :param str primitive: The name of the primitive to execute.
736 :param obj callback: A callback function to receive status changes.
737 :param tuple callback_args: A list of arguments to be passed to the
739 :param dict params: A dictionary of key=value pairs representing the
740 primitive's parameters
743 'rw_mgmt_ip': '1.2.3.4',
744 # Pass the initial-config-primitives section of the vnf or vdu
745 'initial-config-primitives': {...}
748 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
751 if not self
.authenticated
:
754 model
= await self
.get_model(model_name
)
756 if primitive
== 'config':
757 # config is special, and expecting params to be a dictionary
758 await self
.set_config(
764 app
= await self
.get_application(model
, application_name
)
766 # Run against the first (and probably only) unit in the app
769 action
= await unit
.run_action(primitive
, **params
)
771 except Exception as e
:
773 "Caught exception while executing primitive: {}".format(e
)
775 raise N2VCPrimitiveExecutionFailed(e
)
778 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
780 """Remove a charm from the VCA.
782 Remove a charm referenced in a VNF Descriptor.
784 :param str model_name: The name of the network service.
785 :param str application_name: The name of the application
786 :param obj callback: A callback function to receive status changes.
787 :param tuple callback_args: A list of arguments to be passed to the
791 if not self
.authenticated
:
794 model
= await self
.get_model(model_name
)
795 app
= await self
.get_application(model
, application_name
)
797 # Remove this application from event monitoring
798 self
.monitors
[model_name
].RemoveApplication(application_name
)
800 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
802 "Removing the application {}".format(application_name
)
806 await self
.disconnect_model(self
.monitors
[model_name
])
808 self
.notify_callback(
812 "Removing charm {}".format(application_name
),
817 except Exception as e
:
818 print("Caught exception: {}".format(e
))
822 async def CreateNetworkService(self
, ns_uuid
):
823 """Create a new Juju model for the Network Service.
825 Creates a new Model in the Juju Controller.
827 :param str ns_uuid: A unique id representing an instaance of a
830 :returns: True if the model was created. Raises JujuError on failure.
832 if not self
.authenticated
:
835 models
= await self
.controller
.list_models()
836 if ns_uuid
not in models
:
838 self
.models
[ns_uuid
] = await self
.controller
.add_model(
841 except JujuError
as e
:
842 if "already exists" not in e
.message
:
845 # Create an observer for this model
846 await self
.create_model_monitor(ns_uuid
)
850 async def DestroyNetworkService(self
, ns_uuid
):
851 """Destroy a Network Service.
853 Destroy the Network Service and any deployed charms.
855 :param ns_uuid The unique id of the Network Service
857 :returns: True if the model was created. Raises JujuError on failure.
860 # Do not delete the default model. The default model was used by all
861 # Network Services, prior to the implementation of a model per NS.
862 if ns_uuid
.lower() == "default":
865 if not self
.authenticated
:
866 self
.log
.debug("Authenticating with Juju")
869 # Disconnect from the Model
870 if ns_uuid
in self
.models
:
871 await self
.disconnect_model(self
.models
[ns_uuid
])
874 await self
.controller
.destroy_models(ns_uuid
)
876 raise NetworkServiceDoesNotExist(
877 "The Network Service '{}' does not exist".format(ns_uuid
)
882 async def GetMetrics(self
, model_name
, application_name
):
883 """Get the metrics collected by the VCA.
885 :param model_name The name or unique id of the network service
886 :param application_name The name of the application
889 model
= await self
.get_model(model_name
)
890 app
= await self
.get_application(model
, application_name
)
892 metrics
= await app
.get_metrics()
896 async def HasApplication(self
, model_name
, application_name
):
897 model
= await self
.get_model(model_name
)
898 app
= await self
.get_application(model
, application_name
)
904 async def add_relation(self
, model_name
, relation1
, relation2
):
906 Add a relation between two application endpoints.
908 :param str model_name: The name or unique id of the network service
909 :param str relation1: '<application>[:<relation_name>]'
910 :param str relation2: '<application>[:<relation_name>]'
913 if not self
.authenticated
:
916 m
= await self
.get_model(model_name
)
918 await m
.add_relation(relation1
, relation2
)
919 except JujuAPIError
as e
:
920 # If one of the applications in the relationship doesn't exist,
921 # or the relation has already been added, let the operation fail
923 if 'not found' in e
.message
:
925 if 'already exists' in e
.message
:
930 # async def apply_config(self, config, application):
931 # """Apply a configuration to the application."""
932 # print("JujuApi: Applying configuration to {}.".format(
935 # return await self.set_config(application=application, config=config)
937 def _get_config_from_dict(self
, config_primitive
, values
):
938 """Transform the yang config primitive to dict.
947 for primitive
in config_primitive
:
948 if primitive
['name'] == 'config':
949 # config = self._map_primitive_parameters()
950 for parameter
in primitive
['parameter']:
951 param
= str(parameter
['name'])
952 if parameter
['value'] == "<rw_mgmt_ip>":
953 config
[param
] = str(values
[parameter
['value']])
955 config
[param
] = str(parameter
['value'])
959 def _map_primitive_parameters(self
, parameters
, user_values
):
961 for parameter
in parameters
:
962 param
= str(parameter
['name'])
963 value
= parameter
.get('value')
965 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
966 # Must exist at user_values except if there is a default value
967 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
968 if parameter
['value'][1:-1] in user_values
:
969 value
= user_values
[parameter
['value'][1:-1]]
970 elif 'default-value' in parameter
:
971 value
= parameter
['default-value']
973 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
975 # If there's no value, use the default-value (if set)
976 if value
is None and 'default-value' in parameter
:
977 value
= parameter
['default-value']
979 # Typecast parameter value, if present
982 if 'data-type' in parameter
:
983 paramtype
= str(parameter
['data-type']).lower()
985 if paramtype
== "integer":
987 elif paramtype
== "boolean":
992 # If there's no data-type, assume the value is a string
995 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
997 params
[param
] = value
1000 def _get_config_from_yang(self
, config_primitive
, values
):
1001 """Transform the yang config primitive to dict."""
1003 for primitive
in config_primitive
.values():
1004 if primitive
['name'] == 'config':
1005 for parameter
in primitive
['parameter'].values():
1006 param
= str(parameter
['name'])
1007 if parameter
['value'] == "<rw_mgmt_ip>":
1008 config
[param
] = str(values
[parameter
['value']])
1010 config
[param
] = str(parameter
['value'])
1014 def FormatApplicationName(self
, *args
):
1016 Generate a Juju-compatible Application name
1018 :param args tuple: Positional arguments to be used to construct the
1022 - Only accepts characters a-z and non-consequitive dashes (-)
1023 - Application name should not exceed 50 characters
1027 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1030 for c
in "-".join(list(args
)):
1032 c
= chr(97 + int(c
))
1033 elif not c
.isalpha():
1036 return re
.sub('-+', '-', appname
.lower())
1038 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1039 # """Format the name of the application
1042 # - Only accepts characters a-z and non-consequitive dashes (-)
1043 # - Application name should not exceed 50 characters
1045 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1049 # c = chr(97 + int(c))
1050 # elif not c.isalpha():
1053 # return re.sub('\-+', '-', new_name.lower())
1055 def format_model_name(self
, name
):
1056 """Format the name of model.
1058 Model names may only contain lowercase letters, digits and hyphens
1061 return name
.replace('_', '-').lower()
1063 async def get_application(self
, model
, application
):
1064 """Get the deployed application."""
1065 if not self
.authenticated
:
1069 if application
and model
:
1070 if model
.applications
:
1071 if application
in model
.applications
:
1072 app
= model
.applications
[application
]
1076 async def get_model(self
, model_name
):
1077 """Get a model from the Juju Controller.
1079 Note: Model objects returned must call disconnected() before it goes
1081 if not self
.authenticated
:
1084 if model_name
not in self
.models
:
1085 # Get the models in the controller
1086 models
= await self
.controller
.list_models()
1088 if model_name
not in models
:
1090 self
.models
[model_name
] = await self
.controller
.add_model(
1093 except JujuError
as e
:
1094 if "already exists" not in e
.message
:
1097 self
.models
[model_name
] = await self
.controller
.get_model(
1101 self
.refcount
['model'] += 1
1103 # Create an observer for this model
1104 await self
.create_model_monitor(model_name
)
1106 return self
.models
[model_name
]
1108 async def create_model_monitor(self
, model_name
):
1109 """Create a monitor for the model, if none exists."""
1110 if not self
.authenticated
:
1113 if model_name
not in self
.monitors
:
1114 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1115 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1119 async def login(self
):
1120 """Login to the Juju controller."""
1122 if self
.authenticated
:
1125 self
.connecting
= True
1127 self
.log
.debug("JujuApi: Logging into controller")
1129 self
.controller
= Controller(loop
=self
.loop
)
1133 "Connecting to controller... ws://{}:{} as {}/{}".format(
1140 await self
.controller
.connect(
1141 endpoint
=self
.endpoint
,
1143 password
=self
.secret
,
1144 cacert
=self
.ca_cert
,
1146 self
.refcount
['controller'] += 1
1148 # current_controller no longer exists
1149 # self.log.debug("Connecting to current controller...")
1150 # await self.controller.connect_current()
1151 # await self.controller.connect(
1152 # endpoint=self.endpoint,
1153 # username=self.user,
1156 self
.log
.fatal("VCA credentials not configured.")
1158 self
.authenticated
= True
1159 self
.log
.debug("JujuApi: Logged into controller")
1161 async def logout(self
):
1162 """Logout of the Juju controller."""
1163 if not self
.authenticated
:
1167 for model
in self
.models
:
1168 await self
.disconnect_model(model
)
1171 self
.log
.debug("Disconnecting controller {}".format(
1174 await self
.controller
.disconnect()
1175 self
.refcount
['controller'] -= 1
1176 self
.controller
= None
1178 self
.authenticated
= False
1180 self
.log
.debug(self
.refcount
)
1182 except Exception as e
:
1184 "Fatal error logging out of Juju Controller: {}".format(e
)
1189 async def disconnect_model(self
, model
):
1190 self
.log
.debug("Disconnecting model {}".format(model
))
1191 if model
in self
.models
:
1192 print("Disconnecting model")
1193 await self
.models
[model
].disconnect()
1194 self
.refcount
['model'] -= 1
1195 self
.models
[model
] = None
1197 # async def remove_application(self, name):
1198 # """Remove the application."""
1199 # if not self.authenticated:
1200 # await self.login()
1202 # app = await self.get_application(name)
1204 # self.log.debug("JujuApi: Destroying application {}".format(
1208 # await app.destroy()
1210 async def remove_relation(self
, a
, b
):
1212 Remove a relation between two application endpoints
1214 :param a An application endpoint
1215 :param b An application endpoint
1217 if not self
.authenticated
:
1220 m
= await self
.get_model()
1222 m
.remove_relation(a
, b
)
1224 await m
.disconnect()
1226 async def resolve_error(self
, model_name
, application
=None):
1227 """Resolve units in error state."""
1228 if not self
.authenticated
:
1231 model
= await self
.get_model(model_name
)
1233 app
= await self
.get_application(model
, application
)
1236 "JujuApi: Resolving errors for application {}".format(
1241 for unit
in app
.units
:
1242 app
.resolved(retry
=True)
1244 async def run_action(self
, model_name
, application
, action_name
, **params
):
1245 """Execute an action and return an Action object."""
1246 if not self
.authenticated
:
1256 model
= await self
.get_model(model_name
)
1258 app
= await self
.get_application(model
, application
)
1260 # We currently only have one unit per application
1261 # so use the first unit available.
1265 "JujuApi: Running Action {} against Application {}".format(
1271 action
= await unit
.run_action(action_name
, **params
)
1273 # Wait for the action to complete
1276 result
['status'] = action
.status
1277 result
['action']['tag'] = action
.data
['id']
1278 result
['action']['results'] = action
.results
1282 async def set_config(self
, model_name
, application
, config
):
1283 """Apply a configuration to the application."""
1284 if not self
.authenticated
:
1287 app
= await self
.get_application(model_name
, application
)
1289 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1292 await app
.set_config(config
)
1294 # Verify the config is set
1295 newconf
= await app
.get_config()
1297 if config
[key
] != newconf
[key
]['value']:
1298 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1300 # async def set_parameter(self, parameter, value, application=None):
1301 # """Set a config parameter for a service."""
1302 # if not self.authenticated:
1303 # await self.login()
1305 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1310 # return await self.apply_config(
1311 # {parameter: value},
1312 # application=application,
1315 async def wait_for_application(self
, model_name
, application_name
,
1317 """Wait for an application to become active."""
1318 if not self
.authenticated
:
1321 model
= await self
.get_model(model_name
)
1323 app
= await self
.get_application(model
, application_name
)
1324 self
.log
.debug("Application: {}".format(app
))
1327 "JujuApi: Waiting {} seconds for Application {}".format(
1333 await model
.block_until(
1335 unit
.agent_status
== 'idle' and unit
.workload_status
in
1336 ['active', 'unknown'] for unit
in app
.units