12 # FIXME: this should load the juju inside or modules without having to
13 # explicitly install it. Check why it's not working.
14 # Load our subtree of the juju library
15 path
= os
.path
.abspath(os
.path
.join(os
.path
.dirname(__file__
), '..'))
16 path
= os
.path
.join(path
, "modules/libjuju/")
17 if path
not in sys
.path
:
18 sys
.path
.insert(1, path
)
20 from juju
.controller
import Controller
21 from juju
.model
import ModelObserver
22 from juju
.errors
import JujuAPIError
24 # We might need this to connect to the websocket securely, but test and verify.
26 ssl
._create
_default
_https
_context
= ssl
._create
_unverified
_context
27 except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
34 class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
38 class JujuApplicationExists(Exception):
39 """The Application already exists."""
42 class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
46 # Quiet the debug logging
47 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
48 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
49 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
50 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
53 class VCAMonitor(ModelObserver
):
54 """Monitor state changes within the Juju Model."""
59 def __init__(self
, ns_name
):
60 self
.log
= logging
.getLogger(__name__
)
62 self
.ns_name
= ns_name
64 def AddApplication(self
, application_name
, callback
, *callback_args
):
65 if application_name
not in self
.applications
:
66 self
.applications
[application_name
] = {
68 'callback_args': callback_args
71 def RemoveApplication(self
, application_name
):
72 if application_name
in self
.applications
:
73 del self
.applications
[application_name
]
75 async def on_change(self
, delta
, old
, new
, model
):
76 """React to changes in the Juju model."""
78 if delta
.entity
== "unit":
79 # Ignore change events from other applications
80 if delta
.data
['application'] not in self
.applications
.keys():
85 application_name
= delta
.data
['application']
87 callback
= self
.applications
[application_name
]['callback']
89 self
.applications
[application_name
]['callback_args']
92 # Fire off a callback with the application state
96 delta
.data
['application'],
98 new
.workload_status_message
,
102 # This is a charm being removed
106 delta
.data
['application'],
110 except Exception as e
:
111 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
113 elif delta
.entity
== "action":
114 # TODO: Decide how we want to notify the user of actions
116 # uuid = delta.data['id'] # The Action's unique id
117 # msg = delta.data['message'] # The output of the action
119 # if delta.data['status'] == "pending":
120 # # The action is queued
122 # elif delta.data['status'] == "completed""
123 # # The action was successful
125 # elif delta.data['status'] == "failed":
126 # # The action failed.
134 # Create unique models per network service
135 # Document all public functions
150 :param vcaconfig dict A dictionary containing the VCA configuration
152 :param artifacts str The directory where charms required by a vnfd are
156 n2vc = N2VC(vcaconfig={
157 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
159 'ip-address': '10.44.127.137',
161 'artifacts': '/path/to/charms'
165 # Initialize instance-level variables
168 self
.controller
= None
169 self
.connecting
= False
170 self
.authenticated
= False
179 self
.default_model
= None
193 self
.log
= logging
.getLogger(__name__
)
195 # Quiet websocket traffic
196 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
197 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
198 logging
.getLogger('model').setLevel(logging
.WARN
)
199 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
201 self
.log
.debug('JujuApi: instantiated')
207 if user
.startswith('user-'):
210 self
.user
= 'user-{}'.format(user
)
212 self
.endpoint
= '%s:%d' % (server
, int(port
))
214 self
.artifacts
= artifacts
216 self
.loop
= loop
or asyncio
.get_event_loop()
219 """Close any open connections."""
222 def notify_callback(self
, model_name
, application_name
, status
, message
,
223 callback
=None, *callback_args
):
232 except Exception as e
:
233 self
.log
.error("[0] notify_callback exception {}".format(e
))
238 async def CreateNetworkService(self
, nsd
):
239 """Create a new model to encapsulate this network service.
241 Create a new model in the Juju controller to encapsulate the
242 charms associated with a network service.
244 You can pass either the nsd record or the id of the network
245 service, but this method will fail without one of them.
247 if not self
.authenticated
:
250 # Ideally, we will create a unique model per network service.
251 # This change will require all components, i.e., LCM and SO, to use
252 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
253 # services deployed via LCM would't be manageable via SO and vice versa
255 return self
.default_model
257 async def Relate(self
, ns_name
, vnfd
):
258 """Create a relation between the charm-enabled VDUs in a VNF.
260 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
265 - provides: dataVM:db
268 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.
270 :param str ns_name: The name of the network service.
271 :param dict vnfd: The parsed yaml VNF descriptor.
274 # Currently, the call to Relate() is made automatically after the
275 # deployment of each charm; if the relation depends on a charm that
276 # hasn't been deployed yet, the call will fail silently. This will
277 # prevent an API breakage, with the intent of making this an explicitly
278 # required call in a more object-oriented refactor of the N2VC API.
281 vnf_config
= vnfd
.get("vnf-configuration")
283 juju
= vnf_config
['juju']
285 configs
.append(vnf_config
)
287 for vdu
in vnfd
['vdu']:
288 vdu_config
= vdu
.get('vdu-configuration')
290 juju
= vdu_config
['juju']
292 configs
.append(vdu_config
)
294 def _get_application_name(name
):
295 """Get the application name that's mapped to a vnf/vdu."""
297 vnf_name
= vnfd
['name']
299 for vdu
in vnfd
.get('vdu'):
300 # Compare the named portion of the relation to the vdu's id
301 if vdu
['id'] == name
:
302 application_name
= self
.FormatApplicationName(
305 str(vnf_member_index
),
307 return application_name
309 vnf_member_index
+= 1
313 # Loop through relations
316 if 'relation' in juju
:
317 for rel
in juju
['relation']:
320 # get the application name for the provides
321 (name
, endpoint
) = rel
['provides'].split(':')
322 application_name
= _get_application_name(name
)
324 provides
= "{}:{}".format(
329 # get the application name for thr requires
330 (name
, endpoint
) = rel
['requires'].split(':')
331 application_name
= _get_application_name(name
)
333 requires
= "{}:{}".format(
337 self
.log
.debug("Relation: {} <-> {}".format(
341 await self
.add_relation(
346 except Exception as e
:
347 self
.log
.debug("Exception: {}".format(e
))
351 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
352 charm_path
, params
={}, machine_spec
={},
353 callback
=None, *callback_args
):
354 """Deploy one or more charms associated with a VNF.
356 Deploy the charm(s) referenced in a VNF Descriptor.
358 :param str model_name: The name of the network service.
359 :param str application_name: The name of the application
360 :param dict vnfd: The name of the application
361 :param str charm_path: The path to the Juju charm
362 :param dict params: A dictionary of runtime parameters
365 'rw_mgmt_ip': '1.2.3.4',
366 # Pass the initial-config-primitives section of the vnf or vdu
367 'initial-config-primitives': {...}
369 :param dict machine_spec: A dictionary describing the machine to
373 'hostname': '1.2.3.4',
374 'username': 'ubuntu',
376 :param obj callback: A callback function to receive status changes.
377 :param tuple callback_args: A list of arguments to be passed to the
381 ########################################################
382 # Verify the path to the charm exists and is readable. #
383 ########################################################
384 if not os
.path
.exists(charm_path
):
385 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
386 self
.notify_callback(
393 raise JujuCharmNotFound("No artifacts configured.")
395 ################################
396 # Login to the Juju controller #
397 ################################
398 if not self
.authenticated
:
399 self
.log
.debug("Authenticating with Juju")
402 ##########################################
403 # Get the model for this network service #
404 ##########################################
405 # TODO: In a point release, we will use a model per deployed network
406 # service. In the meantime, we will always use the 'default' model.
407 model_name
= 'default'
408 model
= await self
.get_model(model_name
)
410 ########################################
411 # Verify the application doesn't exist #
412 ########################################
413 app
= await self
.get_application(model
, application_name
)
415 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
417 ################################################################
418 # Register this application with the model-level event monitor #
419 ################################################################
421 self
.monitors
[model_name
].AddApplication(
427 ########################################################
428 # Check for specific machine placement (native charms) #
429 ########################################################
431 if machine_spec
.keys():
432 if all(k
in machine_spec
for k
in ['host', 'user']):
433 # Enlist an existing machine as a Juju unit
434 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
435 machine_spec
['user'],
436 machine_spec
['host'],
437 self
.GetPrivateKeyPath(),
441 #######################################
442 # Get the initial charm configuration #
443 #######################################
446 if 'rw_mgmt_ip' in params
:
447 rw_mgmt_ip
= params
['rw_mgmt_ip']
449 if 'initial-config-primitive' not in params
:
450 params
['initial-config-primitive'] = {}
452 initial_config
= self
._get
_config
_from
_dict
(
453 params
['initial-config-primitive'],
454 {'<rw_mgmt_ip>': rw_mgmt_ip
}
457 self
.log
.debug("JujuApi: Deploying charm ({}) from {}".format(
463 ########################################################
464 # Deploy the charm and apply the initial configuration #
465 ########################################################
466 app
= await model
.deploy(
467 # We expect charm_path to be either the path to the charm on disk
468 # or in the format of cs:series/name
470 # This is the formatted, unique name for this charm
471 application_name
=application_name
,
472 # Proxy charms should use the current LTS. This will need to be
473 # changed for native charms.
475 # Apply the initial 'config' primitive during deployment
476 config
=initial_config
,
477 # Where to deploy the charm to.
481 # Map the vdu id<->app name,
483 await self
.Relate(model_name
, vnfd
)
485 # #######################################
486 # # Execute initial config primitive(s) #
487 # #######################################
488 uuids
= await self
.ExecuteInitialPrimitives(
497 # # Build a sequential list of the primitives to execute
498 # for primitive in params['initial-config-primitive']:
500 # if primitive['name'] == 'config':
501 # # This is applied when the Application is deployed
504 # seq = primitive['seq']
507 # if 'parameter' in primitive:
508 # params = primitive['parameter']
510 # primitives[seq] = {
511 # 'name': primitive['name'],
512 # 'parameters': self._map_primitive_parameters(
514 # {'<rw_mgmt_ip>': rw_mgmt_ip}
518 # for primitive in sorted(primitives):
519 # await self.ExecutePrimitive(
522 # primitives[primitive]['name'],
525 # **primitives[primitive]['parameters'],
527 # except N2VCPrimitiveExecutionFailed as e:
529 # "[N2VC] Exception executing primitive: {}".format(e)
533 async def GetPrimitiveStatus(self
, model_name
, uuid
):
534 """Get the status of an executed Primitive.
536 The status of an executed Primitive will be one of three values:
543 if not self
.authenticated
:
546 # FIXME: This is hard-coded until model-per-ns is added
547 model_name
= 'default'
549 model
= await self
.get_model(model_name
)
551 results
= await model
.get_action_status(uuid
)
554 status
= results
[uuid
]
556 except Exception as e
:
558 "Caught exception while getting primitive status: {}".format(e
)
560 raise N2VCPrimitiveExecutionFailed(e
)
564 async def GetPrimitiveOutput(self
, model_name
, uuid
):
565 """Get the output of an executed Primitive.
567 Note: this only returns output for a successfully executed primitive.
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 results
= await model
.get_action_output(uuid
, 60)
579 except Exception as e
:
581 "Caught exception while getting primitive status: {}".format(e
)
583 raise N2VCPrimitiveExecutionFailed(e
)
587 # async def ProvisionMachine(self, model_name, hostname, username):
588 # """Provision machine for usage with Juju.
590 # Provisions a previously instantiated machine for use with Juju.
593 # if not self.authenticated:
596 # # FIXME: This is hard-coded until model-per-ns is added
597 # model_name = 'default'
599 # model = await self.get_model(model_name)
600 # model.add_machine(spec={})
602 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
609 # except Exception as e:
611 # "Caught exception while getting primitive status: {}".format(e)
613 # raise N2VCPrimitiveExecutionFailed(e)
615 def GetPrivateKeyPath(self
):
616 homedir
= os
.environ
['HOME']
617 sshdir
= "{}/.ssh".format(homedir
)
618 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
619 return private_key_path
621 async def GetPublicKey(self
):
622 """Get the N2VC SSH public key.abs
624 Returns the SSH public key, to be injected into virtual machines to
625 be managed by the VCA.
627 The first time this is run, a ssh keypair will be created. The public
628 key is injected into a VM so that we can provision the machine with
629 Juju, after which Juju will communicate with the VM directly via the
634 # Find the path to where we expect our key to live.
635 homedir
= os
.environ
['HOME']
636 sshdir
= "{}/.ssh".format(homedir
)
637 if not os
.path
.exists(sshdir
):
640 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
641 public_key_path
= "{}.pub".format(private_key_path
)
643 # If we don't have a key generated, generate it.
644 if not os
.path
.exists(private_key_path
):
645 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
650 subprocess
.check_output(shlex
.split(cmd
))
652 # Read the public key
653 with
open(public_key_path
, "r") as f
:
654 public_key
= f
.readline()
658 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
659 params
, callback
=None, *callback_args
):
660 """Execute multiple primitives.
662 Execute multiple primitives as declared in initial-config-primitive.
663 This is useful in cases where the primitives initially failed -- for
664 example, if the charm is a proxy but the proxy hasn't been configured
670 # Build a sequential list of the primitives to execute
671 for primitive
in params
['initial-config-primitive']:
673 if primitive
['name'] == 'config':
676 seq
= primitive
['seq']
679 if 'parameter' in primitive
:
680 params
= primitive
['parameter']
683 'name': primitive
['name'],
684 'parameters': self
._map
_primitive
_parameters
(
686 {'<rw_mgmt_ip>': None}
690 for primitive
in sorted(primitives
):
692 await self
.ExecutePrimitive(
695 primitives
[primitive
]['name'],
698 **primitives
[primitive
]['parameters'],
701 except N2VCPrimitiveExecutionFailed
as e
:
703 "[N2VC] Exception executing primitive: {}".format(e
)
708 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
709 callback
, *callback_args
, **params
):
710 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
712 Execute a primitive defined in the VNF descriptor.
714 :param str model_name: The name of the network service.
715 :param str application_name: The name of the application
716 :param str primitive: The name of the primitive to execute.
717 :param obj callback: A callback function to receive status changes.
718 :param tuple callback_args: A list of arguments to be passed to the
720 :param dict params: A dictionary of key=value pairs representing the
721 primitive's parameters
724 'rw_mgmt_ip': '1.2.3.4',
725 # Pass the initial-config-primitives section of the vnf or vdu
726 'initial-config-primitives': {...}
729 self
.log
.debug("Executing {}".format(primitive
))
732 if not self
.authenticated
:
735 # FIXME: This is hard-coded until model-per-ns is added
736 model_name
= 'default'
738 model
= await self
.get_model(model_name
)
740 if primitive
== 'config':
741 # config is special, and expecting params to be a dictionary
742 await self
.set_config(
748 app
= await self
.get_application(model
, application_name
)
750 # Run against the first (and probably only) unit in the app
753 action
= await unit
.run_action(primitive
, **params
)
755 except Exception as e
:
757 "Caught exception while executing primitive: {}".format(e
)
759 raise N2VCPrimitiveExecutionFailed(e
)
762 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
764 """Remove a charm from the VCA.
766 Remove a charm referenced in a VNF Descriptor.
768 :param str model_name: The name of the network service.
769 :param str application_name: The name of the application
770 :param obj callback: A callback function to receive status changes.
771 :param tuple callback_args: A list of arguments to be passed to the
775 if not self
.authenticated
:
778 model
= await self
.get_model(model_name
)
779 app
= await self
.get_application(model
, application_name
)
781 # Remove this application from event monitoring
782 self
.monitors
[model_name
].RemoveApplication(application_name
)
784 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
786 "Removing the application {}".format(application_name
)
790 # Notify the callback that this charm has been removed.
791 self
.notify_callback(
799 except Exception as e
:
800 print("Caught exception: {}".format(e
))
804 async def DestroyNetworkService(self
, nsd
):
805 raise NotImplementedError()
807 async def GetMetrics(self
, model_name
, application_name
):
808 """Get the metrics collected by the VCA.
810 :param model_name The name of the model
811 :param application_name The name of the application
814 model
= await self
.get_model(model_name
)
815 app
= await self
.get_application(model
, application_name
)
817 metrics
= await app
.get_metrics()
821 async def HasApplication(self
, model_name
, application_name
):
822 model
= await self
.get_model(model_name
)
823 app
= await self
.get_application(model
, application_name
)
829 async def add_relation(self
, model_name
, relation1
, relation2
):
831 Add a relation between two application endpoints.
833 :param str model_name Name of the network service.
834 :param str relation1 '<application>[:<relation_name>]'
835 :param str relation12 '<application>[:<relation_name>]'
838 if not self
.authenticated
:
841 m
= await self
.get_model(model_name
)
843 await m
.add_relation(relation1
, relation2
)
844 except JujuAPIError
as e
:
845 # If one of the applications in the relationship doesn't exist,
846 # or the relation has already been added, let the operation fail
848 if 'not found' in e
.message
:
850 if 'already exists' in e
.message
:
855 # async def apply_config(self, config, application):
856 # """Apply a configuration to the application."""
857 # print("JujuApi: Applying configuration to {}.".format(
860 # return await self.set_config(application=application, config=config)
862 def _get_config_from_dict(self
, config_primitive
, values
):
863 """Transform the yang config primitive to dict.
872 for primitive
in config_primitive
:
873 if primitive
['name'] == 'config':
874 # config = self._map_primitive_parameters()
875 for parameter
in primitive
['parameter']:
876 param
= str(parameter
['name'])
877 if parameter
['value'] == "<rw_mgmt_ip>":
878 config
[param
] = str(values
[parameter
['value']])
880 config
[param
] = str(parameter
['value'])
884 def _map_primitive_parameters(self
, parameters
, values
):
886 for parameter
in parameters
:
887 param
= str(parameter
['name'])
890 # If there's no value, use the default-value (if set)
891 if parameter
['value'] is None and 'default-value' in parameter
:
892 value
= parameter
['default-value']
894 # Typecast parameter value, if present
895 if 'data-type' in parameter
:
896 paramtype
= str(parameter
['data-type']).lower()
898 if paramtype
== "integer":
899 value
= int(parameter
['value'])
900 elif paramtype
== "boolean":
901 value
= bool(parameter
['value'])
903 value
= str(parameter
['value'])
905 # If there's no data-type, assume the value is a string
906 value
= str(parameter
['value'])
908 if parameter
['value'] == "<rw_mgmt_ip>":
909 params
[param
] = str(values
[parameter
['value']])
911 params
[param
] = value
914 def _get_config_from_yang(self
, config_primitive
, values
):
915 """Transform the yang config primitive to dict."""
917 for primitive
in config_primitive
.values():
918 if primitive
['name'] == 'config':
919 for parameter
in primitive
['parameter'].values():
920 param
= str(parameter
['name'])
921 if parameter
['value'] == "<rw_mgmt_ip>":
922 config
[param
] = str(values
[parameter
['value']])
924 config
[param
] = str(parameter
['value'])
928 def FormatApplicationName(self
, *args
):
930 Generate a Juju-compatible Application name
932 :param args tuple: Positional arguments to be used to construct the
936 - Only accepts characters a-z and non-consequitive dashes (-)
937 - Application name should not exceed 50 characters
941 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
944 for c
in "-".join(list(args
)):
947 elif not c
.isalpha():
950 return re
.sub('\-+', '-', appname
.lower())
952 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
953 # """Format the name of the application
956 # - Only accepts characters a-z and non-consequitive dashes (-)
957 # - Application name should not exceed 50 characters
959 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
963 # c = chr(97 + int(c))
964 # elif not c.isalpha():
967 # return re.sub('\-+', '-', new_name.lower())
969 def format_model_name(self
, name
):
970 """Format the name of model.
972 Model names may only contain lowercase letters, digits and hyphens
975 return name
.replace('_', '-').lower()
977 async def get_application(self
, model
, application
):
978 """Get the deployed application."""
979 if not self
.authenticated
:
983 if application
and model
:
984 if model
.applications
:
985 if application
in model
.applications
:
986 app
= model
.applications
[application
]
990 async def get_model(self
, model_name
='default'):
991 """Get a model from the Juju Controller.
993 Note: Model objects returned must call disconnected() before it goes
995 if not self
.authenticated
:
998 if model_name
not in self
.models
:
999 self
.models
[model_name
] = await self
.controller
.get_model(
1002 self
.refcount
['model'] += 1
1004 # Create an observer for this model
1005 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1006 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1008 return self
.models
[model_name
]
1010 async def login(self
):
1011 """Login to the Juju controller."""
1013 if self
.authenticated
:
1016 self
.connecting
= True
1018 self
.log
.debug("JujuApi: Logging into controller")
1021 self
.controller
= Controller(loop
=self
.loop
)
1025 "Connecting to controller... ws://{}:{} as {}/{}".format(
1032 await self
.controller
.connect(
1033 endpoint
=self
.endpoint
,
1035 password
=self
.secret
,
1038 self
.refcount
['controller'] += 1
1040 # current_controller no longer exists
1041 # self.log.debug("Connecting to current controller...")
1042 # await self.controller.connect_current()
1043 # await self.controller.connect(
1044 # endpoint=self.endpoint,
1045 # username=self.user,
1048 self
.log
.fatal("VCA credentials not configured.")
1050 self
.authenticated
= True
1051 self
.log
.debug("JujuApi: Logged into controller")
1053 async def logout(self
):
1054 """Logout of the Juju controller."""
1055 if not self
.authenticated
:
1059 if self
.default_model
:
1060 self
.log
.debug("Disconnecting model {}".format(
1063 await self
.default_model
.disconnect()
1064 self
.refcount
['model'] -= 1
1065 self
.default_model
= None
1067 for model
in self
.models
:
1068 await self
.models
[model
].disconnect()
1069 self
.refcount
['model'] -= 1
1070 self
.models
[model
] = None
1073 self
.log
.debug("Disconnecting controller {}".format(
1076 await self
.controller
.disconnect()
1077 self
.refcount
['controller'] -= 1
1078 self
.controller
= None
1080 self
.authenticated
= False
1082 self
.log
.debug(self
.refcount
)
1084 except Exception as e
:
1086 "Fatal error logging out of Juju Controller: {}".format(e
)
1090 # async def remove_application(self, name):
1091 # """Remove the application."""
1092 # if not self.authenticated:
1093 # await self.login()
1095 # app = await self.get_application(name)
1097 # self.log.debug("JujuApi: Destroying application {}".format(
1101 # await app.destroy()
1103 async def remove_relation(self
, a
, b
):
1105 Remove a relation between two application endpoints
1107 :param a An application endpoint
1108 :param b An application endpoint
1110 if not self
.authenticated
:
1113 m
= await self
.get_model()
1115 m
.remove_relation(a
, b
)
1117 await m
.disconnect()
1119 async def resolve_error(self
, application
=None):
1120 """Resolve units in error state."""
1121 if not self
.authenticated
:
1124 app
= await self
.get_application(self
.default_model
, application
)
1127 "JujuApi: Resolving errors for application {}".format(
1132 for unit
in app
.units
:
1133 app
.resolved(retry
=True)
1135 async def run_action(self
, application
, action_name
, **params
):
1136 """Execute an action and return an Action object."""
1137 if not self
.authenticated
:
1146 app
= await self
.get_application(self
.default_model
, application
)
1148 # We currently only have one unit per application
1149 # so use the first unit available.
1153 "JujuApi: Running Action {} against Application {}".format(
1159 action
= await unit
.run_action(action_name
, **params
)
1161 # Wait for the action to complete
1164 result
['status'] = action
.status
1165 result
['action']['tag'] = action
.data
['id']
1166 result
['action']['results'] = action
.results
1170 async def set_config(self
, model_name
, application
, config
):
1171 """Apply a configuration to the application."""
1172 if not self
.authenticated
:
1175 app
= await self
.get_application(model_name
, application
)
1177 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1180 await app
.set_config(config
)
1182 # Verify the config is set
1183 newconf
= await app
.get_config()
1185 if config
[key
] != newconf
[key
]['value']:
1186 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1188 # async def set_parameter(self, parameter, value, application=None):
1189 # """Set a config parameter for a service."""
1190 # if not self.authenticated:
1191 # await self.login()
1193 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1198 # return await self.apply_config(
1199 # {parameter: value},
1200 # application=application,
1203 async def wait_for_application(self
, model_name
, application_name
,
1205 """Wait for an application to become active."""
1206 if not self
.authenticated
:
1209 # TODO: In a point release, we will use a model per deployed network
1210 # service. In the meantime, we will always use the 'default' model.
1211 model_name
= 'default'
1212 model
= await self
.get_model(model_name
)
1214 app
= await self
.get_application(model
, application_name
)
1215 self
.log
.debug("Application: {}".format(app
))
1216 # app = await self.get_application(model_name, application_name)
1219 "JujuApi: Waiting {} seconds for Application {}".format(
1225 await model
.block_until(
1227 unit
.agent_status
== 'idle' and unit
.workload_status
in
1228 ['active', 'unknown'] for unit
in app
.units