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 # Make sure that we have a public key before writing to disk
250 if public_key
is None or len(public_key
) == 0:
251 if 'OSM_VCA_PUBKEY' in os
.environ
:
252 public_key
= os
.getenv('OSM_VCA_PUBKEY', '')
253 if len(public_key
== 0):
258 path
= "{}/.local/share/juju/ssh".format(
259 os
.path
.expanduser('~'),
261 if not os
.path
.exists(path
):
264 with
open('{}/juju_id_rsa.pub'.format(path
), 'w') as f
:
267 def notify_callback(self
, model_name
, application_name
, status
, message
,
268 callback
=None, *callback_args
):
277 except Exception as e
:
278 self
.log
.error("[0] notify_callback exception {}".format(e
))
283 async def Relate(self
, model_name
, vnfd
):
284 """Create a relation between the charm-enabled VDUs in a VNF.
286 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
291 - provides: dataVM:db
294 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.
296 :param str ns_name: The name of the network service.
297 :param dict vnfd: The parsed yaml VNF descriptor.
300 # Currently, the call to Relate() is made automatically after the
301 # deployment of each charm; if the relation depends on a charm that
302 # hasn't been deployed yet, the call will fail silently. This will
303 # prevent an API breakage, with the intent of making this an explicitly
304 # required call in a more object-oriented refactor of the N2VC API.
307 vnf_config
= vnfd
.get("vnf-configuration")
309 juju
= vnf_config
['juju']
311 configs
.append(vnf_config
)
313 for vdu
in vnfd
['vdu']:
314 vdu_config
= vdu
.get('vdu-configuration')
316 juju
= vdu_config
['juju']
318 configs
.append(vdu_config
)
320 def _get_application_name(name
):
321 """Get the application name that's mapped to a vnf/vdu."""
323 vnf_name
= vnfd
['name']
325 for vdu
in vnfd
.get('vdu'):
326 # Compare the named portion of the relation to the vdu's id
327 if vdu
['id'] == name
:
328 application_name
= self
.FormatApplicationName(
331 str(vnf_member_index
),
333 return application_name
335 vnf_member_index
+= 1
339 # Loop through relations
343 if 'relation' in juju
:
344 for rel
in juju
['relation']:
347 # get the application name for the provides
348 (name
, endpoint
) = rel
['provides'].split(':')
349 application_name
= _get_application_name(name
)
351 provides
= "{}:{}".format(
356 # get the application name for thr requires
357 (name
, endpoint
) = rel
['requires'].split(':')
358 application_name
= _get_application_name(name
)
360 requires
= "{}:{}".format(
364 self
.log
.debug("Relation: {} <-> {}".format(
368 await self
.add_relation(
373 except Exception as e
:
374 self
.log
.debug("Exception: {}".format(e
))
378 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
379 charm_path
, params
={}, machine_spec
={},
380 callback
=None, *callback_args
):
381 """Deploy one or more charms associated with a VNF.
383 Deploy the charm(s) referenced in a VNF Descriptor.
385 :param str model_name: The name or unique id of the network service.
386 :param str application_name: The name of the application
387 :param dict vnfd: The name of the application
388 :param str charm_path: The path to the Juju charm
389 :param dict params: A dictionary of runtime parameters
392 'rw_mgmt_ip': '1.2.3.4',
393 # Pass the initial-config-primitives section of the vnf or vdu
394 'initial-config-primitives': {...}
395 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
396 inside < >. rw_mgmt_ip will be included here also
398 :param dict machine_spec: A dictionary describing the machine to
402 'hostname': '1.2.3.4',
403 'username': 'ubuntu',
405 :param obj callback: A callback function to receive status changes.
406 :param tuple callback_args: A list of arguments to be passed to the
410 ########################################################
411 # Verify the path to the charm exists and is readable. #
412 ########################################################
413 if not os
.path
.exists(charm_path
):
414 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
415 self
.notify_callback(
422 raise JujuCharmNotFound("No artifacts configured.")
424 ################################
425 # Login to the Juju controller #
426 ################################
427 if not self
.authenticated
:
428 self
.log
.debug("Authenticating with Juju")
431 ##########################################
432 # Get the model for this network service #
433 ##########################################
434 model
= await self
.get_model(model_name
)
436 ########################################
437 # Verify the application doesn't exist #
438 ########################################
439 app
= await self
.get_application(model
, application_name
)
441 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
443 ################################################################
444 # Register this application with the model-level event monitor #
445 ################################################################
447 self
.log
.debug("JujuApi: Registering callback for {}".format(
450 await self
.Subscribe(model_name
, application_name
, callback
, *callback_args
)
452 ########################################################
453 # Check for specific machine placement (native charms) #
454 ########################################################
456 if machine_spec
.keys():
457 if all(k
in machine_spec
for k
in ['host', 'user']):
458 # Enlist an existing machine as a Juju unit
459 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
460 machine_spec
['username'],
461 machine_spec
['hostname'],
462 self
.GetPrivateKeyPath(),
466 #######################################
467 # Get the initial charm configuration #
468 #######################################
471 if 'rw_mgmt_ip' in params
:
472 rw_mgmt_ip
= params
['rw_mgmt_ip']
474 if 'initial-config-primitive' not in params
:
475 params
['initial-config-primitive'] = {}
477 initial_config
= self
._get
_config
_from
_dict
(
478 params
['initial-config-primitive'],
479 {'<rw_mgmt_ip>': rw_mgmt_ip
}
482 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
489 ########################################################
490 # Deploy the charm and apply the initial configuration #
491 ########################################################
492 app
= await model
.deploy(
493 # We expect charm_path to be either the path to the charm on disk
494 # or in the format of cs:series/name
496 # This is the formatted, unique name for this charm
497 application_name
=application_name
,
498 # Proxy charms should use the current LTS. This will need to be
499 # changed for native charms.
501 # Apply the initial 'config' primitive during deployment
502 config
=initial_config
,
503 # Where to deploy the charm to.
506 #############################
507 # Map the vdu id<->app name #
508 #############################
510 await self
.Relate(model_name
, vnfd
)
511 except KeyError as ex
:
512 # We don't currently support relations between NS and VNF/VDU charms
513 self
.log
.warn("[N2VC] Relations not supported: {}".format(ex
))
514 except Exception as ex
:
515 # This may happen if not all of the charms needed by the relation
516 # are ready. We can safely ignore this, because Relate will be
517 # retried when the endpoint of the relation is deployed.
518 self
.log
.warn("[N2VC] Relations not ready")
520 # #######################################
521 # # Execute initial config primitive(s) #
522 # #######################################
523 uuids
= await self
.ExecuteInitialPrimitives(
532 # # Build a sequential list of the primitives to execute
533 # for primitive in params['initial-config-primitive']:
535 # if primitive['name'] == 'config':
536 # # This is applied when the Application is deployed
539 # seq = primitive['seq']
542 # if 'parameter' in primitive:
543 # params = primitive['parameter']
545 # primitives[seq] = {
546 # 'name': primitive['name'],
547 # 'parameters': self._map_primitive_parameters(
549 # {'<rw_mgmt_ip>': rw_mgmt_ip}
553 # for primitive in sorted(primitives):
554 # await self.ExecutePrimitive(
557 # primitives[primitive]['name'],
560 # **primitives[primitive]['parameters'],
562 # except N2VCPrimitiveExecutionFailed as e:
564 # "[N2VC] Exception executing primitive: {}".format(e)
568 async def GetPrimitiveStatus(self
, model_name
, uuid
):
569 """Get the status of an executed Primitive.
571 The status of an executed Primitive will be one of three values:
578 if not self
.authenticated
:
581 model
= await self
.get_model(model_name
)
583 results
= await model
.get_action_status(uuid
)
586 status
= results
[uuid
]
588 except Exception as e
:
590 "Caught exception while getting primitive status: {}".format(e
)
592 raise N2VCPrimitiveExecutionFailed(e
)
596 async def GetPrimitiveOutput(self
, model_name
, uuid
):
597 """Get the output of an executed Primitive.
599 Note: this only returns output for a successfully executed primitive.
603 if not self
.authenticated
:
606 model
= await self
.get_model(model_name
)
607 results
= await model
.get_action_output(uuid
, 60)
608 except Exception as e
:
610 "Caught exception while getting primitive status: {}".format(e
)
612 raise N2VCPrimitiveExecutionFailed(e
)
616 # async def ProvisionMachine(self, model_name, hostname, username):
617 # """Provision machine for usage with Juju.
619 # Provisions a previously instantiated machine for use with Juju.
622 # if not self.authenticated:
625 # # FIXME: This is hard-coded until model-per-ns is added
626 # model_name = 'default'
628 # model = await self.get_model(model_name)
629 # model.add_machine(spec={})
631 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
638 # except Exception as e:
640 # "Caught exception while getting primitive status: {}".format(e)
642 # raise N2VCPrimitiveExecutionFailed(e)
644 def GetPrivateKeyPath(self
):
645 homedir
= os
.environ
['HOME']
646 sshdir
= "{}/.ssh".format(homedir
)
647 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
648 return private_key_path
650 async def GetPublicKey(self
):
651 """Get the N2VC SSH public key.abs
653 Returns the SSH public key, to be injected into virtual machines to
654 be managed by the VCA.
656 The first time this is run, a ssh keypair will be created. The public
657 key is injected into a VM so that we can provision the machine with
658 Juju, after which Juju will communicate with the VM directly via the
663 # Find the path to where we expect our key to live.
664 homedir
= os
.environ
['HOME']
665 sshdir
= "{}/.ssh".format(homedir
)
666 if not os
.path
.exists(sshdir
):
669 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
670 public_key_path
= "{}.pub".format(private_key_path
)
672 # If we don't have a key generated, generate it.
673 if not os
.path
.exists(private_key_path
):
674 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
679 subprocess
.check_output(shlex
.split(cmd
))
681 # Read the public key
682 with
open(public_key_path
, "r") as f
:
683 public_key
= f
.readline()
687 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
688 params
, callback
=None, *callback_args
):
689 """Execute multiple primitives.
691 Execute multiple primitives as declared in initial-config-primitive.
692 This is useful in cases where the primitives initially failed -- for
693 example, if the charm is a proxy but the proxy hasn't been configured
699 # Build a sequential list of the primitives to execute
700 for primitive
in params
['initial-config-primitive']:
702 if primitive
['name'] == 'config':
705 seq
= primitive
['seq']
708 if 'parameter' in primitive
:
709 params_
= primitive
['parameter']
711 user_values
= params
.get("user_values", {})
712 if 'rw_mgmt_ip' not in user_values
:
713 user_values
['rw_mgmt_ip'] = None
714 # just for backward compatibility, because it will be provided always by modern version of LCM
717 'name': primitive
['name'],
718 'parameters': self
._map
_primitive
_parameters
(
724 for primitive
in sorted(primitives
):
726 await self
.ExecutePrimitive(
729 primitives
[primitive
]['name'],
732 **primitives
[primitive
]['parameters'],
735 except N2VCPrimitiveExecutionFailed
as e
:
737 "[N2VC] Exception executing primitive: {}".format(e
)
742 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
743 callback
, *callback_args
, **params
):
744 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
746 Execute a primitive defined in the VNF descriptor.
748 :param str model_name: The name or unique id of the network service.
749 :param str application_name: The name of the application
750 :param str primitive: The name of the primitive to execute.
751 :param obj callback: A callback function to receive status changes.
752 :param tuple callback_args: A list of arguments to be passed to the
754 :param dict params: A dictionary of key=value pairs representing the
755 primitive's parameters
758 'rw_mgmt_ip': '1.2.3.4',
759 # Pass the initial-config-primitives section of the vnf or vdu
760 'initial-config-primitives': {...}
763 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
766 if not self
.authenticated
:
769 model
= await self
.get_model(model_name
)
771 if primitive
== 'config':
772 # config is special, and expecting params to be a dictionary
773 await self
.set_config(
779 app
= await self
.get_application(model
, application_name
)
781 # Run against the first (and probably only) unit in the app
784 action
= await unit
.run_action(primitive
, **params
)
786 except Exception as e
:
788 "Caught exception while executing primitive: {}".format(e
)
790 raise N2VCPrimitiveExecutionFailed(e
)
793 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
795 """Remove a charm from the VCA.
797 Remove a charm referenced in a VNF Descriptor.
799 :param str model_name: The name of the network service.
800 :param str application_name: The name of the application
801 :param obj callback: A callback function to receive status changes.
802 :param tuple callback_args: A list of arguments to be passed to the
806 if not self
.authenticated
:
809 model
= await self
.get_model(model_name
)
810 app
= await self
.get_application(model
, application_name
)
812 # Remove this application from event monitoring
813 await self
.Unsubscribe(model_name
, application_name
)
815 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
817 "Removing the application {}".format(application_name
)
821 await self
.disconnect_model(self
.monitors
[model_name
])
823 self
.notify_callback(
827 "Removing charm {}".format(application_name
),
832 except Exception as e
:
833 print("Caught exception: {}".format(e
))
837 async def CreateNetworkService(self
, ns_uuid
):
838 """Create a new Juju model for the Network Service.
840 Creates a new Model in the Juju Controller.
842 :param str ns_uuid: A unique id representing an instaance of a
845 :returns: True if the model was created. Raises JujuError on failure.
847 if not self
.authenticated
:
850 models
= await self
.controller
.list_models()
851 if ns_uuid
not in models
:
853 self
.models
[ns_uuid
] = await self
.controller
.add_model(
856 except JujuError
as e
:
857 if "already exists" not in e
.message
:
860 # Create an observer for this model
861 await self
.create_model_monitor(ns_uuid
)
865 async def DestroyNetworkService(self
, ns_uuid
):
866 """Destroy a Network Service.
868 Destroy the Network Service and any deployed charms.
870 :param ns_uuid The unique id of the Network Service
872 :returns: True if the model was created. Raises JujuError on failure.
875 # Do not delete the default model. The default model was used by all
876 # Network Services, prior to the implementation of a model per NS.
877 if ns_uuid
.lower() == "default":
880 if not self
.authenticated
:
881 self
.log
.debug("Authenticating with Juju")
884 # Disconnect from the Model
885 if ns_uuid
in self
.models
:
886 await self
.disconnect_model(self
.models
[ns_uuid
])
889 await self
.controller
.destroy_models(ns_uuid
)
891 raise NetworkServiceDoesNotExist(
892 "The Network Service '{}' does not exist".format(ns_uuid
)
897 async def GetMetrics(self
, model_name
, application_name
):
898 """Get the metrics collected by the VCA.
900 :param model_name The name or unique id of the network service
901 :param application_name The name of the application
904 model
= await self
.get_model(model_name
)
905 app
= await self
.get_application(model
, application_name
)
907 metrics
= await app
.get_metrics()
911 async def HasApplication(self
, model_name
, application_name
):
912 model
= await self
.get_model(model_name
)
913 app
= await self
.get_application(model
, application_name
)
918 async def Subscribe(self
, ns_name
, application_name
, callback
, *callback_args
):
919 """Subscribe to callbacks for an application.
921 :param ns_name str: The name of the Network Service
922 :param application_name str: The name of the application
923 :param callback obj: The callback method
924 :param callback_args list: The list of arguments to append to calls to
927 self
.monitors
[ns_name
].AddApplication(
933 async def Unsubscribe(self
, ns_name
, application_name
):
934 """Unsubscribe to callbacks for an application.
936 Unsubscribes the caller from notifications from a deployed application.
938 :param ns_name str: The name of the Network Service
939 :param application_name str: The name of the application
941 self
.monitors
[ns_name
].RemoveApplication(
946 async def add_relation(self
, model_name
, relation1
, relation2
):
948 Add a relation between two application endpoints.
950 :param str model_name: The name or unique id of the network service
951 :param str relation1: '<application>[:<relation_name>]'
952 :param str relation2: '<application>[:<relation_name>]'
955 if not self
.authenticated
:
958 m
= await self
.get_model(model_name
)
960 await m
.add_relation(relation1
, relation2
)
961 except JujuAPIError
as e
:
962 # If one of the applications in the relationship doesn't exist,
963 # or the relation has already been added, let the operation fail
965 if 'not found' in e
.message
:
967 if 'already exists' in e
.message
:
972 # async def apply_config(self, config, application):
973 # """Apply a configuration to the application."""
974 # print("JujuApi: Applying configuration to {}.".format(
977 # return await self.set_config(application=application, config=config)
979 def _get_config_from_dict(self
, config_primitive
, values
):
980 """Transform the yang config primitive to dict.
989 for primitive
in config_primitive
:
990 if primitive
['name'] == 'config':
991 # config = self._map_primitive_parameters()
992 for parameter
in primitive
['parameter']:
993 param
= str(parameter
['name'])
994 if parameter
['value'] == "<rw_mgmt_ip>":
995 config
[param
] = str(values
[parameter
['value']])
997 config
[param
] = str(parameter
['value'])
1001 def _map_primitive_parameters(self
, parameters
, user_values
):
1003 for parameter
in parameters
:
1004 param
= str(parameter
['name'])
1005 value
= parameter
.get('value')
1007 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1008 # Must exist at user_values except if there is a default value
1009 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
1010 if parameter
['value'][1:-1] in user_values
:
1011 value
= user_values
[parameter
['value'][1:-1]]
1012 elif 'default-value' in parameter
:
1013 value
= parameter
['default-value']
1015 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
1017 # If there's no value, use the default-value (if set)
1018 if value
is None and 'default-value' in parameter
:
1019 value
= parameter
['default-value']
1021 # Typecast parameter value, if present
1022 paramtype
= "string"
1024 if 'data-type' in parameter
:
1025 paramtype
= str(parameter
['data-type']).lower()
1027 if paramtype
== "integer":
1029 elif paramtype
== "boolean":
1034 # If there's no data-type, assume the value is a string
1037 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
1039 params
[param
] = value
1042 def _get_config_from_yang(self
, config_primitive
, values
):
1043 """Transform the yang config primitive to dict."""
1045 for primitive
in config_primitive
.values():
1046 if primitive
['name'] == 'config':
1047 for parameter
in primitive
['parameter'].values():
1048 param
= str(parameter
['name'])
1049 if parameter
['value'] == "<rw_mgmt_ip>":
1050 config
[param
] = str(values
[parameter
['value']])
1052 config
[param
] = str(parameter
['value'])
1056 def FormatApplicationName(self
, *args
):
1058 Generate a Juju-compatible Application name
1060 :param args tuple: Positional arguments to be used to construct the
1064 - Only accepts characters a-z and non-consequitive dashes (-)
1065 - Application name should not exceed 50 characters
1069 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1072 for c
in "-".join(list(args
)):
1074 c
= chr(97 + int(c
))
1075 elif not c
.isalpha():
1078 return re
.sub('-+', '-', appname
.lower())
1080 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1081 # """Format the name of the application
1084 # - Only accepts characters a-z and non-consequitive dashes (-)
1085 # - Application name should not exceed 50 characters
1087 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1091 # c = chr(97 + int(c))
1092 # elif not c.isalpha():
1095 # return re.sub('\-+', '-', new_name.lower())
1097 def format_model_name(self
, name
):
1098 """Format the name of model.
1100 Model names may only contain lowercase letters, digits and hyphens
1103 return name
.replace('_', '-').lower()
1105 async def get_application(self
, model
, application
):
1106 """Get the deployed application."""
1107 if not self
.authenticated
:
1111 if application
and model
:
1112 if model
.applications
:
1113 if application
in model
.applications
:
1114 app
= model
.applications
[application
]
1118 async def get_model(self
, model_name
):
1119 """Get a model from the Juju Controller.
1121 Note: Model objects returned must call disconnected() before it goes
1123 if not self
.authenticated
:
1126 if model_name
not in self
.models
:
1127 # Get the models in the controller
1128 models
= await self
.controller
.list_models()
1130 if model_name
not in models
:
1132 self
.models
[model_name
] = await self
.controller
.add_model(
1135 except JujuError
as e
:
1136 if "already exists" not in e
.message
:
1139 self
.models
[model_name
] = await self
.controller
.get_model(
1143 self
.refcount
['model'] += 1
1145 # Create an observer for this model
1146 await self
.create_model_monitor(model_name
)
1148 return self
.models
[model_name
]
1150 async def create_model_monitor(self
, model_name
):
1151 """Create a monitor for the model, if none exists."""
1152 if not self
.authenticated
:
1155 if model_name
not in self
.monitors
:
1156 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1157 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1161 async def login(self
):
1162 """Login to the Juju controller."""
1164 if self
.authenticated
:
1167 self
.connecting
= True
1169 self
.log
.debug("JujuApi: Logging into controller")
1171 self
.controller
= Controller(loop
=self
.loop
)
1175 "Connecting to controller... ws://{}:{} as {}/{}".format(
1182 await self
.controller
.connect(
1183 endpoint
=self
.endpoint
,
1185 password
=self
.secret
,
1186 cacert
=self
.ca_cert
,
1188 self
.refcount
['controller'] += 1
1190 # current_controller no longer exists
1191 # self.log.debug("Connecting to current controller...")
1192 # await self.controller.connect_current()
1193 # await self.controller.connect(
1194 # endpoint=self.endpoint,
1195 # username=self.user,
1198 self
.log
.fatal("VCA credentials not configured.")
1200 self
.authenticated
= True
1201 self
.log
.debug("JujuApi: Logged into controller")
1203 async def logout(self
):
1204 """Logout of the Juju controller."""
1205 if not self
.authenticated
:
1209 for model
in self
.models
:
1210 await self
.disconnect_model(model
)
1213 self
.log
.debug("Disconnecting controller {}".format(
1216 await self
.controller
.disconnect()
1217 self
.refcount
['controller'] -= 1
1218 self
.controller
= None
1220 self
.authenticated
= False
1222 self
.log
.debug(self
.refcount
)
1224 except Exception as e
:
1226 "Fatal error logging out of Juju Controller: {}".format(e
)
1231 async def disconnect_model(self
, model
):
1232 self
.log
.debug("Disconnecting model {}".format(model
))
1233 if model
in self
.models
:
1234 print("Disconnecting model")
1235 await self
.models
[model
].disconnect()
1236 self
.refcount
['model'] -= 1
1237 self
.models
[model
] = None
1239 # async def remove_application(self, name):
1240 # """Remove the application."""
1241 # if not self.authenticated:
1242 # await self.login()
1244 # app = await self.get_application(name)
1246 # self.log.debug("JujuApi: Destroying application {}".format(
1250 # await app.destroy()
1252 async def remove_relation(self
, a
, b
):
1254 Remove a relation between two application endpoints
1256 :param a An application endpoint
1257 :param b An application endpoint
1259 if not self
.authenticated
:
1262 m
= await self
.get_model()
1264 m
.remove_relation(a
, b
)
1266 await m
.disconnect()
1268 async def resolve_error(self
, model_name
, application
=None):
1269 """Resolve units in error state."""
1270 if not self
.authenticated
:
1273 model
= await self
.get_model(model_name
)
1275 app
= await self
.get_application(model
, application
)
1278 "JujuApi: Resolving errors for application {}".format(
1283 for unit
in app
.units
:
1284 app
.resolved(retry
=True)
1286 async def run_action(self
, model_name
, application
, action_name
, **params
):
1287 """Execute an action and return an Action object."""
1288 if not self
.authenticated
:
1298 model
= await self
.get_model(model_name
)
1300 app
= await self
.get_application(model
, application
)
1302 # We currently only have one unit per application
1303 # so use the first unit available.
1307 "JujuApi: Running Action {} against Application {}".format(
1313 action
= await unit
.run_action(action_name
, **params
)
1315 # Wait for the action to complete
1318 result
['status'] = action
.status
1319 result
['action']['tag'] = action
.data
['id']
1320 result
['action']['results'] = action
.results
1324 async def set_config(self
, model_name
, application
, config
):
1325 """Apply a configuration to the application."""
1326 if not self
.authenticated
:
1329 app
= await self
.get_application(model_name
, application
)
1331 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1334 await app
.set_config(config
)
1336 # Verify the config is set
1337 newconf
= await app
.get_config()
1339 if config
[key
] != newconf
[key
]['value']:
1340 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1342 # async def set_parameter(self, parameter, value, application=None):
1343 # """Set a config parameter for a service."""
1344 # if not self.authenticated:
1345 # await self.login()
1347 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1352 # return await self.apply_config(
1353 # {parameter: value},
1354 # application=application,
1357 async def wait_for_application(self
, model_name
, application_name
,
1359 """Wait for an application to become active."""
1360 if not self
.authenticated
:
1363 model
= await self
.get_model(model_name
)
1365 app
= await self
.get_application(model
, application_name
)
1366 self
.log
.debug("Application: {}".format(app
))
1369 "JujuApi: Waiting {} seconds for Application {}".format(
1375 await model
.block_until(
1377 unit
.agent_status
== 'idle' and unit
.workload_status
in
1378 ['active', 'unknown'] for unit
in app
.units