class JujuApplicationExists(Exception):
"""The Application already exists."""
+
class N2VCPrimitiveExecutionFailed(Exception):
"""Something failed while attempting to execute a primitive."""
logging.getLogger('juju.model').setLevel(logging.WARN)
logging.getLogger('juju.machine').setLevel(logging.WARN)
+
class VCAMonitor(ModelObserver):
"""Monitor state changes within the Juju Model."""
- callback = None
- callback_args = None
log = None
ns_name = None
- application_name = None
+ applications = {}
- def __init__(self, ns_name, application_name, callback, *args):
+ def __init__(self, ns_name):
self.log = logging.getLogger(__name__)
self.ns_name = ns_name
- self.application_name = application_name
- self.callback = callback
- self.callback_args = args
+
+ def AddApplication(self, application_name, callback, *callback_args):
+ if application_name not in self.applications:
+ self.applications[application_name] = {
+ 'callback': callback,
+ 'callback_args': callback_args
+ }
+
+ def RemoveApplication(self, application_name):
+ if application_name in self.applications:
+ del self.applications[application_name]
async def on_change(self, delta, old, new, model):
"""React to changes in the Juju model."""
if delta.entity == "unit":
+ # Ignore change events from other applications
+ if delta.data['application'] not in self.applications.keys():
+ return
+
try:
+
+ application_name = delta.data['application']
+
+ callback = self.applications[application_name]['callback']
+ callback_args = self.applications[application_name]['callback_args']
+
if old and new:
old_status = old.workload_status
new_status = new.workload_status
if old_status == new_status:
-
"""The workload status may fluctuate around certain events,
so wait until the status has stabilized before triggering
the callback."""
- if self.callback:
- self.callback(
+ if callback:
+ callback(
self.ns_name,
- self.application_name,
+ delta.data['application'],
new_status,
- *self.callback_args)
+ new.workload_status_message,
+ *callback_args)
+
+ if old and not new:
+ # This is a charm being removed
+ if callback:
+ callback(
+ self.ns_name,
+ delta.data['application'],
+ "removed",
+ "",
+ *callback_args)
except Exception as e:
self.log.debug("[1] notify_callback exception {}".format(e))
elif delta.entity == "action":
# Create unique models per network service
# Document all public functions
+
class N2VC:
# Juju API
"""Close any open connections."""
yield self.logout()
- def notify_callback(self, model_name, application_name, status, callback=None, *callback_args):
+ def notify_callback(self, model_name, application_name, status, message, callback=None, *callback_args):
try:
if callback:
- callback(model_name, application_name, status, *callback_args)
+ callback(model_name, application_name, status, message, *callback_args)
except Exception as e:
self.log.error("[0] notify_callback exception {}".format(e))
raise e
Deploy the charm(s) referenced in a VNF Descriptor.
- You can pass either the nsd record or the id of the network
- service, but this method will fail without one of them.
-
- :param str ns_name: The name of the network service
+ :param str model_name: The name of the network service.
:param str application_name: The name of the application
:param dict vnfd: The name of the application
:param str charm_path: The path to the Juju charm
########################################
app = await self.get_application(model, application_name)
if app:
- raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model))
+ raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
- ############################################################
- # Create a monitor to watch for application status changes #
- ############################################################
+ ################################################################
+ # Register this application with the model-level event monitor #
+ ################################################################
if callback:
- self.log.debug("Setting monitor<->callback")
- self.monitors[application_name] = VCAMonitor(model_name, application_name, callback, *callback_args)
- model.add_observer(self.monitors[application_name])
+ self.monitors[model_name].AddApplication(
+ application_name,
+ callback,
+ *callback_args
+ )
########################################################
# Check for specific machine placement (native charms) #
# This is applied when the Application is deployed
pass
else:
- # TODO: We need to sort by seq, and queue the actions in order.
-
seq = primitive['seq']
+ params = {}
+ if 'parameter' in primitive:
+ params = primitive['parameter']
+
primitives[seq] = {
'name': primitive['name'],
'parameters': self._map_primitive_parameters(
- primitive['parameter'],
+ params,
{'<rw_mgmt_ip>': rw_mgmt_ip}
),
}
**primitives[primitive]['parameters'],
)
except N2VCPrimitiveExecutionFailed as e:
- self.debug.log(
+ self.log.debug(
"[N2VC] Exception executing primitive: {}".format(e)
)
raise
async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
+ """Execute a primitive of a charm for Day 1 or Day 2 configuration.
+
+ Execute a primitive defined in the VNF descriptor.
+
+ :param str model_name: The name of the network service.
+ :param str application_name: The name of the application
+ :param str primitive: The name of the primitive to execute.
+ :param obj callback: A callback function to receive status changes.
+ :param tuple callback_args: A list of arguments to be passed to the callback function.
+ :param dict params: A dictionary of key=value pairs representing the primitive's parameters
+ Examples::
+ {
+ 'rw_mgmt_ip': '1.2.3.4',
+ # Pass the initial-config-primitives section of the vnf or vdu
+ 'initial-config-primitives': {...}
+ }
+ """
+ uuid = None
try:
if not self.authenticated:
await self.login()
# FIXME: This is hard-coded until model-per-ns is added
model_name = 'default'
+ model = await self.controller.get_model(model_name)
+
if primitive == 'config':
# config is special, and expecting params to be a dictionary
- await self.set_config(application_name, params['params'])
+ self.log.debug("Setting charm configuration for {}".format(application_name))
+ self.log.debug(params['params'])
+ await self.set_config(model, application_name, params['params'])
else:
- model = await self.controller.get_model(model_name)
app = await self.get_application(model, application_name)
if app:
# Run against the first (and probably only) unit in the app
if unit:
self.log.debug("Executing primitive {}".format(primitive))
action = await unit.run_action(primitive, **params)
- # action = await action.wait()
+ uuid = action.id
await model.disconnect()
except Exception as e:
self.log.debug("Caught exception while executing primitive: {}".format(e))
- raise e
+ raise N2VCPrimitiveExecutionFailed(e)
+ return uuid
async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
+ """Remove a charm from the VCA.
+
+ Remove a charm referenced in a VNF Descriptor.
+
+ :param str model_name: The name of the network service.
+ :param str application_name: The name of the application
+ :param obj callback: A callback function to receive status changes.
+ :param tuple callback_args: A list of arguments to be passed to the callback function.
+ """
try:
if not self.authenticated:
await self.login()
model = await self.get_model(model_name)
app = await self.get_application(model, application_name)
if app:
- self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
+ # Remove this application from event monitoring
+ self.monitors[model_name].RemoveApplication(application_name)
+
+ # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
+ self.log.debug("Removing the application {}".format(application_name))
await app.remove()
+
+ # Notify the callback that this charm has been removed.
self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
+
except Exception as e:
print("Caught exception: {}".format(e))
self.log.debug(e)
async def DestroyNetworkService(self, nsd):
raise NotImplementedError()
- async def GetMetrics(self, nsd, vnfd):
- """Get the metrics collected by the VCA."""
- raise NotImplementedError()
+ async def GetMetrics(self, model_name, application_name):
+ """Get the metrics collected by the VCA.
+
+ :param model_name The name of the model
+ :param application_name The name of the application
+ """
+ metrics = {}
+ model = await self.get_model(model_name)
+ app = await self.get_application(model, application_name)
+ if app:
+ metrics = await app.get_metrics()
+
+ return metrics
# Non-public methods
async def add_relation(self, a, b, via=None):
finally:
await m.disconnect()
- async def apply_config(self, config, application):
- """Apply a configuration to the application."""
- print("JujuApi: Applying configuration to {}.".format(
- application
- ))
- return await self.set_config(application=application, config=config)
+ # async def apply_config(self, config, application):
+ # """Apply a configuration to the application."""
+ # print("JujuApi: Applying configuration to {}.".format(
+ # application
+ # ))
+ # return await self.set_config(application=application, config=config)
def _get_config_from_dict(self, config_primitive, values):
"""Transform the yang config primitive to dict.
if parameter['value'] == "<rw_mgmt_ip>":
params[param] = str(values[parameter['value']])
else:
- params[param] = str(parameter['value'])
+ """
+ The Juju API uses strictly typed data-types, so we must make
+ sure the parameters from the VNFD match the appropriate type.
+
+ The honus will still be on the operator, to make sure the
+ data-type in the VNFD matches the one in the charm. N2VC will
+ raise N2VCPrimitiveExecutionFailed when there is a mismatch.
+
+ There are three data types supported by the YANG model:
+ # - STRING
+ # - INTEGER
+ # - BOOLEAN
+
+ Each parameter will look like this:
+ {
+ 'seq': '3',
+ 'name': 'testint',
+ 'parameter': [
+ {
+ 'name': 'interval',
+ 'data-type': 'INTEGER',
+ 'value': 20
+ }
+ ]
+ }
+ """
+
+ if 'value' in parameter:
+ # String is the default format
+ val = str(parameter['value'])
+
+ # If the data-type is explicitly set, cast to that type.
+ if 'data-type' in parameter:
+ dt = parameter['data-type'].upper()
+ if dt == "INTEGER":
+ val = int(val)
+
+ elif dt == "BOOLEAN":
+ if val in ['true', 'false', '0', '1']:
+ val = True
+ else:
+ val = False
+
+ params[param] = val
return params
def _get_config_from_yang(self, config_primitive, values):
print("connecting to model {}".format(model_name))
self.models[model_name] = await self.controller.get_model(model_name)
+ # Create an observer for this model
+ self.monitors[model_name] = VCAMonitor(model_name)
+ self.models[model_name].add_observer(self.monitors[model_name])
+
return self.models[model_name]
async def login(self):
return result
- async def set_config(self, application, config):
+ async def set_config(self, model_name, application, config):
"""Apply a configuration to the application."""
if not self.authenticated:
await self.login()
- app = await self.get_application(self.default_model, application)
+ app = await self.get_application(model_name, application)
if app:
self.log.debug("JujuApi: Setting config for Application {}".format(
application,
if config[key] != newconf[key]['value']:
self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
- async def set_parameter(self, parameter, value, application=None):
- """Set a config parameter for a service."""
- if not self.authenticated:
- await self.login()
-
- self.log.debug("JujuApi: Setting {}={} for Application {}".format(
- parameter,
- value,
- application,
- ))
- return await self.apply_config(
- {parameter: value},
- application=application,
- )
+ # async def set_parameter(self, parameter, value, application=None):
+ # """Set a config parameter for a service."""
+ # if not self.authenticated:
+ # await self.login()
+ #
+ # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
+ # parameter,
+ # value,
+ # application,
+ # ))
+ # return await self.apply_config(
+ # {parameter: value},
+ # application=application,
+ # )
async def wait_for_application(self, name, timeout=300):
"""Wait for an application to become active."""