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 class PrimitiveDoesNotExist(Exception):
51 """The Primitive being executed does not exist."""
53 # Quiet the debug logging
54 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
55 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
56 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
57 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
60 class VCAMonitor(ModelObserver
):
61 """Monitor state changes within the Juju Model."""
64 def __init__(self
, ns_name
):
65 self
.log
= logging
.getLogger(__name__
)
67 self
.ns_name
= ns_name
68 self
.applications
= {}
70 def AddApplication(self
, application_name
, callback
, *callback_args
):
71 if application_name
not in self
.applications
:
72 self
.applications
[application_name
] = {
74 'callback_args': callback_args
77 def RemoveApplication(self
, application_name
):
78 if application_name
in self
.applications
:
79 del self
.applications
[application_name
]
81 async def on_change(self
, delta
, old
, new
, model
):
82 """React to changes in the Juju model."""
84 if delta
.entity
== "unit":
85 # Ignore change events from other applications
86 if delta
.data
['application'] not in self
.applications
.keys():
91 application_name
= delta
.data
['application']
93 callback
= self
.applications
[application_name
]['callback']
95 self
.applications
[application_name
]['callback_args']
98 # Fire off a callback with the application state
102 delta
.data
['application'],
104 new
.workload_status_message
,
108 # This is a charm being removed
112 delta
.data
['application'],
116 except Exception as e
:
117 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
119 elif delta
.entity
== "action":
120 # TODO: Decide how we want to notify the user of actions
122 # uuid = delta.data['id'] # The Action's unique id
123 # msg = delta.data['message'] # The output of the action
125 # if delta.data['status'] == "pending":
126 # # The action is queued
128 # elif delta.data['status'] == "completed""
129 # # The action was successful
131 # elif delta.data['status'] == "failed":
132 # # The action failed.
140 # Create unique models per network service
141 # Document all public functions
153 juju_public_key
=None,
157 :param log obj: The logging object to log to
158 :param server str: The IP Address or Hostname of the Juju controller
159 :param port int: The port of the Juju Controller
160 :param user str: The Juju username to authenticate with
161 :param secret str: The Juju password to authenticate with
162 :param artifacts str: The directory where charms required by a vnfd are
164 :param loop obj: The loop to use.
165 :param juju_public_key str: The contents of the Juju public SSH key
166 :param ca_cert str: The CA certificate to use to authenticate
170 client = n2vc.vnf.N2VC(
176 artifacts='/app/storage/myvnf/charms',
178 juju_public_key='<contents of the juju public key>',
179 ca_cert='<contents of CA certificate>',
183 # Initialize instance-level variables
186 self
.controller
= None
187 self
.connecting
= False
188 self
.authenticated
= False
207 self
.juju_public_key
= juju_public_key
209 self
._create
_juju
_public
_key
(juju_public_key
)
211 self
.ca_cert
= ca_cert
216 self
.log
= logging
.getLogger(__name__
)
218 # Quiet websocket traffic
219 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
220 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
221 logging
.getLogger('model').setLevel(logging
.WARN
)
222 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
224 self
.log
.debug('JujuApi: instantiated')
230 if user
.startswith('user-'):
233 self
.user
= 'user-{}'.format(user
)
235 self
.endpoint
= '%s:%d' % (server
, int(port
))
237 self
.artifacts
= artifacts
239 self
.loop
= loop
or asyncio
.get_event_loop()
242 """Close any open connections."""
245 def _create_juju_public_key(self
, public_key
):
246 """Recreate the Juju public key on disk.
248 Certain libjuju commands expect to be run from the same machine as Juju
249 is bootstrapped to. This method will write the public key to disk in
250 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
252 # Make sure that we have a public key before writing to disk
253 if public_key
is None or len(public_key
) == 0:
254 if 'OSM_VCA_PUBKEY' in os
.environ
:
255 public_key
= os
.getenv('OSM_VCA_PUBKEY', '')
256 if len(public_key
== 0):
261 path
= "{}/.local/share/juju/ssh".format(
262 os
.path
.expanduser('~'),
264 if not os
.path
.exists(path
):
267 with
open('{}/juju_id_rsa.pub'.format(path
), 'w') as f
:
270 def notify_callback(self
, model_name
, application_name
, status
, message
,
271 callback
=None, *callback_args
):
280 except Exception as e
:
281 self
.log
.error("[0] notify_callback exception {}".format(e
))
286 async def Relate(self
, model_name
, vnfd
):
287 """Create a relation between the charm-enabled VDUs in a VNF.
289 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
295 - provides: dataVM:db
298 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.
300 :param str ns_name: The name of the network service.
301 :param dict vnfd: The parsed yaml VNF descriptor.
304 # Currently, the call to Relate() is made automatically after the
305 # deployment of each charm; if the relation depends on a charm that
306 # hasn't been deployed yet, the call will fail silently. This will
307 # prevent an API breakage, with the intent of making this an explicitly
308 # required call in a more object-oriented refactor of the N2VC API.
311 vnf_config
= vnfd
.get("vnf-configuration")
313 juju
= vnf_config
['juju']
315 configs
.append(vnf_config
)
317 for vdu
in vnfd
['vdu']:
318 vdu_config
= vdu
.get('vdu-configuration')
320 juju
= vdu_config
['juju']
322 configs
.append(vdu_config
)
324 def _get_application_name(name
):
325 """Get the application name that's mapped to a vnf/vdu."""
327 vnf_name
= vnfd
['name']
329 for vdu
in vnfd
.get('vdu'):
330 # Compare the named portion of the relation to the vdu's id
331 if vdu
['id'] == name
:
332 application_name
= self
.FormatApplicationName(
335 str(vnf_member_index
),
337 return application_name
339 vnf_member_index
+= 1
343 # Loop through relations
347 if 'vca-relationships' in juju
and 'relation' in juju
['vca-relationships']:
348 for rel
in juju
['vca-relationships']['relation']:
351 # get the application name for the provides
352 (name
, endpoint
) = rel
['provides'].split(':')
353 application_name
= _get_application_name(name
)
355 provides
= "{}:{}".format(
360 # get the application name for thr requires
361 (name
, endpoint
) = rel
['requires'].split(':')
362 application_name
= _get_application_name(name
)
364 requires
= "{}:{}".format(
368 self
.log
.debug("Relation: {} <-> {}".format(
372 await self
.add_relation(
377 except Exception as e
:
378 self
.log
.debug("Exception: {}".format(e
))
382 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
383 charm_path
, params
={}, machine_spec
={},
384 callback
=None, *callback_args
):
385 """Deploy one or more charms associated with a VNF.
387 Deploy the charm(s) referenced in a VNF Descriptor.
389 :param str model_name: The name or unique id of the network service.
390 :param str application_name: The name of the application
391 :param dict vnfd: The name of the application
392 :param str charm_path: The path to the Juju charm
393 :param dict params: A dictionary of runtime parameters
396 'rw_mgmt_ip': '1.2.3.4',
397 # Pass the initial-config-primitives section of the vnf or vdu
398 'initial-config-primitives': {...}
399 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
400 inside < >. rw_mgmt_ip will be included here also
402 :param dict machine_spec: A dictionary describing the machine to
406 'hostname': '1.2.3.4',
407 'username': 'ubuntu',
409 :param obj callback: A callback function to receive status changes.
410 :param tuple callback_args: A list of arguments to be passed to the
414 ########################################################
415 # Verify the path to the charm exists and is readable. #
416 ########################################################
417 if not os
.path
.exists(charm_path
):
418 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
419 self
.notify_callback(
426 raise JujuCharmNotFound("No artifacts configured.")
428 ################################
429 # Login to the Juju controller #
430 ################################
431 if not self
.authenticated
:
432 self
.log
.debug("Authenticating with Juju")
435 ##########################################
436 # Get the model for this network service #
437 ##########################################
438 model
= await self
.get_model(model_name
)
440 ########################################
441 # Verify the application doesn't exist #
442 ########################################
443 app
= await self
.get_application(model
, application_name
)
445 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
447 ################################################################
448 # Register this application with the model-level event monitor #
449 ################################################################
451 self
.log
.debug("JujuApi: Registering callback for {}".format(
454 await self
.Subscribe(model_name
, application_name
, callback
, *callback_args
)
456 ########################################################
457 # Check for specific machine placement (native charms) #
458 ########################################################
460 if machine_spec
.keys():
461 if all(k
in machine_spec
for k
in ['host', 'user']):
462 # Enlist an existing machine as a Juju unit
463 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
464 machine_spec
['username'],
465 machine_spec
['hostname'],
466 self
.GetPrivateKeyPath(),
470 #######################################
471 # Get the initial charm configuration #
472 #######################################
475 if 'rw_mgmt_ip' in params
:
476 rw_mgmt_ip
= params
['rw_mgmt_ip']
478 if 'initial-config-primitive' not in params
:
479 params
['initial-config-primitive'] = {}
481 initial_config
= self
._get
_config
_from
_dict
(
482 params
['initial-config-primitive'],
483 {'<rw_mgmt_ip>': rw_mgmt_ip
}
486 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
493 ########################################################
494 # Deploy the charm and apply the initial configuration #
495 ########################################################
496 app
= await model
.deploy(
497 # We expect charm_path to be either the path to the charm on disk
498 # or in the format of cs:series/name
500 # This is the formatted, unique name for this charm
501 application_name
=application_name
,
502 # Proxy charms should use the current LTS. This will need to be
503 # changed for native charms.
505 # Apply the initial 'config' primitive during deployment
506 config
=initial_config
,
507 # Where to deploy the charm to.
510 #############################
511 # Map the vdu id<->app name #
512 #############################
514 await self
.Relate(model_name
, vnfd
)
515 except KeyError as ex
:
516 # We don't currently support relations between NS and VNF/VDU charms
517 self
.log
.warn("[N2VC] Relations not supported: {}".format(ex
))
518 except Exception as ex
:
519 # This may happen if not all of the charms needed by the relation
520 # are ready. We can safely ignore this, because Relate will be
521 # retried when the endpoint of the relation is deployed.
522 self
.log
.warn("[N2VC] Relations not ready")
524 # #######################################
525 # # Execute initial config primitive(s) #
526 # #######################################
527 uuids
= await self
.ExecuteInitialPrimitives(
536 # # Build a sequential list of the primitives to execute
537 # for primitive in params['initial-config-primitive']:
539 # if primitive['name'] == 'config':
540 # # This is applied when the Application is deployed
543 # seq = primitive['seq']
546 # if 'parameter' in primitive:
547 # params = primitive['parameter']
549 # primitives[seq] = {
550 # 'name': primitive['name'],
551 # 'parameters': self._map_primitive_parameters(
553 # {'<rw_mgmt_ip>': rw_mgmt_ip}
557 # for primitive in sorted(primitives):
558 # await self.ExecutePrimitive(
561 # primitives[primitive]['name'],
564 # **primitives[primitive]['parameters'],
566 # except N2VCPrimitiveExecutionFailed as e:
568 # "[N2VC] Exception executing primitive: {}".format(e)
572 async def GetPrimitiveStatus(self
, model_name
, uuid
):
573 """Get the status of an executed Primitive.
575 The status of an executed Primitive will be one of three values:
582 if not self
.authenticated
:
585 model
= await self
.get_model(model_name
)
587 results
= await model
.get_action_status(uuid
)
590 status
= results
[uuid
]
592 except Exception as e
:
594 "Caught exception while getting primitive status: {}".format(e
)
596 raise N2VCPrimitiveExecutionFailed(e
)
600 async def GetPrimitiveOutput(self
, model_name
, uuid
):
601 """Get the output of an executed Primitive.
603 Note: this only returns output for a successfully executed primitive.
607 if not self
.authenticated
:
610 model
= await self
.get_model(model_name
)
611 results
= await model
.get_action_output(uuid
, 60)
612 except Exception as e
:
614 "Caught exception while getting primitive status: {}".format(e
)
616 raise N2VCPrimitiveExecutionFailed(e
)
620 # async def ProvisionMachine(self, model_name, hostname, username):
621 # """Provision machine for usage with Juju.
623 # Provisions a previously instantiated machine for use with Juju.
626 # if not self.authenticated:
629 # # FIXME: This is hard-coded until model-per-ns is added
630 # model_name = 'default'
632 # model = await self.get_model(model_name)
633 # model.add_machine(spec={})
635 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
642 # except Exception as e:
644 # "Caught exception while getting primitive status: {}".format(e)
646 # raise N2VCPrimitiveExecutionFailed(e)
648 def GetPrivateKeyPath(self
):
649 homedir
= os
.environ
['HOME']
650 sshdir
= "{}/.ssh".format(homedir
)
651 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
652 return private_key_path
654 async def GetPublicKey(self
):
655 """Get the N2VC SSH public key.abs
657 Returns the SSH public key, to be injected into virtual machines to
658 be managed by the VCA.
660 The first time this is run, a ssh keypair will be created. The public
661 key is injected into a VM so that we can provision the machine with
662 Juju, after which Juju will communicate with the VM directly via the
667 # Find the path to where we expect our key to live.
668 homedir
= os
.environ
['HOME']
669 sshdir
= "{}/.ssh".format(homedir
)
670 if not os
.path
.exists(sshdir
):
673 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
674 public_key_path
= "{}.pub".format(private_key_path
)
676 # If we don't have a key generated, generate it.
677 if not os
.path
.exists(private_key_path
):
678 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
683 subprocess
.check_output(shlex
.split(cmd
))
685 # Read the public key
686 with
open(public_key_path
, "r") as f
:
687 public_key
= f
.readline()
691 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
692 params
, callback
=None, *callback_args
):
693 """Execute multiple primitives.
695 Execute multiple primitives as declared in initial-config-primitive.
696 This is useful in cases where the primitives initially failed -- for
697 example, if the charm is a proxy but the proxy hasn't been configured
703 # Build a sequential list of the primitives to execute
704 for primitive
in params
['initial-config-primitive']:
706 if primitive
['name'] == 'config':
709 seq
= primitive
['seq']
712 if 'parameter' in primitive
:
713 params_
= primitive
['parameter']
715 user_values
= params
.get("user_values", {})
716 if 'rw_mgmt_ip' not in user_values
:
717 user_values
['rw_mgmt_ip'] = None
718 # just for backward compatibility, because it will be provided always by modern version of LCM
721 'name': primitive
['name'],
722 'parameters': self
._map
_primitive
_parameters
(
728 for primitive
in sorted(primitives
):
730 # self.log.debug("Queuing action {}".format(primitives[primitive]['name']))
732 await self
.ExecutePrimitive(
735 primitives
[primitive
]['name'],
738 **primitives
[primitive
]['parameters'],
741 except PrimitiveDoesNotExist
as e
:
742 self
.log
.debug("Ignoring exception PrimitiveDoesNotExist: {}".format(e
))
744 except Exception as e
:
745 self
.log
.debug("XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}".format(e
))
748 except N2VCPrimitiveExecutionFailed
as e
:
750 "[N2VC] Exception executing primitive: {}".format(e
)
755 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
756 callback
, *callback_args
, **params
):
757 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
759 Execute a primitive defined in the VNF descriptor.
761 :param str model_name: The name or unique id of the network service.
762 :param str application_name: The name of the application
763 :param str primitive: The name of the primitive to execute.
764 :param obj callback: A callback function to receive status changes.
765 :param tuple callback_args: A list of arguments to be passed to the
767 :param dict params: A dictionary of key=value pairs representing the
768 primitive's parameters
771 'rw_mgmt_ip': '1.2.3.4',
772 # Pass the initial-config-primitives section of the vnf or vdu
773 'initial-config-primitives': {...}
776 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
779 if not self
.authenticated
:
782 model
= await self
.get_model(model_name
)
784 if primitive
== 'config':
785 # config is special, and expecting params to be a dictionary
786 await self
.set_config(
792 app
= await self
.get_application(model
, application_name
)
794 # Does this primitive exist?
795 actions
= await app
.get_actions()
797 if primitive
not in actions
.keys():
798 raise PrimitiveDoesNotExist("Primitive {} does not exist".format(primitive
))
800 # Run against the first (and probably only) unit in the app
803 action
= await unit
.run_action(primitive
, **params
)
805 except PrimitiveDoesNotExist
as e
:
806 # Catch and raise this exception if it's thrown from the inner block
808 except Exception as e
:
809 # An unexpected exception was caught
811 "Caught exception while executing primitive: {}".format(e
)
813 raise N2VCPrimitiveExecutionFailed(e
)
816 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
818 """Remove a charm from the VCA.
820 Remove a charm referenced in a VNF Descriptor.
822 :param str model_name: The name of the network service.
823 :param str application_name: The name of the application
824 :param obj callback: A callback function to receive status changes.
825 :param tuple callback_args: A list of arguments to be passed to the
829 if not self
.authenticated
:
832 model
= await self
.get_model(model_name
)
833 app
= await self
.get_application(model
, application_name
)
835 # Remove this application from event monitoring
836 await self
.Unsubscribe(model_name
, application_name
)
838 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
840 "Removing the application {}".format(application_name
)
844 await self
.disconnect_model(self
.monitors
[model_name
])
846 self
.notify_callback(
850 "Removing charm {}".format(application_name
),
855 except Exception as e
:
856 print("Caught exception: {}".format(e
))
860 async def CreateNetworkService(self
, ns_uuid
):
861 """Create a new Juju model for the Network Service.
863 Creates a new Model in the Juju Controller.
865 :param str ns_uuid: A unique id representing an instaance of a
868 :returns: True if the model was created. Raises JujuError on failure.
870 if not self
.authenticated
:
873 models
= await self
.controller
.list_models()
874 if ns_uuid
not in models
:
876 self
.models
[ns_uuid
] = await self
.controller
.add_model(
879 except JujuError
as e
:
880 if "already exists" not in e
.message
:
883 # Create an observer for this model
884 await self
.create_model_monitor(ns_uuid
)
888 async def DestroyNetworkService(self
, ns_uuid
):
889 """Destroy a Network Service.
891 Destroy the Network Service and any deployed charms.
893 :param ns_uuid The unique id of the Network Service
895 :returns: True if the model was created. Raises JujuError on failure.
898 # Do not delete the default model. The default model was used by all
899 # Network Services, prior to the implementation of a model per NS.
900 if ns_uuid
.lower() == "default":
903 if not self
.authenticated
:
904 self
.log
.debug("Authenticating with Juju")
907 # Disconnect from the Model
908 if ns_uuid
in self
.models
:
909 await self
.disconnect_model(self
.models
[ns_uuid
])
912 await self
.controller
.destroy_models(ns_uuid
)
914 raise NetworkServiceDoesNotExist(
915 "The Network Service '{}' does not exist".format(ns_uuid
)
920 async def GetMetrics(self
, model_name
, application_name
):
921 """Get the metrics collected by the VCA.
923 :param model_name The name or unique id of the network service
924 :param application_name The name of the application
927 model
= await self
.get_model(model_name
)
928 app
= await self
.get_application(model
, application_name
)
930 metrics
= await app
.get_metrics()
934 async def HasApplication(self
, model_name
, application_name
):
935 model
= await self
.get_model(model_name
)
936 app
= await self
.get_application(model
, application_name
)
941 async def Subscribe(self
, ns_name
, application_name
, callback
, *callback_args
):
942 """Subscribe to callbacks for an application.
944 :param ns_name str: The name of the Network Service
945 :param application_name str: The name of the application
946 :param callback obj: The callback method
947 :param callback_args list: The list of arguments to append to calls to
950 self
.monitors
[ns_name
].AddApplication(
956 async def Unsubscribe(self
, ns_name
, application_name
):
957 """Unsubscribe to callbacks for an application.
959 Unsubscribes the caller from notifications from a deployed application.
961 :param ns_name str: The name of the Network Service
962 :param application_name str: The name of the application
964 self
.monitors
[ns_name
].RemoveApplication(
969 async def add_relation(self
, model_name
, relation1
, relation2
):
971 Add a relation between two application endpoints.
973 :param str model_name: The name or unique id of the network service
974 :param str relation1: '<application>[:<relation_name>]'
975 :param str relation2: '<application>[:<relation_name>]'
978 if not self
.authenticated
:
981 m
= await self
.get_model(model_name
)
983 await m
.add_relation(relation1
, relation2
)
984 except JujuAPIError
as e
:
985 # If one of the applications in the relationship doesn't exist,
986 # or the relation has already been added, let the operation fail
988 if 'not found' in e
.message
:
990 if 'already exists' in e
.message
:
995 # async def apply_config(self, config, application):
996 # """Apply a configuration to the application."""
997 # print("JujuApi: Applying configuration to {}.".format(
1000 # return await self.set_config(application=application, config=config)
1002 def _get_config_from_dict(self
, config_primitive
, values
):
1003 """Transform the yang config primitive to dict.
1012 for primitive
in config_primitive
:
1013 if primitive
['name'] == 'config':
1014 # config = self._map_primitive_parameters()
1015 for parameter
in primitive
['parameter']:
1016 param
= str(parameter
['name'])
1017 if parameter
['value'] == "<rw_mgmt_ip>":
1018 config
[param
] = str(values
[parameter
['value']])
1020 config
[param
] = str(parameter
['value'])
1024 def _map_primitive_parameters(self
, parameters
, user_values
):
1026 for parameter
in parameters
:
1027 param
= str(parameter
['name'])
1028 value
= parameter
.get('value')
1030 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1031 # Must exist at user_values except if there is a default value
1032 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
1033 if parameter
['value'][1:-1] in user_values
:
1034 value
= user_values
[parameter
['value'][1:-1]]
1035 elif 'default-value' in parameter
:
1036 value
= parameter
['default-value']
1038 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
1040 # If there's no value, use the default-value (if set)
1041 if value
is None and 'default-value' in parameter
:
1042 value
= parameter
['default-value']
1044 # Typecast parameter value, if present
1045 paramtype
= "string"
1047 if 'data-type' in parameter
:
1048 paramtype
= str(parameter
['data-type']).lower()
1050 if paramtype
== "integer":
1052 elif paramtype
== "boolean":
1057 # If there's no data-type, assume the value is a string
1060 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
1062 params
[param
] = value
1065 def _get_config_from_yang(self
, config_primitive
, values
):
1066 """Transform the yang config primitive to dict."""
1068 for primitive
in config_primitive
.values():
1069 if primitive
['name'] == 'config':
1070 for parameter
in primitive
['parameter'].values():
1071 param
= str(parameter
['name'])
1072 if parameter
['value'] == "<rw_mgmt_ip>":
1073 config
[param
] = str(values
[parameter
['value']])
1075 config
[param
] = str(parameter
['value'])
1079 def FormatApplicationName(self
, *args
):
1081 Generate a Juju-compatible Application name
1083 :param args tuple: Positional arguments to be used to construct the
1087 - Only accepts characters a-z and non-consequitive dashes (-)
1088 - Application name should not exceed 50 characters
1092 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1095 for c
in "-".join(list(args
)):
1097 c
= chr(97 + int(c
))
1098 elif not c
.isalpha():
1101 return re
.sub('-+', '-', appname
.lower())
1103 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1104 # """Format the name of the application
1107 # - Only accepts characters a-z and non-consequitive dashes (-)
1108 # - Application name should not exceed 50 characters
1110 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1114 # c = chr(97 + int(c))
1115 # elif not c.isalpha():
1118 # return re.sub('\-+', '-', new_name.lower())
1120 def format_model_name(self
, name
):
1121 """Format the name of model.
1123 Model names may only contain lowercase letters, digits and hyphens
1126 return name
.replace('_', '-').lower()
1128 async def get_application(self
, model
, application
):
1129 """Get the deployed application."""
1130 if not self
.authenticated
:
1134 if application
and model
:
1135 if model
.applications
:
1136 if application
in model
.applications
:
1137 app
= model
.applications
[application
]
1141 async def get_model(self
, model_name
):
1142 """Get a model from the Juju Controller.
1144 Note: Model objects returned must call disconnected() before it goes
1146 if not self
.authenticated
:
1149 if model_name
not in self
.models
:
1150 # Get the models in the controller
1151 models
= await self
.controller
.list_models()
1153 if model_name
not in models
:
1155 self
.models
[model_name
] = await self
.controller
.add_model(
1158 except JujuError
as e
:
1159 if "already exists" not in e
.message
:
1162 self
.models
[model_name
] = await self
.controller
.get_model(
1166 self
.refcount
['model'] += 1
1168 # Create an observer for this model
1169 await self
.create_model_monitor(model_name
)
1171 return self
.models
[model_name
]
1173 async def create_model_monitor(self
, model_name
):
1174 """Create a monitor for the model, if none exists."""
1175 if not self
.authenticated
:
1178 if model_name
not in self
.monitors
:
1179 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1180 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1184 async def login(self
):
1185 """Login to the Juju controller."""
1187 if self
.authenticated
:
1190 self
.connecting
= True
1192 self
.log
.debug("JujuApi: Logging into controller")
1194 self
.controller
= Controller(loop
=self
.loop
)
1198 "Connecting to controller... ws://{}:{} as {}/{}".format(
1205 await self
.controller
.connect(
1206 endpoint
=self
.endpoint
,
1208 password
=self
.secret
,
1209 cacert
=self
.ca_cert
,
1211 self
.refcount
['controller'] += 1
1213 # current_controller no longer exists
1214 # self.log.debug("Connecting to current controller...")
1215 # await self.controller.connect_current()
1216 # await self.controller.connect(
1217 # endpoint=self.endpoint,
1218 # username=self.user,
1221 self
.log
.fatal("VCA credentials not configured.")
1223 self
.authenticated
= True
1224 self
.log
.debug("JujuApi: Logged into controller")
1226 async def logout(self
):
1227 """Logout of the Juju controller."""
1228 if not self
.authenticated
:
1232 for model
in self
.models
:
1233 await self
.disconnect_model(model
)
1236 self
.log
.debug("Disconnecting controller {}".format(
1239 await self
.controller
.disconnect()
1240 self
.refcount
['controller'] -= 1
1241 self
.controller
= None
1243 self
.authenticated
= False
1245 self
.log
.debug(self
.refcount
)
1247 except Exception as e
:
1249 "Fatal error logging out of Juju Controller: {}".format(e
)
1254 async def disconnect_model(self
, model
):
1255 self
.log
.debug("Disconnecting model {}".format(model
))
1256 if model
in self
.models
:
1257 print("Disconnecting model")
1258 await self
.models
[model
].disconnect()
1259 self
.refcount
['model'] -= 1
1260 self
.models
[model
] = None
1262 # async def remove_application(self, name):
1263 # """Remove the application."""
1264 # if not self.authenticated:
1265 # await self.login()
1267 # app = await self.get_application(name)
1269 # self.log.debug("JujuApi: Destroying application {}".format(
1273 # await app.destroy()
1275 async def remove_relation(self
, a
, b
):
1277 Remove a relation between two application endpoints
1279 :param a An application endpoint
1280 :param b An application endpoint
1282 if not self
.authenticated
:
1285 m
= await self
.get_model()
1287 m
.remove_relation(a
, b
)
1289 await m
.disconnect()
1291 async def resolve_error(self
, model_name
, application
=None):
1292 """Resolve units in error state."""
1293 if not self
.authenticated
:
1296 model
= await self
.get_model(model_name
)
1298 app
= await self
.get_application(model
, application
)
1301 "JujuApi: Resolving errors for application {}".format(
1306 for unit
in app
.units
:
1307 app
.resolved(retry
=True)
1309 async def run_action(self
, model_name
, application
, action_name
, **params
):
1310 """Execute an action and return an Action object."""
1311 if not self
.authenticated
:
1321 model
= await self
.get_model(model_name
)
1323 app
= await self
.get_application(model
, application
)
1325 # We currently only have one unit per application
1326 # so use the first unit available.
1330 "JujuApi: Running Action {} against Application {}".format(
1336 action
= await unit
.run_action(action_name
, **params
)
1338 # Wait for the action to complete
1341 result
['status'] = action
.status
1342 result
['action']['tag'] = action
.data
['id']
1343 result
['action']['results'] = action
.results
1347 async def set_config(self
, model_name
, application
, config
):
1348 """Apply a configuration to the application."""
1349 if not self
.authenticated
:
1352 app
= await self
.get_application(model_name
, application
)
1354 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1357 await app
.set_config(config
)
1359 # Verify the config is set
1360 newconf
= await app
.get_config()
1362 if config
[key
] != newconf
[key
]['value']:
1363 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1365 # async def set_parameter(self, parameter, value, application=None):
1366 # """Set a config parameter for a service."""
1367 # if not self.authenticated:
1368 # await self.login()
1370 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1375 # return await self.apply_config(
1376 # {parameter: value},
1377 # application=application,
1380 async def wait_for_application(self
, model_name
, application_name
,
1382 """Wait for an application to become active."""
1383 if not self
.authenticated
:
1386 model
= await self
.get_model(model_name
)
1388 app
= await self
.get_application(model
, application_name
)
1389 self
.log
.debug("Application: {}".format(app
))
1392 "JujuApi: Waiting {} seconds for Application {}".format(
1398 await model
.block_until(
1400 unit
.agent_status
== 'idle' and unit
.workload_status
in
1401 ['active', 'unknown'] for unit
in app
.units