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.
292 - provides: dataVM:db
295 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.
297 :param str ns_name: The name of the network service.
298 :param dict vnfd: The parsed yaml VNF descriptor.
301 # Currently, the call to Relate() is made automatically after the
302 # deployment of each charm; if the relation depends on a charm that
303 # hasn't been deployed yet, the call will fail silently. This will
304 # prevent an API breakage, with the intent of making this an explicitly
305 # required call in a more object-oriented refactor of the N2VC API.
308 vnf_config
= vnfd
.get("vnf-configuration")
310 juju
= vnf_config
['juju']
312 configs
.append(vnf_config
)
314 for vdu
in vnfd
['vdu']:
315 vdu_config
= vdu
.get('vdu-configuration')
317 juju
= vdu_config
['juju']
319 configs
.append(vdu_config
)
321 def _get_application_name(name
):
322 """Get the application name that's mapped to a vnf/vdu."""
324 vnf_name
= vnfd
['name']
326 for vdu
in vnfd
.get('vdu'):
327 # Compare the named portion of the relation to the vdu's id
328 if vdu
['id'] == name
:
329 application_name
= self
.FormatApplicationName(
332 str(vnf_member_index
),
334 return application_name
336 vnf_member_index
+= 1
340 # Loop through relations
344 if 'relation' in juju
:
345 for rel
in juju
['vca-relations']['relation']:
348 # get the application name for the provides
349 (name
, endpoint
) = rel
['provides'].split(':')
350 application_name
= _get_application_name(name
)
352 provides
= "{}:{}".format(
357 # get the application name for thr requires
358 (name
, endpoint
) = rel
['requires'].split(':')
359 application_name
= _get_application_name(name
)
361 requires
= "{}:{}".format(
365 self
.log
.debug("Relation: {} <-> {}".format(
369 await self
.add_relation(
374 except Exception as e
:
375 self
.log
.debug("Exception: {}".format(e
))
379 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
380 charm_path
, params
={}, machine_spec
={},
381 callback
=None, *callback_args
):
382 """Deploy one or more charms associated with a VNF.
384 Deploy the charm(s) referenced in a VNF Descriptor.
386 :param str model_name: The name or unique id of the network service.
387 :param str application_name: The name of the application
388 :param dict vnfd: The name of the application
389 :param str charm_path: The path to the Juju charm
390 :param dict params: A dictionary of runtime parameters
393 'rw_mgmt_ip': '1.2.3.4',
394 # Pass the initial-config-primitives section of the vnf or vdu
395 'initial-config-primitives': {...}
396 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
397 inside < >. rw_mgmt_ip will be included here also
399 :param dict machine_spec: A dictionary describing the machine to
403 'hostname': '1.2.3.4',
404 'username': 'ubuntu',
406 :param obj callback: A callback function to receive status changes.
407 :param tuple callback_args: A list of arguments to be passed to the
411 ########################################################
412 # Verify the path to the charm exists and is readable. #
413 ########################################################
414 if not os
.path
.exists(charm_path
):
415 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
416 self
.notify_callback(
423 raise JujuCharmNotFound("No artifacts configured.")
425 ################################
426 # Login to the Juju controller #
427 ################################
428 if not self
.authenticated
:
429 self
.log
.debug("Authenticating with Juju")
432 ##########################################
433 # Get the model for this network service #
434 ##########################################
435 model
= await self
.get_model(model_name
)
437 ########################################
438 # Verify the application doesn't exist #
439 ########################################
440 app
= await self
.get_application(model
, application_name
)
442 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
444 ################################################################
445 # Register this application with the model-level event monitor #
446 ################################################################
448 self
.log
.debug("JujuApi: Registering callback for {}".format(
451 await self
.Subscribe(model_name
, application_name
, callback
, *callback_args
)
453 ########################################################
454 # Check for specific machine placement (native charms) #
455 ########################################################
457 if machine_spec
.keys():
458 if all(k
in machine_spec
for k
in ['host', 'user']):
459 # Enlist an existing machine as a Juju unit
460 machine
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
461 machine_spec
['username'],
462 machine_spec
['hostname'],
463 self
.GetPrivateKeyPath(),
467 #######################################
468 # Get the initial charm configuration #
469 #######################################
472 if 'rw_mgmt_ip' in params
:
473 rw_mgmt_ip
= params
['rw_mgmt_ip']
475 if 'initial-config-primitive' not in params
:
476 params
['initial-config-primitive'] = {}
478 initial_config
= self
._get
_config
_from
_dict
(
479 params
['initial-config-primitive'],
480 {'<rw_mgmt_ip>': rw_mgmt_ip
}
483 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
490 ########################################################
491 # Deploy the charm and apply the initial configuration #
492 ########################################################
493 app
= await model
.deploy(
494 # We expect charm_path to be either the path to the charm on disk
495 # or in the format of cs:series/name
497 # This is the formatted, unique name for this charm
498 application_name
=application_name
,
499 # Proxy charms should use the current LTS. This will need to be
500 # changed for native charms.
502 # Apply the initial 'config' primitive during deployment
503 config
=initial_config
,
504 # Where to deploy the charm to.
507 #############################
508 # Map the vdu id<->app name #
509 #############################
511 await self
.Relate(model_name
, vnfd
)
512 except KeyError as ex
:
513 # We don't currently support relations between NS and VNF/VDU charms
514 self
.log
.warn("[N2VC] Relations not supported: {}".format(ex
))
515 except Exception as ex
:
516 # This may happen if not all of the charms needed by the relation
517 # are ready. We can safely ignore this, because Relate will be
518 # retried when the endpoint of the relation is deployed.
519 self
.log
.warn("[N2VC] Relations not ready")
521 # #######################################
522 # # Execute initial config primitive(s) #
523 # #######################################
524 uuids
= await self
.ExecuteInitialPrimitives(
533 # # Build a sequential list of the primitives to execute
534 # for primitive in params['initial-config-primitive']:
536 # if primitive['name'] == 'config':
537 # # This is applied when the Application is deployed
540 # seq = primitive['seq']
543 # if 'parameter' in primitive:
544 # params = primitive['parameter']
546 # primitives[seq] = {
547 # 'name': primitive['name'],
548 # 'parameters': self._map_primitive_parameters(
550 # {'<rw_mgmt_ip>': rw_mgmt_ip}
554 # for primitive in sorted(primitives):
555 # await self.ExecutePrimitive(
558 # primitives[primitive]['name'],
561 # **primitives[primitive]['parameters'],
563 # except N2VCPrimitiveExecutionFailed as e:
565 # "[N2VC] Exception executing primitive: {}".format(e)
569 async def GetPrimitiveStatus(self
, model_name
, uuid
):
570 """Get the status of an executed Primitive.
572 The status of an executed Primitive will be one of three values:
579 if not self
.authenticated
:
582 model
= await self
.get_model(model_name
)
584 results
= await model
.get_action_status(uuid
)
587 status
= results
[uuid
]
589 except Exception as e
:
591 "Caught exception while getting primitive status: {}".format(e
)
593 raise N2VCPrimitiveExecutionFailed(e
)
597 async def GetPrimitiveOutput(self
, model_name
, uuid
):
598 """Get the output of an executed Primitive.
600 Note: this only returns output for a successfully executed primitive.
604 if not self
.authenticated
:
607 model
= await self
.get_model(model_name
)
608 results
= await model
.get_action_output(uuid
, 60)
609 except Exception as e
:
611 "Caught exception while getting primitive status: {}".format(e
)
613 raise N2VCPrimitiveExecutionFailed(e
)
617 # async def ProvisionMachine(self, model_name, hostname, username):
618 # """Provision machine for usage with Juju.
620 # Provisions a previously instantiated machine for use with Juju.
623 # if not self.authenticated:
626 # # FIXME: This is hard-coded until model-per-ns is added
627 # model_name = 'default'
629 # model = await self.get_model(model_name)
630 # model.add_machine(spec={})
632 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
639 # except Exception as e:
641 # "Caught exception while getting primitive status: {}".format(e)
643 # raise N2VCPrimitiveExecutionFailed(e)
645 def GetPrivateKeyPath(self
):
646 homedir
= os
.environ
['HOME']
647 sshdir
= "{}/.ssh".format(homedir
)
648 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
649 return private_key_path
651 async def GetPublicKey(self
):
652 """Get the N2VC SSH public key.abs
654 Returns the SSH public key, to be injected into virtual machines to
655 be managed by the VCA.
657 The first time this is run, a ssh keypair will be created. The public
658 key is injected into a VM so that we can provision the machine with
659 Juju, after which Juju will communicate with the VM directly via the
664 # Find the path to where we expect our key to live.
665 homedir
= os
.environ
['HOME']
666 sshdir
= "{}/.ssh".format(homedir
)
667 if not os
.path
.exists(sshdir
):
670 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
671 public_key_path
= "{}.pub".format(private_key_path
)
673 # If we don't have a key generated, generate it.
674 if not os
.path
.exists(private_key_path
):
675 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
680 subprocess
.check_output(shlex
.split(cmd
))
682 # Read the public key
683 with
open(public_key_path
, "r") as f
:
684 public_key
= f
.readline()
688 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
689 params
, callback
=None, *callback_args
):
690 """Execute multiple primitives.
692 Execute multiple primitives as declared in initial-config-primitive.
693 This is useful in cases where the primitives initially failed -- for
694 example, if the charm is a proxy but the proxy hasn't been configured
700 # Build a sequential list of the primitives to execute
701 for primitive
in params
['initial-config-primitive']:
703 if primitive
['name'] == 'config':
706 seq
= primitive
['seq']
709 if 'parameter' in primitive
:
710 params_
= primitive
['parameter']
712 user_values
= params
.get("user_values", {})
713 if 'rw_mgmt_ip' not in user_values
:
714 user_values
['rw_mgmt_ip'] = None
715 # just for backward compatibility, because it will be provided always by modern version of LCM
718 'name': primitive
['name'],
719 'parameters': self
._map
_primitive
_parameters
(
725 for primitive
in sorted(primitives
):
727 await self
.ExecutePrimitive(
730 primitives
[primitive
]['name'],
733 **primitives
[primitive
]['parameters'],
736 except N2VCPrimitiveExecutionFailed
as e
:
738 "[N2VC] Exception executing primitive: {}".format(e
)
743 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
744 callback
, *callback_args
, **params
):
745 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
747 Execute a primitive defined in the VNF descriptor.
749 :param str model_name: The name or unique id of the network service.
750 :param str application_name: The name of the application
751 :param str primitive: The name of the primitive to execute.
752 :param obj callback: A callback function to receive status changes.
753 :param tuple callback_args: A list of arguments to be passed to the
755 :param dict params: A dictionary of key=value pairs representing the
756 primitive's parameters
759 'rw_mgmt_ip': '1.2.3.4',
760 # Pass the initial-config-primitives section of the vnf or vdu
761 'initial-config-primitives': {...}
764 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
767 if not self
.authenticated
:
770 model
= await self
.get_model(model_name
)
772 if primitive
== 'config':
773 # config is special, and expecting params to be a dictionary
774 await self
.set_config(
780 app
= await self
.get_application(model
, application_name
)
782 # Run against the first (and probably only) unit in the app
785 action
= await unit
.run_action(primitive
, **params
)
787 except Exception as e
:
789 "Caught exception while executing primitive: {}".format(e
)
791 raise N2VCPrimitiveExecutionFailed(e
)
794 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
796 """Remove a charm from the VCA.
798 Remove a charm referenced in a VNF Descriptor.
800 :param str model_name: The name of the network service.
801 :param str application_name: The name of the application
802 :param obj callback: A callback function to receive status changes.
803 :param tuple callback_args: A list of arguments to be passed to the
807 if not self
.authenticated
:
810 model
= await self
.get_model(model_name
)
811 app
= await self
.get_application(model
, application_name
)
813 # Remove this application from event monitoring
814 await self
.Unsubscribe(model_name
, application_name
)
816 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
818 "Removing the application {}".format(application_name
)
822 await self
.disconnect_model(self
.monitors
[model_name
])
824 self
.notify_callback(
828 "Removing charm {}".format(application_name
),
833 except Exception as e
:
834 print("Caught exception: {}".format(e
))
838 async def CreateNetworkService(self
, ns_uuid
):
839 """Create a new Juju model for the Network Service.
841 Creates a new Model in the Juju Controller.
843 :param str ns_uuid: A unique id representing an instaance of a
846 :returns: True if the model was created. Raises JujuError on failure.
848 if not self
.authenticated
:
851 models
= await self
.controller
.list_models()
852 if ns_uuid
not in models
:
854 self
.models
[ns_uuid
] = await self
.controller
.add_model(
857 except JujuError
as e
:
858 if "already exists" not in e
.message
:
861 # Create an observer for this model
862 await self
.create_model_monitor(ns_uuid
)
866 async def DestroyNetworkService(self
, ns_uuid
):
867 """Destroy a Network Service.
869 Destroy the Network Service and any deployed charms.
871 :param ns_uuid The unique id of the Network Service
873 :returns: True if the model was created. Raises JujuError on failure.
876 # Do not delete the default model. The default model was used by all
877 # Network Services, prior to the implementation of a model per NS.
878 if ns_uuid
.lower() == "default":
881 if not self
.authenticated
:
882 self
.log
.debug("Authenticating with Juju")
885 # Disconnect from the Model
886 if ns_uuid
in self
.models
:
887 await self
.disconnect_model(self
.models
[ns_uuid
])
890 await self
.controller
.destroy_models(ns_uuid
)
892 raise NetworkServiceDoesNotExist(
893 "The Network Service '{}' does not exist".format(ns_uuid
)
898 async def GetMetrics(self
, model_name
, application_name
):
899 """Get the metrics collected by the VCA.
901 :param model_name The name or unique id of the network service
902 :param application_name The name of the application
905 model
= await self
.get_model(model_name
)
906 app
= await self
.get_application(model
, application_name
)
908 metrics
= await app
.get_metrics()
912 async def HasApplication(self
, model_name
, application_name
):
913 model
= await self
.get_model(model_name
)
914 app
= await self
.get_application(model
, application_name
)
919 async def Subscribe(self
, ns_name
, application_name
, callback
, *callback_args
):
920 """Subscribe to callbacks for an application.
922 :param ns_name str: The name of the Network Service
923 :param application_name str: The name of the application
924 :param callback obj: The callback method
925 :param callback_args list: The list of arguments to append to calls to
928 self
.monitors
[ns_name
].AddApplication(
934 async def Unsubscribe(self
, ns_name
, application_name
):
935 """Unsubscribe to callbacks for an application.
937 Unsubscribes the caller from notifications from a deployed application.
939 :param ns_name str: The name of the Network Service
940 :param application_name str: The name of the application
942 self
.monitors
[ns_name
].RemoveApplication(
947 async def add_relation(self
, model_name
, relation1
, relation2
):
949 Add a relation between two application endpoints.
951 :param str model_name: The name or unique id of the network service
952 :param str relation1: '<application>[:<relation_name>]'
953 :param str relation2: '<application>[:<relation_name>]'
956 if not self
.authenticated
:
959 m
= await self
.get_model(model_name
)
961 await m
.add_relation(relation1
, relation2
)
962 except JujuAPIError
as e
:
963 # If one of the applications in the relationship doesn't exist,
964 # or the relation has already been added, let the operation fail
966 if 'not found' in e
.message
:
968 if 'already exists' in e
.message
:
973 # async def apply_config(self, config, application):
974 # """Apply a configuration to the application."""
975 # print("JujuApi: Applying configuration to {}.".format(
978 # return await self.set_config(application=application, config=config)
980 def _get_config_from_dict(self
, config_primitive
, values
):
981 """Transform the yang config primitive to dict.
990 for primitive
in config_primitive
:
991 if primitive
['name'] == 'config':
992 # config = self._map_primitive_parameters()
993 for parameter
in primitive
['parameter']:
994 param
= str(parameter
['name'])
995 if parameter
['value'] == "<rw_mgmt_ip>":
996 config
[param
] = str(values
[parameter
['value']])
998 config
[param
] = str(parameter
['value'])
1002 def _map_primitive_parameters(self
, parameters
, user_values
):
1004 for parameter
in parameters
:
1005 param
= str(parameter
['name'])
1006 value
= parameter
.get('value')
1008 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1009 # Must exist at user_values except if there is a default value
1010 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
1011 if parameter
['value'][1:-1] in user_values
:
1012 value
= user_values
[parameter
['value'][1:-1]]
1013 elif 'default-value' in parameter
:
1014 value
= parameter
['default-value']
1016 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
1018 # If there's no value, use the default-value (if set)
1019 if value
is None and 'default-value' in parameter
:
1020 value
= parameter
['default-value']
1022 # Typecast parameter value, if present
1023 paramtype
= "string"
1025 if 'data-type' in parameter
:
1026 paramtype
= str(parameter
['data-type']).lower()
1028 if paramtype
== "integer":
1030 elif paramtype
== "boolean":
1035 # If there's no data-type, assume the value is a string
1038 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
1040 params
[param
] = value
1043 def _get_config_from_yang(self
, config_primitive
, values
):
1044 """Transform the yang config primitive to dict."""
1046 for primitive
in config_primitive
.values():
1047 if primitive
['name'] == 'config':
1048 for parameter
in primitive
['parameter'].values():
1049 param
= str(parameter
['name'])
1050 if parameter
['value'] == "<rw_mgmt_ip>":
1051 config
[param
] = str(values
[parameter
['value']])
1053 config
[param
] = str(parameter
['value'])
1057 def FormatApplicationName(self
, *args
):
1059 Generate a Juju-compatible Application name
1061 :param args tuple: Positional arguments to be used to construct the
1065 - Only accepts characters a-z and non-consequitive dashes (-)
1066 - Application name should not exceed 50 characters
1070 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1073 for c
in "-".join(list(args
)):
1075 c
= chr(97 + int(c
))
1076 elif not c
.isalpha():
1079 return re
.sub('-+', '-', appname
.lower())
1081 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1082 # """Format the name of the application
1085 # - Only accepts characters a-z and non-consequitive dashes (-)
1086 # - Application name should not exceed 50 characters
1088 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1092 # c = chr(97 + int(c))
1093 # elif not c.isalpha():
1096 # return re.sub('\-+', '-', new_name.lower())
1098 def format_model_name(self
, name
):
1099 """Format the name of model.
1101 Model names may only contain lowercase letters, digits and hyphens
1104 return name
.replace('_', '-').lower()
1106 async def get_application(self
, model
, application
):
1107 """Get the deployed application."""
1108 if not self
.authenticated
:
1112 if application
and model
:
1113 if model
.applications
:
1114 if application
in model
.applications
:
1115 app
= model
.applications
[application
]
1119 async def get_model(self
, model_name
):
1120 """Get a model from the Juju Controller.
1122 Note: Model objects returned must call disconnected() before it goes
1124 if not self
.authenticated
:
1127 if model_name
not in self
.models
:
1128 # Get the models in the controller
1129 models
= await self
.controller
.list_models()
1131 if model_name
not in models
:
1133 self
.models
[model_name
] = await self
.controller
.add_model(
1136 except JujuError
as e
:
1137 if "already exists" not in e
.message
:
1140 self
.models
[model_name
] = await self
.controller
.get_model(
1144 self
.refcount
['model'] += 1
1146 # Create an observer for this model
1147 await self
.create_model_monitor(model_name
)
1149 return self
.models
[model_name
]
1151 async def create_model_monitor(self
, model_name
):
1152 """Create a monitor for the model, if none exists."""
1153 if not self
.authenticated
:
1156 if model_name
not in self
.monitors
:
1157 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1158 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1162 async def login(self
):
1163 """Login to the Juju controller."""
1165 if self
.authenticated
:
1168 self
.connecting
= True
1170 self
.log
.debug("JujuApi: Logging into controller")
1172 self
.controller
= Controller(loop
=self
.loop
)
1176 "Connecting to controller... ws://{}:{} as {}/{}".format(
1183 await self
.controller
.connect(
1184 endpoint
=self
.endpoint
,
1186 password
=self
.secret
,
1187 cacert
=self
.ca_cert
,
1189 self
.refcount
['controller'] += 1
1191 # current_controller no longer exists
1192 # self.log.debug("Connecting to current controller...")
1193 # await self.controller.connect_current()
1194 # await self.controller.connect(
1195 # endpoint=self.endpoint,
1196 # username=self.user,
1199 self
.log
.fatal("VCA credentials not configured.")
1201 self
.authenticated
= True
1202 self
.log
.debug("JujuApi: Logged into controller")
1204 async def logout(self
):
1205 """Logout of the Juju controller."""
1206 if not self
.authenticated
:
1210 for model
in self
.models
:
1211 await self
.disconnect_model(model
)
1214 self
.log
.debug("Disconnecting controller {}".format(
1217 await self
.controller
.disconnect()
1218 self
.refcount
['controller'] -= 1
1219 self
.controller
= None
1221 self
.authenticated
= False
1223 self
.log
.debug(self
.refcount
)
1225 except Exception as e
:
1227 "Fatal error logging out of Juju Controller: {}".format(e
)
1232 async def disconnect_model(self
, model
):
1233 self
.log
.debug("Disconnecting model {}".format(model
))
1234 if model
in self
.models
:
1235 print("Disconnecting model")
1236 await self
.models
[model
].disconnect()
1237 self
.refcount
['model'] -= 1
1238 self
.models
[model
] = None
1240 # async def remove_application(self, name):
1241 # """Remove the application."""
1242 # if not self.authenticated:
1243 # await self.login()
1245 # app = await self.get_application(name)
1247 # self.log.debug("JujuApi: Destroying application {}".format(
1251 # await app.destroy()
1253 async def remove_relation(self
, a
, b
):
1255 Remove a relation between two application endpoints
1257 :param a An application endpoint
1258 :param b An application endpoint
1260 if not self
.authenticated
:
1263 m
= await self
.get_model()
1265 m
.remove_relation(a
, b
)
1267 await m
.disconnect()
1269 async def resolve_error(self
, model_name
, application
=None):
1270 """Resolve units in error state."""
1271 if not self
.authenticated
:
1274 model
= await self
.get_model(model_name
)
1276 app
= await self
.get_application(model
, application
)
1279 "JujuApi: Resolving errors for application {}".format(
1284 for unit
in app
.units
:
1285 app
.resolved(retry
=True)
1287 async def run_action(self
, model_name
, application
, action_name
, **params
):
1288 """Execute an action and return an Action object."""
1289 if not self
.authenticated
:
1299 model
= await self
.get_model(model_name
)
1301 app
= await self
.get_application(model
, application
)
1303 # We currently only have one unit per application
1304 # so use the first unit available.
1308 "JujuApi: Running Action {} against Application {}".format(
1314 action
= await unit
.run_action(action_name
, **params
)
1316 # Wait for the action to complete
1319 result
['status'] = action
.status
1320 result
['action']['tag'] = action
.data
['id']
1321 result
['action']['results'] = action
.results
1325 async def set_config(self
, model_name
, application
, config
):
1326 """Apply a configuration to the application."""
1327 if not self
.authenticated
:
1330 app
= await self
.get_application(model_name
, application
)
1332 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1335 await app
.set_config(config
)
1337 # Verify the config is set
1338 newconf
= await app
.get_config()
1340 if config
[key
] != newconf
[key
]['value']:
1341 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1343 # async def set_parameter(self, parameter, value, application=None):
1344 # """Set a config parameter for a service."""
1345 # if not self.authenticated:
1346 # await self.login()
1348 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1353 # return await self.apply_config(
1354 # {parameter: value},
1355 # application=application,
1358 async def wait_for_application(self
, model_name
, application_name
,
1360 """Wait for an application to become active."""
1361 if not self
.authenticated
:
1364 model
= await self
.get_model(model_name
)
1366 app
= await self
.get_application(model
, application_name
)
1367 self
.log
.debug("Application: {}".format(app
))
1370 "JujuApi: Waiting {} seconds for Application {}".format(
1376 await model
.block_until(
1378 unit
.agent_status
== 'idle' and unit
.workload_status
in
1379 ['active', 'unknown'] for unit
in app
.units