From 5e08a0e8fa4fd9d0156d28f8f4e53e5b176c704a Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Thu, 6 Sep 2018 19:22:47 -0400 Subject: [PATCH] Tox + Integration testing This commit implements a VNF Descriptor-driven integration test framework, which will lead to integration tests being able to run via jenkins, and more robust testing in general. N2VC: - Allow the use of an event loop passed when instantiating N2VC - Refactor the execution of the initial-config-primitive so that it can be easily re-run, such as the case of when a proxy charm is deployed before the VNF's VM is created. - Refactor GetPrimitiveStatus, to return the status (queued, running, complete, failed) of a primitive. - Add GetPrimitiveOutput, to return the output of a completed primitive - Fix model disconnection when executing a primitive (it was happening in the wrong scope) - Fix wait_for_application, which was previously unused and broken - Add support for parameter's 'data-type' field - Add support for better SSH key management, allowing for a proxy charm to be deployed before the VNF, so that it's public SSH key can be injected when the VNF's VM is created. Integration Tests: The integration tests are intended to exercise the expected functionality of a VNF/charm: deploy the charm, configure it as required (i.e., ssh credentials), and execute the VNF's initial-config-primitives. - test_native_charm: deploy a native charm to a juju-managed machine and verify primitive execution works - test_proxy_charm: deploy a proxy charm, configured to talk to a remote machine, and verify primitive execution works - test_metrics_native: deploy a native charm and collect a metric - test_metrics_proxy: deploy a proxy charm and collect a metric from the vnf - test_no_initial-config-primitive: deploy a vnf without an initial-config-primitive - test_non-string_parameter: deploy a vnf with a non-string parameter in initial-config-primitive - test_no_parameter: deploy a vnf with a primitive with no parameters General: - Add a build target to tox.ini so that a .deb is built via Jenkins TODO (in a follow-up commit): - test multi-vdu, multi-charm - test deploying a native charm to a manually-provisioned machine - Update inline pydoc - Add more integration tests - Add global per-test timeout to catch stalled tests Signed-off-by: Adam Israel Change-Id: Id322b45d65c44714e8051fc5764f8c20b76d846c --- Makefile | 11 + n2vc/vnf.py | 409 +++++--- tests/README.md | 51 + tests/base.py | 899 ++++++++++++++++++ .../charms/layers/metrics-ci/deps/layer/basic | 1 - .../layers/metrics-ci/deps/layer/metrics | 1 - .../layers/metrics-ci/deps/layer/options | 1 - tests/charms/layers/metrics-ci/icon.svg | 279 ------ .../charms/layers/metrics-proxy-ci/README.ex | 65 ++ .../layers/metrics-proxy-ci/config.yaml | 14 + .../charms/layers/metrics-proxy-ci/layer.yaml | 4 + .../layers/metrics-proxy-ci/metadata.yaml | 12 + .../layers/metrics-proxy-ci/metrics.yaml | 9 + .../metrics-proxy-ci/reactive/metrics_ci.py | 13 + .../layers/metrics-proxy-ci/tests/00-setup | 5 + .../layers/metrics-proxy-ci/tests/10-deploy | 35 + tests/charms/layers/native-ci/README.md | 3 + tests/charms/layers/native-ci/actions.yaml | 8 + tests/charms/layers/native-ci/actions/test | 33 + tests/charms/layers/native-ci/actions/testint | 33 + tests/charms/layers/native-ci/layer.yaml | 4 + tests/charms/layers/native-ci/metadata.yaml | 6 + .../layers/native-ci/reactive/native-ci.py | 44 + tests/charms/layers/proxy-ci/README.md | 3 + tests/charms/layers/proxy-ci/actions.yaml | 2 + tests/charms/layers/proxy-ci/actions/test | 33 + tests/charms/layers/proxy-ci/layer.yaml | 4 + tests/charms/layers/proxy-ci/metadata.yaml | 12 + .../layers/proxy-ci/reactive/proxy_ci.py | 34 + tests/integration/test_charm_native.py | 141 +++ tests/integration/test_charm_proxy.py | 142 +++ tests/integration/test_metrics.py | 315 ------ tests/integration/test_metrics_native.py | 144 +++ tests/integration/test_metrics_proxy.py | 139 +++ .../test_no_initial_config_primitive.py | 141 +++ tests/integration/test_no_parameter.py | 142 +++ .../integration/test_non_string_parameter.py | 147 +++ tests/test_async_task.py | 16 - tests/test_libjuju.py | 18 + tests/test_lxd.py | 96 ++ tests/test_primitive_no_parameter.py | 274 ------ tests/test_primitive_non-string_parameter.py | 282 ------ tests/test_python.py | 347 ------- tests/test_single_vdu_proxy_charm.py | 351 ------- tests/utils.py | 261 ----- tox.ini | 44 +- 46 files changed, 2743 insertions(+), 2285 deletions(-) create mode 100644 Makefile create mode 100644 tests/README.md create mode 100644 tests/base.py delete mode 160000 tests/charms/layers/metrics-ci/deps/layer/basic delete mode 160000 tests/charms/layers/metrics-ci/deps/layer/metrics delete mode 160000 tests/charms/layers/metrics-ci/deps/layer/options delete mode 100755 tests/charms/layers/metrics-ci/icon.svg create mode 100644 tests/charms/layers/metrics-proxy-ci/README.ex create mode 100644 tests/charms/layers/metrics-proxy-ci/config.yaml create mode 100644 tests/charms/layers/metrics-proxy-ci/layer.yaml create mode 100644 tests/charms/layers/metrics-proxy-ci/metadata.yaml create mode 100644 tests/charms/layers/metrics-proxy-ci/metrics.yaml create mode 100644 tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py create mode 100644 tests/charms/layers/metrics-proxy-ci/tests/00-setup create mode 100644 tests/charms/layers/metrics-proxy-ci/tests/10-deploy create mode 100644 tests/charms/layers/native-ci/README.md create mode 100644 tests/charms/layers/native-ci/actions.yaml create mode 100755 tests/charms/layers/native-ci/actions/test create mode 100755 tests/charms/layers/native-ci/actions/testint create mode 100644 tests/charms/layers/native-ci/layer.yaml create mode 100644 tests/charms/layers/native-ci/metadata.yaml create mode 100644 tests/charms/layers/native-ci/reactive/native-ci.py create mode 100644 tests/charms/layers/proxy-ci/README.md create mode 100644 tests/charms/layers/proxy-ci/actions.yaml create mode 100755 tests/charms/layers/proxy-ci/actions/test create mode 100644 tests/charms/layers/proxy-ci/layer.yaml create mode 100644 tests/charms/layers/proxy-ci/metadata.yaml create mode 100644 tests/charms/layers/proxy-ci/reactive/proxy_ci.py create mode 100644 tests/integration/test_charm_native.py create mode 100644 tests/integration/test_charm_proxy.py delete mode 100644 tests/integration/test_metrics.py create mode 100644 tests/integration/test_metrics_native.py create mode 100644 tests/integration/test_metrics_proxy.py create mode 100644 tests/integration/test_no_initial_config_primitive.py create mode 100644 tests/integration/test_no_parameter.py create mode 100644 tests/integration/test_non_string_parameter.py delete mode 100644 tests/test_async_task.py create mode 100644 tests/test_libjuju.py create mode 100644 tests/test_lxd.py delete mode 100644 tests/test_primitive_no_parameter.py delete mode 100644 tests/test_primitive_non-string_parameter.py delete mode 100755 tests/test_python.py delete mode 100644 tests/test_single_vdu_proxy_charm.py delete mode 100644 tests/utils.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4655d33 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +clean: + find . -name __pycache__ -type d -exec rm -r {} + + find . -name *.pyc -delete + rm -rf .tox + rm -rf tests/charms/tmp* +.tox: + tox -r --notest +test: lint + tox +lint: + tox -e lint diff --git a/n2vc/vnf.py b/n2vc/vnf.py index f642ead..df3ec00 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -1,11 +1,11 @@ - +import asyncio import logging import os import os.path import re import ssl import sys -import time +# import time # FIXME: this should load the juju inside or modules without having to # explicitly install it. Check why it's not working. @@ -16,7 +16,7 @@ if path not in sys.path: sys.path.insert(1, path) from juju.controller import Controller -from juju.model import Model, ModelObserver +from juju.model import ModelObserver # We might need this to connect to the websocket securely, but test and verify. @@ -83,16 +83,17 @@ class VCAMonitor(ModelObserver): application_name = delta.data['application'] callback = self.applications[application_name]['callback'] - callback_args = self.applications[application_name]['callback_args'] + 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.""" + """The workload status may fluctuate around certain + events, so wait until the status has stabilized before + triggering the callback.""" if callback: callback( self.ns_name, @@ -111,7 +112,8 @@ class VCAMonitor(ModelObserver): "", *callback_args) except Exception as e: - self.log.debug("[1] notify_callback exception {}".format(e)) + self.log.debug("[1] notify_callback exception: {}".format(e)) + elif delta.entity == "action": # TODO: Decide how we want to notify the user of actions @@ -138,33 +140,14 @@ class VCAMonitor(ModelObserver): class N2VC: - - # Juju API - api = None - log = None - controller = None - connecting = False - authenticated = False - - models = {} - default_model = None - - # Model Observers - monitors = {} - - # VCA config - hostname = "" - port = 17070 - username = "" - secret = "" - def __init__(self, log=None, server='127.0.0.1', port=17070, user='admin', secret=None, - artifacts=None + artifacts=None, + loop=None, ): """Initialize N2VC @@ -181,9 +164,27 @@ class N2VC: 'port': 17070, 'artifacts': '/path/to/charms' }) - """ + # Initialize instance-level variables + self.api = None + self.log = None + self.controller = None + self.connecting = False + self.authenticated = False + + self.models = {} + self.default_model = None + + # Model Observers + self.monitors = {} + + # VCA config + self.hostname = "" + self.port = 17070 + self.username = "" + self.secret = "" + if log: self.log = log else: @@ -210,14 +211,22 @@ class N2VC: self.artifacts = artifacts + self.loop = loop or asyncio.get_event_loop() + def __del__(self): """Close any open connections.""" yield self.logout() - def notify_callback(self, model_name, application_name, status, message, 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, message, *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 @@ -243,7 +252,9 @@ class N2VC: return self.default_model - async def DeployCharms(self, model_name, application_name, vnfd, charm_path, params={}, machine_spec={}, callback=None, *callback_args): + async def DeployCharms(self, model_name, application_name, vnfd, + charm_path, params={}, machine_spec={}, + callback=None, *callback_args): """Deploy one or more charms associated with a VNF. Deploy the charm(s) referenced in a VNF Descriptor. @@ -259,14 +270,16 @@ class N2VC: # Pass the initial-config-primitives section of the vnf or vdu 'initial-config-primitives': {...} } - :param dict machine_spec: A dictionary describing the machine to install to + :param dict machine_spec: A dictionary describing the machine to + install to Examples:: { 'hostname': '1.2.3.4', 'username': 'ubuntu', } :param obj callback: A callback function to receive status changes. - :param tuple callback_args: A list of arguments to be passed to the callback + :param tuple callback_args: A list of arguments to be passed to the + callback """ ######################################################## @@ -274,7 +287,13 @@ class N2VC: ######################################################## if not os.path.exists(charm_path): self.log.debug("Charm path doesn't exist: {}".format(charm_path)) - self.notify_callback(model_name, application_name, "failed", callback, *callback_args) + self.notify_callback( + model_name, + application_name, + "failed", + callback, + *callback_args, + ) raise JujuCharmNotFound("No artifacts configured.") ################################ @@ -333,6 +352,8 @@ class N2VC: rw_mgmt_ip = params['rw_mgmt_ip'] # initial_config = {} + # self.log.debug(type(params)) + # self.log.debug("Params: {}".format(params)) if 'initial-config-primitive' not in params: params['initial-config-primitive'] = {} @@ -368,13 +389,120 @@ class N2VC: # ####################################### # # Execute initial config primitive(s) # # ####################################### + await self.ExecuteInitialPrimitives( + model_name, + application_name, + params, + ) + + # primitives = {} + # + # # Build a sequential list of the primitives to execute + # for primitive in params['initial-config-primitive']: + # try: + # if primitive['name'] == 'config': + # # This is applied when the Application is deployed + # pass + # else: + # seq = primitive['seq'] + # + # params = {} + # if 'parameter' in primitive: + # params = primitive['parameter'] + # + # primitives[seq] = { + # 'name': primitive['name'], + # 'parameters': self._map_primitive_parameters( + # params, + # {'': rw_mgmt_ip} + # ), + # } + # + # for primitive in sorted(primitives): + # await self.ExecutePrimitive( + # model_name, + # application_name, + # primitives[primitive]['name'], + # callback, + # callback_args, + # **primitives[primitive]['parameters'], + # ) + # except N2VCPrimitiveExecutionFailed as e: + # self.log.debug( + # "[N2VC] Exception executing primitive: {}".format(e) + # ) + # raise + + async def GetPrimitiveStatus(self, model_name, uuid): + """Get the status of an executed Primitive. + + The status of an executed Primitive will be one of three values: + - completed + - failed + - running + """ + status = 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.get_model(model_name) + + results = await model.get_action_status(uuid) + + if uuid in results: + status = results[uuid] + + except Exception as e: + self.log.debug( + "Caught exception while getting primitive status: {}".format(e) + ) + raise N2VCPrimitiveExecutionFailed(e) + + return status + + async def GetPrimitiveOutput(self, model_name, uuid): + """Get the output of an executed Primitive. + + Note: this only returns output for a successfully executed primitive. + """ + results = 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.get_model(model_name) + results = await model.get_action_output(uuid, 60) + except Exception as e: + self.log.debug( + "Caught exception while getting primitive status: {}".format(e) + ) + raise N2VCPrimitiveExecutionFailed(e) + + return results + + async def ExecuteInitialPrimitives(self, model_name, application_name, + params, callback=None, *callback_args): + """Execute multiple primitives. + + Execute multiple primitives as declared in initial-config-primitive. + This is useful in cases where the primitives initially failed -- for + example, if the charm is a proxy but the proxy hasn't been configured + yet. + """ + uuids = [] primitives = {} # Build a sequential list of the primitives to execute for primitive in params['initial-config-primitive']: try: if primitive['name'] == 'config': - # This is applied when the Application is deployed pass else: seq = primitive['seq'] @@ -387,49 +515,30 @@ class N2VC: 'name': primitive['name'], 'parameters': self._map_primitive_parameters( params, - {'': rw_mgmt_ip} + {'': None} ), } for primitive in sorted(primitives): - await self.ExecutePrimitive( - model_name, - application_name, - primitives[primitive]['name'], - callback, - callback_args, - **primitives[primitive]['parameters'], + uuids.append( + await self.ExecutePrimitive( + model_name, + application_name, + primitives[primitive]['name'], + callback, + callback_args, + **primitives[primitive]['parameters'], + ) ) except N2VCPrimitiveExecutionFailed as e: self.log.debug( "[N2VC] Exception executing primitive: {}".format(e) ) raise + return uuids - async def GetPrimitiveStatus(self, model_name, uuid): - results = 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) - - results = await model.get_action_output(uuid) - - await model.disconnect() - except Exception as e: - self.log.debug( - "Caught exception while getting primitive status: {}".format(e) - ) - raise N2VCPrimitiveExecutionFailed(e) - - return results - - - async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params): + 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. @@ -438,8 +547,10 @@ class N2VC: :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 + :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', @@ -447,6 +558,7 @@ class N2VC: 'initial-config-primitives': {...} } """ + self.log.debug("Executing {}".format(primitive)) uuid = None try: if not self.authenticated: @@ -455,7 +567,7 @@ class N2VC: # FIXME: This is hard-coded until model-per-ns is added model_name = 'default' - model = await self.controller.get_model(model_name) + model = await self.get_model(model_name) if primitive == 'config': # config is special, and expecting params to be a dictionary @@ -470,12 +582,8 @@ class N2VC: # Run against the first (and probably only) unit in the app unit = app.units[0] if unit: - self.log.debug( - "Executing primitive {}".format(primitive) - ) action = await unit.run_action(primitive, **params) uuid = action.id - await model.disconnect() except Exception as e: self.log.debug( "Caught exception while executing primitive: {}".format(e) @@ -483,7 +591,8 @@ class N2VC: raise N2VCPrimitiveExecutionFailed(e) return uuid - async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args): + 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. @@ -491,7 +600,8 @@ class N2VC: :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. + :param tuple callback_args: A list of arguments to be passed to the + callback function. """ try: if not self.authenticated: @@ -504,11 +614,19 @@ class N2VC: 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)) + 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) + self.notify_callback( + model_name, + application_name, + "removed", + callback, + *callback_args, + ) except Exception as e: print("Caught exception: {}".format(e)) @@ -584,53 +702,23 @@ class N2VC: params = {} for parameter in parameters: param = str(parameter['name']) + + # Typecast parameter value, if present + if 'data-type' in parameter: + paramtype = str(parameter['data-type']).lower() + value = None + + if paramtype == "integer": + value = int(parameter['value']) + elif paramtype == "boolean": + value = bool(parameter['value']) + else: + value = str(parameter['value']) + if parameter['value'] == "": params[param] = str(values[parameter['value']]) else: - """ - 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 + params[param] = value return params def _get_config_from_yang(self, config_primitive, values): @@ -647,6 +735,7 @@ class N2VC: return config + @staticmethod def FormatApplicationName(self, *args): """ Generate a Juju-compatible Application name @@ -672,7 +761,6 @@ class N2VC: appname += c return re.sub('\-+', '-', appname.lower()) - # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0): # """Format the name of the application # @@ -720,8 +808,9 @@ class N2VC: await self.login() if model_name not in self.models: - print("connecting to model {}".format(model_name)) - self.models[model_name] = await self.controller.get_model(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) @@ -740,10 +829,17 @@ class N2VC: self.log.debug("JujuApi: Logging into controller") cacert = None - self.controller = Controller() + self.controller = Controller(loop=self.loop) if self.secret: - self.log.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self.endpoint, self.port, self.user, self.secret)) + self.log.debug( + "Connecting to controller... ws://{}:{} as {}/{}".format( + self.endpoint, + self.port, + self.user, + self.secret, + ) + ) await self.controller.connect( endpoint=self.endpoint, username=self.user, @@ -773,24 +869,30 @@ class N2VC: try: if self.default_model: - self.log.debug("Disconnecting model {}".format(self.default_model)) + self.log.debug("Disconnecting model {}".format( + self.default_model + )) await self.default_model.disconnect() self.default_model = None for model in self.models: await self.models[model].disconnect() + model = None if self.controller: - self.log.debug("Disconnecting controller {}".format(self.controller)) + self.log.debug("Disconnecting controller {}".format( + self.controller + )) await self.controller.disconnect() - # self.controller = None + self.controller = None self.authenticated = False except Exception as e: - self.log.fail("Fatal error logging out of Juju Controller: {}".format(e)) + self.log.fatal( + "Fatal error logging out of Juju Controller: {}".format(e) + ) raise e - # async def remove_application(self, name): # """Remove the application.""" # if not self.authenticated: @@ -827,9 +929,11 @@ class N2VC: app = await self.get_application(self.default_model, application) if app: - self.log.debug("JujuApi: Resolving errors for application {}".format( - application, - )) + self.log.debug( + "JujuApi: Resolving errors for application {}".format( + application, + ) + ) for unit in app.units: app.resolved(retry=True) @@ -851,10 +955,12 @@ class N2VC: # so use the first unit available. unit = app.units[0] - self.log.debug("JujuApi: Running Action {} against Application {}".format( - action_name, - application, - )) + self.log.debug( + "JujuApi: Running Action {} against Application {}".format( + action_name, + application, + ) + ) action = await unit.run_action(action_name, **params) @@ -900,25 +1006,32 @@ class N2VC: # application=application, # ) - async def wait_for_application(self, name, timeout=300): + async def wait_for_application(self, model_name, application_name, + timeout=300): """Wait for an application to become active.""" if not self.authenticated: await self.login() - app = await self.get_application(self.default_model, name) + # TODO: In a point release, we will use a model per deployed network + # service. In the meantime, we will always use the 'default' model. + model_name = 'default' + model = await self.get_model(model_name) + + app = await self.get_application(model, application_name) + self.log.debug("Application: {}".format(app)) + # app = await self.get_application(model_name, application_name) if app: self.log.debug( "JujuApi: Waiting {} seconds for Application {}".format( timeout, - name, + application_name, ) ) - await self.default_model.block_until( + await model.block_until( lambda: all( - unit.agent_status == 'idle' - and unit.workload_status - in ['active', 'unknown'] for unit in app.units + unit.agent_status == 'idle' and unit.workload_status in + ['active', 'unknown'] for unit in app.units ), timeout=timeout ) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..56380a4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,51 @@ +# N2VC Testing + + +# Preparation +## Environment variables + +The test currently requires some environment variables set in order to run, but these will be deprecated as soon as possible. + +## LXD + +LXD should be installed via snap. + +The connection to the LXD API server expects to use a self-signed SSL certificate, generated by lxc (`lxc list`, et al) is first one. + +## Juju + +Juju is expected to be installed via snap and bootstrapped. + +Run `juju status -m controller` and capture the IP address of machine 0. This is the Juju controller, specified in VCA_HOST + +export VCA_HOST=1.2.3.4 +export VCA_USER=admin +export VCA_SECRET=admin + + +# Running tests + +Tests are written with pytest, driven by tox. All tests are run from the root directory of the repository. + +## Run one test + +To run a single integration test, we tell tox which environment we need, and then the path to the test. + +```bash +tox -e integration -- tests/integration/test_non-string_parameter.py +``` + +## Running all tests + +`make test` will invoke tox to run all unit tests. Alternatively, you can limit this to a specific type of test by invoking tox manually: +```bash +tox -e integration -- tests/integration/ +``` + +# TODO +- Update CI environment to have Juju and LXD available via snap +- Investigate running via Docker +- Remove the requirement for setting environment variables +- Integrate into Jenkins so that tests run against every commit +- Add global timeout to abort tests that are hung +- Only build a charm once per test run, i.e., if two or more tests use the same charm, we should only call `charm build` once. diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..a02ab7e --- /dev/null +++ b/tests/base.py @@ -0,0 +1,899 @@ +#!/usr/bin/env python3 +import asyncio +import functools + +import logging +import n2vc.vnf +import pylxd +import pytest +import os +import shlex +import shutil +import subprocess +import tempfile +import time +import uuid +import yaml + +from juju.controller import Controller + +# Disable InsecureRequestWarning w/LXD +import urllib3 +urllib3.disable_warnings() +logging.getLogger("urllib3").setLevel(logging.WARNING) + +here = os.path.dirname(os.path.realpath(__file__)) + + +def is_bootstrapped(): + result = subprocess.run(['juju', 'switch'], stdout=subprocess.PIPE) + return ( + result.returncode == 0 and + len(result.stdout.decode().strip()) > 0) + + +bootstrapped = pytest.mark.skipif( + not is_bootstrapped(), + reason='bootstrapped Juju environment required') + + +class CleanController(): + """ + Context manager that automatically connects and disconnects from + the currently active controller. + + Note: Unlike CleanModel, this will not create a new controller for you, + and an active controller must already be available. + """ + def __init__(self): + self._controller = None + + async def __aenter__(self): + self._controller = Controller() + await self._controller.connect() + return self._controller + + async def __aexit__(self, exc_type, exc, tb): + await self._controller.disconnect() + + +def get_charm_path(): + return "{}/charms".format(here) + + +def get_layer_path(): + return "{}/charms/layers".format(here) + + +def parse_metrics(application, results): + """Parse the returned metrics into a dict.""" + + # We'll receive the results for all units, to look for the one we want + # Caveat: we're grabbing results from the first unit of the application, + # which is enough for testing, since we're only deploying a single unit. + retval = {} + for unit in results: + if unit.startswith(application): + for result in results[unit]: + retval[result['key']] = result['value'] + return retval + + +def collect_metrics(application): + """Invoke Juju's metrics collector. + + Caveat: this shells out to the `juju collect-metrics` command, rather than + making an API call. At the time of writing, that API is not exposed through + the client library. + """ + + try: + subprocess.check_call(['juju', 'collect-metrics', application]) + except subprocess.CalledProcessError as e: + raise Exception("Unable to collect metrics: {}".format(e)) + + +def has_metrics(charm): + """Check if a charm has metrics defined.""" + metricsyaml = "{}/{}/metrics.yaml".format( + get_layer_path(), + charm, + ) + if os.path.exists(metricsyaml): + return True + return False + + +def get_descriptor(descriptor): + desc = None + try: + tmp = yaml.load(descriptor) + + # Remove the envelope + root = list(tmp.keys())[0] + if root == "nsd:nsd-catalog": + desc = tmp['nsd:nsd-catalog']['nsd'][0] + elif root == "vnfd:vnfd-catalog": + desc = tmp['vnfd:vnfd-catalog']['vnfd'][0] + except ValueError: + assert False + return desc + + +def get_n2vc(loop=None): + """Return an instance of N2VC.VNF.""" + log = logging.getLogger() + log.level = logging.DEBUG + + # Running under tox/pytest makes getting env variables harder. + + # Extract parameters from the environment in order to run our test + vca_host = os.getenv('VCA_HOST', '127.0.0.1') + vca_port = os.getenv('VCA_PORT', 17070) + vca_user = os.getenv('VCA_USER', 'admin') + vca_charms = os.getenv('VCA_CHARMS', None) + vca_secret = os.getenv('VCA_SECRET', None) + + client = n2vc.vnf.N2VC( + log=log, + server=vca_host, + port=vca_port, + user=vca_user, + secret=vca_secret, + artifacts=vca_charms, + loop=loop + ) + return client + + +def create_lxd_container(public_key=None, name="test_name"): + """ + Returns a container object + + If public_key isn't set, we'll use the Juju ssh key + + :param public_key: The public key to inject into the container + :param name: The name of the test being run + """ + container = None + + # Format name so it's valid + name = name.replace("_", "-").replace(".", "") + + client = get_lxd_client() + test_machine = "test-{}-{}".format( + uuid.uuid4().hex[-4:], + name, + ) + + private_key_path, public_key_path = find_juju_ssh_keys() + + # create profile w/cloud-init and juju ssh key + if not public_key: + public_key = "" + with open(public_key_path, "r") as f: + public_key = f.readline() + + client.profiles.create( + test_machine, + config={ + 'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key)}, + devices={ + 'root': {'path': '/', 'pool': 'default', 'type': 'disk'}, + 'eth0': { + 'nictype': 'bridged', + 'parent': 'lxdbr0', + 'type': 'nic' + } + } + ) + + # create lxc machine + config = { + 'name': test_machine, + 'source': { + 'type': 'image', + 'alias': 'xenial', + 'mode': 'pull', + 'protocol': 'simplestreams', + 'server': 'https://cloud-images.ubuntu.com/releases', + }, + 'profiles': [test_machine], + } + container = client.containers.create(config, wait=True) + container.start(wait=True) + + def wait_for_network(container, timeout=30): + """Wait for eth0 to have an ipv4 address.""" + starttime = time.time() + while(time.time() < starttime + timeout): + time.sleep(1) + if 'eth0' in container.state().network: + addresses = container.state().network['eth0']['addresses'] + if len(addresses) > 0: + if addresses[0]['family'] == 'inet': + return addresses[0] + return None + + wait_for_network(container) + + # HACK: We need to give sshd a chance to bind to the interface, + # and pylxd's container.execute seems to be broken and fails and/or + # hangs trying to properly check if the service is up. + time.sleep(5) + client = None + + return container + + +def destroy_lxd_container(container): + """Stop and delete a LXD container.""" + name = container.name + client = get_lxd_client() + + def wait_for_stop(timeout=30): + """Wait for eth0 to have an ipv4 address.""" + starttime = time.time() + while(time.time() < starttime + timeout): + time.sleep(1) + if container.state == "Stopped": + return + + def wait_for_delete(timeout=30): + starttime = time.time() + while(time.time() < starttime + timeout): + time.sleep(1) + if client.containers.exists(name) is False: + return + + container.stop(wait=False) + wait_for_stop() + + container.delete(wait=False) + wait_for_delete() + + # Delete the profile created for this container + profile = client.profiles.get(name) + if profile: + profile.delete() + + +def find_lxd_config(): + """Find the LXD configuration directory.""" + paths = [] + paths.append(os.path.expanduser("~/.config/lxc")) + paths.append(os.path.expanduser("~/snap/lxd/current/.config/lxc")) + + for path in paths: + if os.path.exists(path): + crt = os.path.expanduser("{}/client.crt".format(path)) + key = os.path.expanduser("{}/client.key".format(path)) + if os.path.exists(crt) and os.path.exists(key): + return (crt, key) + return (None, None) + + +def find_juju_ssh_keys(): + """Find the Juju ssh keys.""" + + paths = [] + paths.append(os.path.expanduser("~/.local/share/juju/ssh/")) + + for path in paths: + if os.path.exists(path): + private = os.path.expanduser("{}/juju_id_rsa".format(path)) + public = os.path.expanduser("{}/juju_id_rsa.pub".format(path)) + if os.path.exists(private) and os.path.exists(public): + return (private, public) + return (None, None) + + +def get_juju_private_key(): + keys = find_juju_ssh_keys() + return keys[0] + + +def get_lxd_client(host="127.0.0.1", port="8443", verify=False): + """ Get the LXD client.""" + client = None + (crt, key) = find_lxd_config() + + if crt and key: + client = pylxd.Client( + endpoint="https://{}:{}".format(host, port), + cert=(crt, key), + verify=verify, + ) + + return client + +# TODO: This is marked serial but can be run in parallel with work, including: +# - Fixing an event loop issue; seems that all tests stop when one test stops? + + +@pytest.mark.serial +class TestN2VC(object): + """TODO: + 1. Validator Validation + + Automatically validate the descriptors we're using here, unless the test author explicitly wants to skip them. Useful to make sure tests aren't being run against invalid descriptors, validating functionality that may fail against a properly written descriptor. + + We need to have a flag (instance variable) that controls this behavior. It may be necessary to skip validation and run against a descriptor implementing features that have not yet been released in the Information Model. + """ + + @classmethod + def setup_class(self): + """ setup any state specific to the execution of the given class (which + usually contains tests). + """ + # Initialize instance variable(s) + self.container = None + + # Parse the test's descriptors + self.nsd = get_descriptor(self.NSD_YAML) + self.vnfd = get_descriptor(self.VNFD_YAML) + + self.ns_name = self.nsd['name'] + self.vnf_name = self.vnfd['name'] + + self.charms = {} + self.parse_vnf_descriptor() + assert self.charms is not {} + + # Track artifacts, like compiled charms, that will need to be removed + self.artifacts = {} + + # Build the charm(s) needed for this test + for charm in self.get_charm_names(): + self.get_charm(charm) + + # A bit of a hack, in order to allow the N2VC callback to run parallel + # to pytest. Test(s) should wait for this flag to change to False + # before returning. + self._running = True + + @classmethod + def teardown_class(self): + """ teardown any state that was previously setup with a call to + setup_class. + """ + if self.container: + destroy_lxd_container(self.container) + + # Clean up any artifacts created during the test + logging.debug("Artifacts: {}".format(self.artifacts)) + for charm in self.artifacts: + artifact = self.artifacts[charm] + if os.path.exists(artifact['tmpdir']): + logging.debug("Removing directory '{}'".format(artifact)) + shutil.rmtree(artifact['tmpdir']) + + # Logout of N2VC + asyncio.ensure_future(self.n2vc.logout()) + + @classmethod + def running(self, timeout=600): + """Returns if the test is still running. + + @param timeout The time, in seconds, to wait for the test to complete. + """ + + # if start + now > start > timeout: + # self.stop_test() + return self._running + + @classmethod + def get_charm(self, charm): + """Build and return the path to the test charm. + + Builds one of the charms in tests/charms/layers and returns the path + to the compiled charm. The charm will automatically be removed when + when the test is complete. + + Returns: The path to the built charm or None if `charm build` failed. + """ + + # Make sure the charm snap is installed + try: + subprocess.check_call(['which', 'charm']) + except subprocess.CalledProcessError as e: + raise Exception("charm snap not installed.") + + if charm not in self.artifacts: + try: + # Note: This builds the charm under N2VC/tests/charms/ + # The snap-installed command only has write access to the users $HOME + # so writing to /tmp isn't possible at the moment. + builds = tempfile.mkdtemp(dir=get_charm_path()) + + cmd = "charm build {}/{} -o {}/".format( + get_layer_path(), + charm, + builds, + ) + logging.debug(cmd) + + subprocess.check_call(shlex.split(cmd)) + + self.artifacts[charm] = { + 'tmpdir': builds, + 'charm': "{}/builds/{}".format(builds, charm), + } + except subprocess.CalledProcessError as e: + raise Exception("charm build failed: {}.".format(e)) + + return self.artifacts[charm]['charm'] + + @classmethod + async def deploy(self, vnf_index, charm, params, loop): + """An inner function to do the deployment of a charm from + either a vdu or vnf. + """ + + self.n2vc = get_n2vc(loop=loop) + + vnf_name = self.n2vc.FormatApplicationName( + self.ns_name, + self.vnf_name, + str(vnf_index), + ) + logging.debug("Deploying charm at {}".format(self.artifacts[charm])) + + await self.n2vc.DeployCharms( + self.ns_name, + vnf_name, + self.vnfd, + self.get_charm(charm), + params, + {}, + self.n2vc_callback + ) + + @classmethod + def parse_vnf_descriptor(self): + """Parse the VNF descriptor to make running tests easier. + + Parse the charm information in the descriptor to make it easy to write + tests to run again it. + + Each charm becomes a dictionary in a list: + [ + 'is-proxy': True, + 'vnf-member-index': 1, + 'vnf-name': '', + 'charm-name': '', + + 'initial-config-primitive': {}, + 'config-primitive': {} + ] + - charm name + - is this a proxy charm? + - what are the initial-config-primitives (day 1)? + - what are the config primitives (day 2)? + + """ + charms = {} + + # You'd think this would be explicit, but it's just an incremental + # value that should be consistent. + vnf_member_index = 0 + + """Get all vdu and/or vdu config in a descriptor.""" + config = self.get_config() + for cfg in config: + if 'juju' in cfg: + + # Get the name to be used for the deployed application + application_name = n2vc.vnf.N2VC().FormatApplicationName( + self.ns_name, + self.vnf_name, + str(vnf_member_index), + ) + + charm = { + 'application-name': application_name, + 'proxy': True, + 'vnf-member-index': vnf_member_index, + 'vnf-name': self.vnf_name, + 'name': None, + 'initial-config-primitive': {}, + 'config-primitive': {}, + } + + juju = cfg['juju'] + charm['name'] = juju['charm'] + + if 'proxy' in juju: + charm['proxy'] = juju['proxy'] + + if 'initial-config-primitive' in cfg: + charm['initial-config-primitive'] = \ + cfg['initial-config-primitive'] + + if 'config-primitive' in cfg: + charm['config-primitive'] = cfg['config-primitive'] + + charms[application_name] = charm + + # Increment the vnf-member-index + vnf_member_index += 1 + + self.charms = charms + + @classmethod + def isproxy(self, application_name): + + assert application_name in self.charms + assert 'proxy' in self.charms[application_name] + assert type(self.charms[application_name]['proxy']) is bool + + # logging.debug(self.charms[application_name]) + return self.charms[application_name]['proxy'] + + @classmethod + def get_config(self): + """Return an iterable list of config items (vdu and vnf). + + As far as N2VC is concerned, the config section for vdu and vnf are + identical. This joins them together so tests only need to iterate + through one list. + """ + configs = [] + + """Get all vdu and/or vdu config in a descriptor.""" + vnf_config = self.vnfd.get("vnf-configuration") + if vnf_config: + juju = vnf_config['juju'] + if juju: + configs.append(vnf_config) + + for vdu in self.vnfd['vdu']: + vdu_config = vdu.get('vdu-configuration') + if vdu_config: + juju = vdu_config['juju'] + if juju: + configs.append(vdu_config) + + return configs + + @classmethod + def get_charm_names(self): + """Return a list of charms used by the test descriptor.""" + + charms = {} + + # Check if the VDUs in this VNF have a charm + for config in self.get_config(): + juju = config['juju'] + + name = juju['charm'] + if name not in charms: + charms[name] = 1 + + return charms.keys() + + @classmethod + async def CreateContainer(self, *args): + """Create a LXD container for use with a proxy charm.abs + + 1. Get the public key from the charm via `get-ssh-public-key` action + 2. Create container with said key injected for the ubuntu user + """ + if self.container is None: + # logging.debug("CreateContainer called.") + + # HACK: Set this so the n2vc_callback knows + # there's a container being created + self.container = True + + # Create and configure a LXD container for use with a proxy charm. + (model_name, application_name, _, _) = args + + # Execute 'get-ssh-public-key' primitive and get returned value + uuid = await self.n2vc.ExecutePrimitive( + model_name, + application_name, + "get-ssh-public-key", + None, + ) + # logging.debug("Action UUID: {}".format(uuid)) + result = await self.n2vc.GetPrimitiveOutput(model_name, uuid) + # logging.debug("Action result: {}".format(result)) + pubkey = result['pubkey'] + + self.container = create_lxd_container( + public_key=pubkey, + name=os.path.basename(__file__) + ) + + return self.container + + @classmethod + def get_container_ip(self): + """Return the IPv4 address of container's eth0 interface.""" + ipaddr = None + if self.container: + addresses = self.container.state().network['eth0']['addresses'] + # The interface may have more than one address, but we only need + # the first one for testing purposes. + ipaddr = addresses[0]['address'] + + return ipaddr + + @classmethod + def n2vc_callback(self, *args, **kwargs): + """Monitor and react to changes in the charm state. + + This is where we will monitor the state of the charm: + - is it active? + - is it in error? + - is it waiting on input to continue? + + When the state changes, we respond appropriately: + - configuring ssh credentials for a proxy charm + - running a service primitive + + Lastly, when the test has finished we begin the teardown, removing the + charm and associated LXD container, and notify pytest that this test + is over. + + Args are expected to contain four values, received from N2VC: + - str, the name of the model + - str, the name of the application + - str, the workload status as reported by Juju + - str, the workload message as reported by Juju + """ + (model, application, status, message) = args + # logging.debug("Callback for {}/{} - {} ({})".format( + # model, + # application, + # status, + # message + # )) + + # Make sure we're only receiving valid status. This will catch charms + # that aren't setting their workload state and appear as "unknown" + # assert status not in ["active", "blocked", "waiting", "maintenance"] + + task = None + if kwargs and 'task' in kwargs: + task = kwargs['task'] + # logging.debug("Got task: {}".format(task)) + + # Closures and inner functions, oh my. + def is_active(): + """Is the charm in an active state?""" + if status in ["active"]: + return True + return False + + def is_blocked(): + """Is the charm waiting for us?""" + if status in ["blocked"]: + return True + return False + + def configure_ssh_proxy(task): + """Configure the proxy charm to use the lxd container.""" + logging.debug("configure_ssh_proxy({})".format(task)) + + mgmtaddr = self.get_container_ip() + + logging.debug( + "Setting config ssh-hostname={}".format(mgmtaddr) + ) + + task = asyncio.ensure_future( + self.n2vc.ExecutePrimitive( + model, + application, + "config", + None, + params={ + 'ssh-hostname': mgmtaddr, + 'ssh-username': 'ubuntu', + } + ) + ) + + # Execute the VNFD's 'initial-config-primitive' + task.add_done_callback(functools.partial( + execute_initial_config_primitives, + )) + + def execute_initial_config_primitives(task=None): + logging.debug("execute_initial_config_primitives({})".format(task)) + + init_config = self.charms[application] + + """ + The initial-config-primitive is run during deploy but may fail + on some steps because proxy charm access isn't configured. + + At this stage, we'll re-run those actions. + """ + + task = asyncio.ensure_future( + self.n2vc.ExecuteInitialPrimitives( + model, + application, + init_config, + ) + ) + + """ + ExecutePrimitives will return a list of uuids. We need to check the + status of each. The test continues if all Actions succeed, and + fails if any of them fail. + """ + task.add_done_callback(functools.partial(wait_for_uuids)) + + def check_metrics(): + task = asyncio.ensure_future( + self.n2vc.GetMetrics( + model, + application, + ) + ) + + task.add_done_callback( + functools.partial( + verify_metrics, + ) + ) + + def verify_metrics(task): + logging.debug("Verifying metrics!") + # Check if task returned metrics + results = task.result() + + metrics = parse_metrics(application, results) + logging.debug(metrics) + + if len(metrics): + task = asyncio.ensure_future( + self.n2vc.RemoveCharms(model, application) + ) + + task.add_done_callback(functools.partial(stop_test)) + + else: + # TODO: Ran into a case where it took 9 attempts before metrics + # were available; the controller is slow sometimes. + time.sleep(60) + check_metrics() + + def wait_for_uuids(task): + logging.debug("wait_for_uuids({})".format(task)) + uuids = task.result() + + waitfor = len(uuids) + finished = 0 + + def get_primitive_result(uuid, task): + logging.debug("Got result from action") + # completed, failed, or running + result = task.result() + + if status in result and result['status'] \ + in ["completed", "failed"]: + + # It's over + logging.debug("Action {} is {}".format( + uuid, + task.result['status']) + ) + pass + else: + logging.debug("action is still running") + + def get_primitive_status(uuid, task): + result = task.result() + + if result == "completed": + # Make sure all primitives are finished + global finished + finished += 1 + + if waitfor == finished: + # logging.debug("Action complete; removing charm") + task = asyncio.ensure_future( + self.n2vc.RemoveCharms(model, application) + ) + + task.add_done_callback(functools.partial(stop_test)) + elif result == "failed": + # logging.debug("Action failed; removing charm") + assert False + self._running = False + return + else: + # logging.debug("action is still running: {}".format(result)) + # logging.debug(result) + # pass + # The primitive is running; try again. + task = asyncio.ensure_future( + self.n2vc.GetPrimitiveStatus(model, uuid) + ) + task.add_done_callback(functools.partial( + get_primitive_result, + uuid, + )) + + for actionid in uuids: + task = asyncio.ensure_future( + self.n2vc.GetPrimitiveStatus(model, actionid) + ) + task.add_done_callback(functools.partial( + get_primitive_result, + actionid, + )) + + def stop_test(task): + """Stop the test. + + When the test has either succeeded or reached a failing state, + begin the process of removing the test fixtures. + """ + asyncio.ensure_future( + self.n2vc.RemoveCharms(model, application) + ) + + self._running = False + + if is_blocked(): + # logging.debug("Charm is in a blocked state!") + + # Container only applies to proxy charms. + if self.isproxy(application): + + if self.container is None: + # logging.debug( + # "Ensuring CreateContainer: status is {}".format(status) + # ) + + # Create the new LXD container + task = asyncio.ensure_future(self.CreateContainer(*args)) + + # Configure the proxy charm to use the container when ready + task.add_done_callback(functools.partial( + configure_ssh_proxy, + )) + + # task.add_done_callback(functools.partial( + # stop_test, + # )) + # create_lxd_container() + # self.container = True + else: + # A charm may validly be in a blocked state if it's waiting for + # relations or some other kind of manual intervention + # logging.debug("This is not a proxy charm.") + # TODO: needs testing + task = asyncio.ensure_future( + execute_initial_config_primitives() + ) + + task.add_done_callback(functools.partial(stop_test)) + + elif is_active(): + # Does the charm have metrics defined? + if has_metrics(self.charms[application]['name']): + # logging.debug("metrics.yaml defined in the layer!") + + # Force a run of the metric collector, so we don't have + # to wait for it's normal 5 minute interval run. + # NOTE: this shouldn't be done outside of CI + collect_metrics(application) + + # get the current metrics + check_metrics() + else: + # When the charm reaches an active state and hasn't been + # handled (metrics collection, etc)., the test has succeded. + # logging.debug("Charm is active! Removing charm...") + task = asyncio.ensure_future( + self.n2vc.RemoveCharms(model, application) + ) + + task.add_done_callback(functools.partial(stop_test)) diff --git a/tests/charms/layers/metrics-ci/deps/layer/basic b/tests/charms/layers/metrics-ci/deps/layer/basic deleted file mode 160000 index d59d361..0000000 --- a/tests/charms/layers/metrics-ci/deps/layer/basic +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d59d3613006a5afe1b9322aed9d77b5945b44356 diff --git a/tests/charms/layers/metrics-ci/deps/layer/metrics b/tests/charms/layers/metrics-ci/deps/layer/metrics deleted file mode 160000 index 6861ce3..0000000 --- a/tests/charms/layers/metrics-ci/deps/layer/metrics +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6861ce384f0dcf4e3eb1eaddf421143f4f76e64e diff --git a/tests/charms/layers/metrics-ci/deps/layer/options b/tests/charms/layers/metrics-ci/deps/layer/options deleted file mode 160000 index fcdcea4..0000000 --- a/tests/charms/layers/metrics-ci/deps/layer/options +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fcdcea4e5de3e1556c24e6704607862d0ba00a56 diff --git a/tests/charms/layers/metrics-ci/icon.svg b/tests/charms/layers/metrics-ci/icon.svg deleted file mode 100755 index e092eef..0000000 --- a/tests/charms/layers/metrics-ci/icon.svg +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/tests/charms/layers/metrics-proxy-ci/README.ex b/tests/charms/layers/metrics-proxy-ci/README.ex new file mode 100644 index 0000000..b6816b2 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/README.ex @@ -0,0 +1,65 @@ +# Overview + +Describe the intended usage of this charm and anything unique about how this +charm relates to others here. + +This README will be displayed in the Charm Store, it should be either Markdown +or RST. Ideal READMEs include instructions on how to use the charm, expected +usage, and charm features that your audience might be interested in. For an +example of a well written README check out Hadoop: +http://jujucharms.com/charms/precise/hadoop + +Use this as a Markdown reference if you need help with the formatting of this +README: http://askubuntu.com/editing-help + +This charm provides [service][]. Add a description here of what the service +itself actually does. + +Also remember to check the [icon guidelines][] so that your charm looks good +in the Juju GUI. + +# Usage + +Step by step instructions on using the charm: + +juju deploy servicename + +and so on. If you're providing a web service or something that the end user +needs to go to, tell them here, especially if you're deploying a service that +might listen to a non-default port. + +You can then browse to http://ip-address to configure the service. + +## Scale out Usage + +If the charm has any recommendations for running at scale, outline them in +examples here. For example if you have a memcached relation that improves +performance, mention it here. + +## Known Limitations and Issues + +This not only helps users but gives people a place to start if they want to help +you add features to your charm. + +# Configuration + +The configuration options will be listed on the charm store, however If you're +making assumptions or opinionated decisions in the charm (like setting a default +administrator password), you should detail that here so the user knows how to +change it immediately, etc. + +# Contact Information + +Though this will be listed in the charm store itself don't assume a user will +know that, so include that information here: + +## Upstream Project Name + + - Upstream website + - Upstream bug tracker + - Upstream mailing list or contact information + - Feel free to add things if it's useful for users + + +[service]: http://example.com +[icon guidelines]: https://jujucharms.com/docs/stable/authors-charm-icon diff --git a/tests/charms/layers/metrics-proxy-ci/config.yaml b/tests/charms/layers/metrics-proxy-ci/config.yaml new file mode 100644 index 0000000..51f2ce4 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/config.yaml @@ -0,0 +1,14 @@ +options: + string-option: + type: string + default: "Default Value" + description: "A short description of the configuration option" + boolean-option: + type: boolean + default: False + description: "A short description of the configuration option" + int-option: + type: int + default: 9001 + description: "A short description of the configuration option" + diff --git a/tests/charms/layers/metrics-proxy-ci/layer.yaml b/tests/charms/layers/metrics-proxy-ci/layer.yaml new file mode 100644 index 0000000..790dee6 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/layer.yaml @@ -0,0 +1,4 @@ +includes: + - 'layer:basic' + - 'layer:vnfproxy' + - 'layer:sshproxy' diff --git a/tests/charms/layers/metrics-proxy-ci/metadata.yaml b/tests/charms/layers/metrics-proxy-ci/metadata.yaml new file mode 100644 index 0000000..ae42434 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/metadata.yaml @@ -0,0 +1,12 @@ +name: metrics-proxy-ci +summary: +maintainer: Adam Israel +description: | + +tags: + # Replace "misc" with one or more whitelisted tags from this list: + # https://jujucharms.com/docs/stable/authors-charm-metadata + - misc +subordinate: false +series: + - xenial diff --git a/tests/charms/layers/metrics-proxy-ci/metrics.yaml b/tests/charms/layers/metrics-proxy-ci/metrics.yaml new file mode 100644 index 0000000..dae092f --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/metrics.yaml @@ -0,0 +1,9 @@ +metrics: + users: + type: gauge + description: "# of users" + command: who|wc -l + load: + type: gauge + description: "5 minute load average" + command: cat /proc/loadavg |awk '{print $1}' diff --git a/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py b/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py new file mode 100644 index 0000000..51ce49e --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py @@ -0,0 +1,13 @@ +from charmhelpers.core.hookenv import ( + status_set, +) +from charms.reactive import ( + set_flag, + when_not, +) + + +@when_not('metrics-ci.installed') +def install_metrics_ci(): + status_set('blocked', "Waiting for SSH credentials.") + set_flag('metrics-ci.installed') diff --git a/tests/charms/layers/metrics-proxy-ci/tests/00-setup b/tests/charms/layers/metrics-proxy-ci/tests/00-setup new file mode 100644 index 0000000..f0616a5 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/tests/00-setup @@ -0,0 +1,5 @@ +#!/bin/bash + +sudo add-apt-repository ppa:juju/stable -y +sudo apt-get update +sudo apt-get install amulet python-requests -y diff --git a/tests/charms/layers/metrics-proxy-ci/tests/10-deploy b/tests/charms/layers/metrics-proxy-ci/tests/10-deploy new file mode 100644 index 0000000..7595ecf --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/tests/10-deploy @@ -0,0 +1,35 @@ +#!/usr/bin/python3 + +import amulet +import requests +import unittest + + +class TestCharm(unittest.TestCase): + def setUp(self): + self.d = amulet.Deployment() + + self.d.add('metrics-demo') + self.d.expose('metrics-demo') + + self.d.setup(timeout=900) + self.d.sentry.wait() + + self.unit = self.d.sentry['metrics-demo'][0] + + def test_service(self): + # test we can access over http + page = requests.get('http://{}'.format(self.unit.info['public-address'])) + self.assertEqual(page.status_code, 200) + # Now you can use self.d.sentry[SERVICE][UNIT] to address each of the units and perform + # more in-depth steps. Each self.d.sentry[SERVICE][UNIT] has the following methods: + # - .info - An array of the information of that unit from Juju + # - .file(PATH) - Get the details of a file on that unit + # - .file_contents(PATH) - Get plain text output of PATH file from that unit + # - .directory(PATH) - Get details of directory + # - .directory_contents(PATH) - List files and folders in PATH on that unit + # - .relation(relation, service:rel) - Get relation data from return service + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/charms/layers/native-ci/README.md b/tests/charms/layers/native-ci/README.md new file mode 100644 index 0000000..d58b762 --- /dev/null +++ b/tests/charms/layers/native-ci/README.md @@ -0,0 +1,3 @@ +# Overview + +A native charm. diff --git a/tests/charms/layers/native-ci/actions.yaml b/tests/charms/layers/native-ci/actions.yaml new file mode 100644 index 0000000..6adcba7 --- /dev/null +++ b/tests/charms/layers/native-ci/actions.yaml @@ -0,0 +1,8 @@ +test: + description: "Verify that the action can run." +testint: + description: "Test a primitive with a non-string parameter" + params: + intval: + type: integer + default: 0 diff --git a/tests/charms/layers/native-ci/actions/test b/tests/charms/layers/native-ci/actions/test new file mode 100755 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/native-ci/actions/test @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +## +# Copyright 2016 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +import sys +sys.path.append('lib') + +from charms.reactive import main, set_flag +from charmhelpers.core.hookenv import action_fail, action_name + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_flag('actions.{}'.format(action_name())) + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/tests/charms/layers/native-ci/actions/testint b/tests/charms/layers/native-ci/actions/testint new file mode 100755 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/native-ci/actions/testint @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +## +# Copyright 2016 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +import sys +sys.path.append('lib') + +from charms.reactive import main, set_flag +from charmhelpers.core.hookenv import action_fail, action_name + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_flag('actions.{}'.format(action_name())) + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/tests/charms/layers/native-ci/layer.yaml b/tests/charms/layers/native-ci/layer.yaml new file mode 100644 index 0000000..edc8839 --- /dev/null +++ b/tests/charms/layers/native-ci/layer.yaml @@ -0,0 +1,4 @@ +includes: ['layer:basic'] +options: + basic: + use_venv: false diff --git a/tests/charms/layers/native-ci/metadata.yaml b/tests/charms/layers/native-ci/metadata.yaml new file mode 100644 index 0000000..6acf296 --- /dev/null +++ b/tests/charms/layers/native-ci/metadata.yaml @@ -0,0 +1,6 @@ +name: native-ci +summary: A native VNF charm +description: A native VNF charm +maintainer: Adam Israel +subordinate: false +series: ['xenial'] diff --git a/tests/charms/layers/native-ci/reactive/native-ci.py b/tests/charms/layers/native-ci/reactive/native-ci.py new file mode 100644 index 0000000..17bf5f4 --- /dev/null +++ b/tests/charms/layers/native-ci/reactive/native-ci.py @@ -0,0 +1,44 @@ +from charmhelpers.core.hookenv import ( + action_fail, + action_set, + action_get, + status_set, +) +from charms.reactive import ( + clear_flag, + set_flag, + when, + when_not, +) + + +@when_not('native-ci.installed') +def install_native_ci_charm(): + set_flag('native-ci.installed') + status_set('active', 'Ready!') + + +@when('actions.test', 'native-ci.installed') +def test(): + try: + result = True + except Exception as e: + action_fail('command failed: {}'.format(e)) + else: + action_set({'output': result}) + finally: + clear_flag('actions.test') + + +@when('actions.testint', 'native-ci.installed') +def testint(): + try: + # Test the value is an int by performing a mathmatical operation on it. + intval = action_get('intval') + intval = intval + 1 + except Exception as e: + action_fail('command failed: {}'.format(e)) + else: + action_set({'output': intval}) + finally: + clear_flag('actions.testint') diff --git a/tests/charms/layers/proxy-ci/README.md b/tests/charms/layers/proxy-ci/README.md new file mode 100644 index 0000000..c16d9d8 --- /dev/null +++ b/tests/charms/layers/proxy-ci/README.md @@ -0,0 +1,3 @@ +# Overview + +A `charm layer` to test the functionality of proxy charms. diff --git a/tests/charms/layers/proxy-ci/actions.yaml b/tests/charms/layers/proxy-ci/actions.yaml new file mode 100644 index 0000000..5af8591 --- /dev/null +++ b/tests/charms/layers/proxy-ci/actions.yaml @@ -0,0 +1,2 @@ +test: + description: "Verify that the action can run." diff --git a/tests/charms/layers/proxy-ci/actions/test b/tests/charms/layers/proxy-ci/actions/test new file mode 100755 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/proxy-ci/actions/test @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +## +# Copyright 2016 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +import sys +sys.path.append('lib') + +from charms.reactive import main, set_flag +from charmhelpers.core.hookenv import action_fail, action_name + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_flag('actions.{}'.format(action_name())) + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/tests/charms/layers/proxy-ci/layer.yaml b/tests/charms/layers/proxy-ci/layer.yaml new file mode 100644 index 0000000..790dee6 --- /dev/null +++ b/tests/charms/layers/proxy-ci/layer.yaml @@ -0,0 +1,4 @@ +includes: + - 'layer:basic' + - 'layer:vnfproxy' + - 'layer:sshproxy' diff --git a/tests/charms/layers/proxy-ci/metadata.yaml b/tests/charms/layers/proxy-ci/metadata.yaml new file mode 100644 index 0000000..b96abe4 --- /dev/null +++ b/tests/charms/layers/proxy-ci/metadata.yaml @@ -0,0 +1,12 @@ +name: proxy-ci +summary: +maintainer: Adam Israel +description: | + +tags: + # Replace "misc" with one or more whitelisted tags from this list: + # https://jujucharms.com/docs/stable/authors-charm-metadata + - misc +subordinate: false +series: + - xenial diff --git a/tests/charms/layers/proxy-ci/reactive/proxy_ci.py b/tests/charms/layers/proxy-ci/reactive/proxy_ci.py new file mode 100644 index 0000000..30e4eea --- /dev/null +++ b/tests/charms/layers/proxy-ci/reactive/proxy_ci.py @@ -0,0 +1,34 @@ +from charmhelpers.core.hookenv import ( + action_fail, + action_set, + status_set, +) +from charms.reactive import ( + set_flag, + clear_flag, + when_not, + when, +) +import charms.sshproxy + + +@when_not('proxy-ci.installed') +def install_metrics_ci(): + status_set('blocked', "Waiting for SSH credentials.") + set_flag('proxy-ci.installed') + + +@when('actions.test', 'proxy-ci.installed') +def test(): + err = '' + try: + cmd = ['hostname'] + result, err = charms.sshproxy._run(cmd) + if len(result) == 0: + raise Exception("Proxy failed") + except Exception as e: + action_fail('command failed: {}'.format(e)) + else: + action_set({'output': result}) + finally: + clear_flag('actions.test') diff --git a/tests/integration/test_charm_native.py b/tests/integration/test_charm_native.py new file mode 100644 index 0000000..d1b60ff --- /dev/null +++ b/tests/integration/test_charm_native.py @@ -0,0 +1,141 @@ +""" +Deploy a native charm (to LXD) and execute a primitive +""" + +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: charmnative-ns + name: charmnative-ns + short-name: charmnative-ns + description: NS with 1 VNFs charmnative-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: charmnative-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: charmnative-vnf + name: charmnative-vnf + short-name: charmnative-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vdu-configuration: + juju: + charm: native-ci + proxy: false + initial-config-primitive: + - seq: '1' + name: test + """ + + @pytest.mark.asyncio + async def test_charm_native(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + loop=event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_native stopped") + + return 'ok' diff --git a/tests/integration/test_charm_proxy.py b/tests/integration/test_charm_proxy.py new file mode 100644 index 0000000..c1661ac --- /dev/null +++ b/tests/integration/test_charm_proxy.py @@ -0,0 +1,142 @@ +""" +Deploy a VNF with a proxy charm, executing an initial-config-primitive +""" + +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: charmproxy-ns + name: charmproxy-ns + short-name: charmproxy-ns + description: NS with 1 VNF connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: charmproxy-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: charmproxy-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: charmproxy-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: charmproxy-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: charmproxy-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: charmproxy-vnf + name: charmproxy-vnf + short-name: charmproxy-vnf + version: '1.0' + description: A VNF consisting of 1 VDUs w/proxy charm + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vdu-configuration: + juju: + charm: proxy-ci + proxy: true + initial-config-primitive: + - seq: '1' + name: test + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_charm_proxy(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_native stopped") + + return 'ok' diff --git a/tests/integration/test_metrics.py b/tests/integration/test_metrics.py deleted file mode 100644 index 1151d46..0000000 --- a/tests/integration/test_metrics.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Test the collection of charm metrics. - 1. Deploy a charm w/metrics to a unit - 2. Collect metrics or wait for collection to run - 3. Execute n2vc.GetMetrics() - 5. Destroy Juju unit -""" -import asyncio -import functools -import logging -import sys -import time -import unittest -from .. import utils - -NSD_YAML = """ -nsd:nsd-catalog: - nsd: - - id: singlecharmvdu-ns - name: singlecharmvdu-ns - short-name: singlecharmvdu-ns - description: NS with 1 VNFs singlecharmvdu-vnf connected by datanet and mgmtnet VLs - version: '1.0' - logo: osm.png - constituent-vnfd: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index: '1' - vld: - - id: mgmtnet - name: mgmtnet - short-name: mgmtnet - type: ELAN - mgmt-network: 'true' - vim-network-name: mgmt - vnfd-connection-point-ref: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-mgmt - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-mgmt - - id: datanet - name: datanet - short-name: datanet - type: ELAN - vnfd-connection-point-ref: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-data - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-data -""" - -VNFD_YAML = """ -vnfd:vnfd-catalog: - vnfd: - - id: singlecharmvdu-vnf - name: singlecharmvdu-vnf - short-name: singlecharmvdu-vnf - version: '1.0' - description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init - logo: osm.png - connection-point: - - id: vnf-mgmt - name: vnf-mgmt - short-name: vnf-mgmt - type: VPORT - - id: vnf-data - name: vnf-data - short-name: vnf-data - type: VPORT - mgmt-interface: - cp: vnf-mgmt - internal-vld: - - id: internal - name: internal - short-name: internal - type: ELAN - internal-connection-point: - - id-ref: mgmtVM-internal - - id-ref: dataVM-internal - vdu: - - id: mgmtVM - name: mgmtVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: mgmtVM-eth0 - position: '1' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-mgmt - - name: mgmtVM-eth1 - position: '2' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: mgmtVM-internal - internal-connection-point: - - id: mgmtVM-internal - name: mgmtVM-internal - short-name: mgmtVM-internal - type: VPORT - cloud-init-file: cloud-config.txt - vnf-configuration: - juju: - charm: metrics-ci - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' -""" - - -class PythonTest(unittest.TestCase): - n2vc = None - charm = None - - def setUp(self): - self.log = logging.getLogger() - self.log.level = logging.DEBUG - - self.stream_handler = logging.StreamHandler(sys.stdout) - self.log.addHandler(self.stream_handler) - - self.loop = asyncio.get_event_loop() - - self.n2vc = utils.get_n2vc() - - # Parse the descriptor - self.log.debug("Parsing the descriptor") - self.nsd = utils.get_descriptor(NSD_YAML) - self.vnfd = utils.get_descriptor(VNFD_YAML) - - - # Build the charm - - vnf_config = self.vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - charm = juju['charm'] - - self.log.debug("Building charm {}".format(charm)) - self.charm = utils.build_charm(charm) - - def tearDown(self): - self.loop.run_until_complete(self.n2vc.logout()) - self.log.removeHandler(self.stream_handler) - - def n2vc_callback(self, model_name, application_name, workload_status,\ - workload_message, task=None): - """We pass the vnfd when setting up the callback, so expect it to be - returned as a tuple.""" - self.log.debug("status: {}; task: {}".format(workload_status, task)) - - # if workload_status in ["stop_test"]: - # # Stop the test - # self.log.debug("Stopping the test1") - # self.loop.call_soon_threadsafe(self.loop.stop) - # return - - if workload_status: - if workload_status in ["active"] and not task: - # Force a run of the metric collector, so we don't have - # to wait for it's normal 5 minute interval run. - # NOTE: this shouldn't be done outside of CI - utils.collect_metrics(application_name) - - # get the current metrics - task = asyncio.ensure_future( - self.n2vc.GetMetrics( - model_name, - application_name, - ) - ) - task.add_done_callback( - functools.partial( - self.n2vc_callback, - model_name, - application_name, - "collect_metrics", - task, - ) - ) - - elif workload_status in ["collect_metrics"]: - - if task: - # Check if task returned metrics - results = task.result() - - foo = utils.parse_metrics(application_name, results) - if 'load' in foo: - self.log.debug("Removing charm") - task = asyncio.ensure_future( - self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback) - ) - task.add_done_callback( - functools.partial( - self.n2vc_callback, - model_name, - application_name, - "stop_test", - task, - ) - ) - return - - # No metrics are available yet, so try again in a minute. - self.log.debug("Sleeping for 60 seconds") - time.sleep(60) - task = asyncio.ensure_future( - self.n2vc.GetMetrics( - model_name, - application_name, - ) - ) - task.add_done_callback( - functools.partial( - self.n2vc_callback, - model_name, - application_name, - "collect_metrics", - task, - ) - ) - elif workload_status in ["stop_test"]: - # Stop the test - self.log.debug("Stopping the test2") - self.loop.call_soon_threadsafe(self.loop.stop) - - def test_deploy_application(self): - """Deploy proxy charm to a unit.""" - if self.nsd and self.vnfd: - params = {} - vnf_index = 0 - - def deploy(): - """An inner function to do the deployment of a charm from - either a vdu or vnf. - """ - charm_dir = "{}/builds/{}".format(utils.get_charm_path(), charm) - - # Setting this to an IP that will fail the initial config. - # This will be detected in the callback, which will execute - # the "config" primitive with the right IP address. - # mgmtaddr = self.container.state().network['eth0']['addresses'] - # params['rw_mgmt_ip'] = mgmtaddr[0]['address'] - - # Legacy method is to set the ssh-private-key config - # with open(utils.get_juju_private_key(), "r") as f: - # pkey = f.readline() - # params['ssh-private-key'] = pkey - - ns_name = "default" - - vnf_name = self.n2vc.FormatApplicationName( - ns_name, - self.vnfd['name'], - str(vnf_index), - ) - - self.loop.run_until_complete( - self.n2vc.DeployCharms( - ns_name, - vnf_name, - self.vnfd, - charm_dir, - params, - {}, - self.n2vc_callback - ) - ) - - # Check if the VDUs in this VNF have a charm - # for vdu in vnfd['vdu']: - # vdu_config = vdu.get('vdu-configuration') - # if vdu_config: - # juju = vdu_config['juju'] - # self.assertIsNotNone(juju) - # - # charm = juju['charm'] - # self.assertIsNotNone(charm) - # - # params['initial-config-primitive'] = vdu_config['initial-config-primitive'] - # - # deploy() - # vnf_index += 1 - # - # # Check if this VNF has a charm - vnf_config = self.vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - if 'initial-config-primitive' in vnf_config: - params['initial-config-primitive'] = vnf_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - self.loop.run_forever() - # while self.loop.is_running(): - # # await asyncio.sleep(1) - # time.sleep(1) diff --git a/tests/integration/test_metrics_native.py b/tests/integration/test_metrics_native.py new file mode 100644 index 0000000..74faebf --- /dev/null +++ b/tests/integration/test_metrics_native.py @@ -0,0 +1,144 @@ +""" +Deploy a VNF w/native charm that collects metrics +""" +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: metricsnative-ns + name: metricsnative-ns + short-name: metricsnative-ns + description: NS with 1 VNFs metricsnative-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: metricsnative-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: metricsnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: metricsnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: metricsnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: metricsnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: metricsnative-vnf + name: metricsnative-vnf + short-name: metricsnative-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vnf-configuration: + juju: + charm: metrics-ci + proxy: false + config-primitive: + - name: touch + parameter: + - name: filename + data-type: STRING + default-value: '/home/ubuntu/touched' + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_metrics_native(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_metrics_native stopped") + + return 'ok' diff --git a/tests/integration/test_metrics_proxy.py b/tests/integration/test_metrics_proxy.py new file mode 100644 index 0000000..98285fd --- /dev/null +++ b/tests/integration/test_metrics_proxy.py @@ -0,0 +1,139 @@ +""" +Deploy a VNF w/proxy charm that collects metrics +""" +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: metricsproxy-ns + name: metricsproxy-ns + short-name: metricsproxy-ns + description: NS with 1 VNFs metricsproxy-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: metricsproxy-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: metricsproxy-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: metricsproxy-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: metricsproxy-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: metricsproxy-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: metricsproxy-vnf + name: metricsproxy-vnf + short-name: metricsproxy-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vnf-configuration: + juju: + charm: metrics-proxy-ci + proxy: true + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_metrics_proxy(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + + logging.debug("test_metrics_proxy stopped") + + return 'ok' diff --git a/tests/integration/test_no_initial_config_primitive.py b/tests/integration/test_no_initial_config_primitive.py new file mode 100644 index 0000000..e66a695 --- /dev/null +++ b/tests/integration/test_no_initial_config_primitive.py @@ -0,0 +1,141 @@ +""" +Test N2VC when the VNF descriptor does not contain an initial-config-primitive. +""" +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: noinitconfig-ns + name: noinitconfig-ns + short-name: noinitconfig-ns + description: NS with 1 VNFs noinitconfig-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: noinitconfig-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: noinitconfig-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: noinitconfig-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: noinitconfig-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: noinitconfig-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: noinitconfig-vnf + name: noinitconfig-vnf + short-name: noinitconfig-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vdu-configuration: + juju: + charm: native-ci + proxy: false + config-primitive: + - name: test + + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_charm_no_initial_config_primitive(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_native stopped") + + return 'ok' diff --git a/tests/integration/test_no_parameter.py b/tests/integration/test_no_parameter.py new file mode 100644 index 0000000..39c2443 --- /dev/null +++ b/tests/integration/test_no_parameter.py @@ -0,0 +1,142 @@ +""" +Describe what this test is meant to do. +""" +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: noparam-ns + name: noparam-ns + short-name: noparam-ns + description: NS with 1 VNFs noparam-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: noparam-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: noparam-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: noparam-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: noparam-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: noparam-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: noparam-vnf + name: noparam-vnf + short-name: noparam-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vdu-configuration: + juju: + charm: native-ci + proxy: false + initial-config-primitive: + - seq: '1' + name: test + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_charm_no_parameter(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + logging.warning("event_loop: {}".format(event_loop)) + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_native stopped") + await self.n2vc.logout() + + return 'ok' diff --git a/tests/integration/test_non_string_parameter.py b/tests/integration/test_non_string_parameter.py new file mode 100644 index 0000000..ed3dfc7 --- /dev/null +++ b/tests/integration/test_non_string_parameter.py @@ -0,0 +1,147 @@ +""" +Deploy a VNF with a non-string parameter passed to a primitive +""" +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: charmnative-ns + name: charmnative-ns + short-name: charmnative-ns + description: NS with 1 VNFs charmnative-vnf connected by datanet and mgmtnet VLs + version: '1.0' + logo: osm.png + constituent-vnfd: + - vnfd-id-ref: charmnative-vnf + member-vnf-index: '1' + vld: + - id: mgmtnet + name: mgmtnet + short-name: mgmtnet + type: ELAN + mgmt-network: 'true' + vim-network-name: mgmt + vnfd-connection-point-ref: + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-mgmt + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-mgmt + - id: datanet + name: datanet + short-name: datanet + type: ELAN + vnfd-connection-point-ref: + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf-data + - vnfd-id-ref: charmnative-vnf + member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf-data + """ + + VNFD_YAML = """ + vnfd:vnfd-catalog: + vnfd: + - id: charmnative-vnf + name: charmnative-vnf + short-name: charmnative-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init + logo: osm.png + connection-point: + - id: vnf-mgmt + name: vnf-mgmt + short-name: vnf-mgmt + type: VPORT + - id: vnf-data + name: vnf-data + short-name: vnf-data + type: VPORT + mgmt-interface: + cp: vnf-mgmt + internal-vld: + - id: internal + name: internal + short-name: internal + type: ELAN + internal-connection-point: + - id-ref: mgmtVM-internal + - id-ref: dataVM-internal + vdu: + - id: mgmtVM + name: mgmtVM + image: xenial + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: VIRTIO + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: VIRTIO + internal-connection-point-ref: mgmtVM-internal + internal-connection-point: + - id: mgmtVM-internal + name: mgmtVM-internal + short-name: mgmtVM-internal + type: VPORT + cloud-init-file: cloud-config.txt + vdu-configuration: + juju: + charm: native-ci + proxy: false + initial-config-primitive: + - seq: '1' + name: test + - seq: '2' + name: testint + parameter: + - name: intval + data-type: INTEGER + value: 1 + """ + + # @pytest.mark.serial + @pytest.mark.asyncio + async def test_charm_non_string_parameter(self, event_loop): + """Deploy and execute the initial-config-primitive of a VNF.""" + + if self.nsd and self.vnfd: + vnf_index = 0 + + for config in self.get_config(): + juju = config['juju'] + charm = juju['charm'] + + await self.deploy( + vnf_index, + charm, + config, + event_loop, + ) + + while self.running(): + logging.debug("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_native stopped") + + return 'ok' diff --git a/tests/test_async_task.py b/tests/test_async_task.py deleted file mode 100644 index da6e96e..0000000 --- a/tests/test_async_task.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Recreate the conditions in which this will be used in OSM, called via tasks -import asyncio - -if __name__ == "__main__": - main() - -async def do_something(): - pass - -def main(): - - loop = asyncio.get_event_loop() - loop.run_until_complete(do_something()) - loop.close() - loop = None diff --git a/tests/test_libjuju.py b/tests/test_libjuju.py new file mode 100644 index 0000000..8adc202 --- /dev/null +++ b/tests/test_libjuju.py @@ -0,0 +1,18 @@ +# A simple test to verify we're using the right libjuju module +from n2vc.vnf import N2VC # noqa: F401 +import sys + + +def test_libjuju(): + """Test the module import for our vendored version of libjuju. + + Test and verify that the version of libjuju being imported by N2VC is our + vendored version, not one installed externally. + """ + for name in sys.modules: + if name.startswith("juju"): + module = sys.modules[name] + if getattr(module, "__file__"): + assert module.__file__.find("N2VC/modules/libjuju/juju") + + return diff --git a/tests/test_lxd.py b/tests/test_lxd.py new file mode 100644 index 0000000..f68fa3a --- /dev/null +++ b/tests/test_lxd.py @@ -0,0 +1,96 @@ +""" +This test exercises LXD, to make sure that we can: +1. Create a container profile +2. Launch a container with a profile +3. Stop a container +4. Destroy a container +5. Delete a container profile + +""" +import logging +# import os +import pytest +from . import base +import subprocess +import shlex +import tempfile + + +@pytest.mark.asyncio +async def test_lxd(): + + container = base.create_lxd_container(name="test-lxd") + assert container is not None + + # Get the hostname of the container + hostname = container.name + + # Delete the container + base.destroy_lxd_container(container) + + # Verify the container is deleted + client = base.get_lxd_client() + assert client.containers.exists(hostname) is False + + +@pytest.mark.asyncio +async def test_lxd_ssh(): + + with tempfile.TemporaryDirectory() as tmp: + try: + # Create a temporary keypair + cmd = shlex.split( + "ssh-keygen -t rsa -b 4096 -N '' -f {}/id_lxd_rsa".format( + tmp, + ) + ) + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + logging.debug(e) + assert False + + # Slurp the public key + public_key = None + with open("{}/id_lxd_rsa.pub".format(tmp), "r") as f: + public_key = f.read() + + assert public_key is not None + + # Create the container with the keypair injected via profile + container = base.create_lxd_container( + public_key=public_key, + name="test-lxd" + ) + assert container is not None + + # Get the hostname of the container + hostname = container.name + + addresses = container.state().network['eth0']['addresses'] + # The interface may have more than one address, but we only need + # the first one for testing purposes. + ipaddr = addresses[0]['address'] + + # Verify we can SSH into container + try: + cmd = shlex.split( + "ssh -i {}/id_lxd_rsa {} root@{} hostname".format( + tmp, + "-oStrictHostKeyChecking=no", + ipaddr, + ) + ) + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + logging.debug(e) + assert False + + # Delete the container + base.destroy_lxd_container(container) + + # Verify the container is deleted + client = base.get_lxd_client() + assert client.containers.exists(hostname) is False + + # Verify the container profile is deleted + assert client.profiles.exists(hostname) is False diff --git a/tests/test_primitive_no_parameter.py b/tests/test_primitive_no_parameter.py deleted file mode 100644 index 8ac9380..0000000 --- a/tests/test_primitive_no_parameter.py +++ /dev/null @@ -1,274 +0,0 @@ -# A simple test to exercise the libraries' functionality -import asyncio -import functools -import os -import sys -import logging -import unittest -import yaml -from n2vc.vnf import N2VC - -NSD_YAML = """ -nsd:nsd-catalog: - nsd: - - id: multicharmvdu-ns - name: multicharmvdu-ns - short-name: multicharmvdu-ns - description: NS with 1 VNF - version: '1.0' - logo: osm.png - constituent-vnfd: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index: '1' - vld: - - id: datanet - name: datanet - short-name: datanet - type: ELAN - vnfd-connection-point-ref: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-data -""" - -VNFD_YAML = """ -vnfd:vnfd-catalog: - vnfd: - - id: multicharmvdu-vnf - name: multicharmvdu-vnf - short-name: multicharmvdu-vnf - version: '1.0' - description: A VNF consisting of 1 VDUs w/charm - logo: osm.png - connection-point: - - id: vnf-data - name: vnf-data - short-name: vnf-data - type: VPORT - mgmt-interface: - cp: vnf-data - internal-vld: - - id: internal - name: internal - short-name: internal - type: ELAN - internal-connection-point: - - id-ref: dataVM-internal - vdu: - - id: dataVM - name: dataVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: dataVM-eth0 - position: '1' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: dataVM-internal - - name: dataVM-xe0 - position: '2' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-data - internal-connection-point: - - id: dataVM-internal - name: dataVM-internal - short-name: dataVM-internal - type: VPORT - vdu-configuration: - juju: - charm: simple - initial-config-primitive: - - seq: '1' - name: config - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: ubuntu - - seq: '2' - name: touch - parameter: - - name: filename - value: '/home/ubuntu/first-touch-dataVM' - - seq: '3' - name: start - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' -""" - -class PythonTest(unittest.TestCase): - n2vc = None - - def setUp(self): - - self.log = logging.getLogger() - self.log.level = logging.DEBUG - - self.loop = asyncio.get_event_loop() - - # self.loop = asyncio.new_event_loop() - # asyncio.set_event_loop(None) - - # Extract parameters from the environment in order to run our test - vca_host = os.getenv('VCA_HOST', '127.0.0.1') - vca_port = os.getenv('VCA_PORT', 17070) - vca_user = os.getenv('VCA_USER', 'admin') - vca_charms = os.getenv('VCA_CHARMS', None) - vca_secret = os.getenv('VCA_SECRET', None) - self.n2vc = N2VC( - log=self.log, - server=vca_host, - port=vca_port, - user=vca_user, - secret=vca_secret, - artifacts=vca_charms, - ) - - def tearDown(self): - self.loop.run_until_complete(self.n2vc.logout()) - - def get_descriptor(self, descriptor): - desc = None - try: - tmp = yaml.load(descriptor) - - # Remove the envelope - root = list(tmp.keys())[0] - if root == "nsd:nsd-catalog": - desc = tmp['nsd:nsd-catalog']['nsd'][0] - elif root == "vnfd:vnfd-catalog": - desc = tmp['vnfd:vnfd-catalog']['vnfd'][0] - except ValueError: - assert False - return desc - - def n2vc_callback(self, model_name, application_name, workload_status, task=None): - """We pass the vnfd when setting up the callback, so expect it to be - returned as a tuple.""" - if workload_status and not task: - self.log.debug("Callback: workload status \"{}\"".format(workload_status)) - - if workload_status in ["blocked"]: - task = asyncio.ensure_future( - self.n2vc.ExecutePrimitive( - model_name, - application_name, - "config", - None, - params={ - 'ssh-hostname': '10.195.8.78', - 'ssh-username': 'ubuntu', - 'ssh-password': 'ubuntu' - } - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - pass - elif workload_status in ["active"]: - self.log.debug("Removing charm") - task = asyncio.ensure_future( - self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - - def test_deploy_application(self): - stream_handler = logging.StreamHandler(sys.stdout) - self.log.addHandler(stream_handler) - try: - self.log.info("Log handler installed") - nsd = self.get_descriptor(NSD_YAML) - vnfd = self.get_descriptor(VNFD_YAML) - - if nsd and vnfd: - - vca_charms = os.getenv('VCA_CHARMS', None) - - params = {} - vnf_index = 0 - - def deploy(): - """An inner function to do the deployment of a charm from - either a vdu or vnf. - """ - charm_dir = "{}/{}".format(vca_charms, charm) - - # Setting this to an IP that will fail the initial config. - # This will be detected in the callback, which will execute - # the "config" primitive with the right IP address. - params['rw_mgmt_ip'] = '10.195.8.78' - - # self.loop.run_until_complete(n.CreateNetworkService(nsd)) - ns_name = "default" - - vnf_name = self.n2vc.FormatApplicationName( - ns_name, - vnfd['name'], - str(vnf_index), - ) - - self.loop.run_until_complete( - self.n2vc.DeployCharms( - ns_name, - vnf_name, - vnfd, - charm_dir, - params, - {}, - self.n2vc_callback - ) - ) - - # Check if the VDUs in this VNF have a charm - for vdu in vnfd['vdu']: - vdu_config = vdu.get('vdu-configuration') - if vdu_config: - juju = vdu_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vdu_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - # Check if this VNF has a charm - vnf_config = vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vnf_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - self.loop.run_forever() - - # self.loop.run_until_complete(n.GetMetrics(vnfd, nsd=nsd)) - - # Test actions - # ExecutePrimitive(self, nsd, vnfd, vnf_member_index, primitive, callback, *callback_args, **params): - - # self.loop.run_until_complete(n.DestroyNetworkService(nsd)) - - # self.loop.run_until_complete(self.n2vc.logout()) - finally: - self.log.removeHandler(stream_handler) diff --git a/tests/test_primitive_non-string_parameter.py b/tests/test_primitive_non-string_parameter.py deleted file mode 100644 index 9a2d5ad..0000000 --- a/tests/test_primitive_non-string_parameter.py +++ /dev/null @@ -1,282 +0,0 @@ -# A simple test to exercise the libraries' functionality -import asyncio -import functools -import os -import sys -import logging -import unittest -import yaml -from n2vc.vnf import N2VC - -NSD_YAML = """ -nsd:nsd-catalog: - nsd: - - id: multicharmvdu-ns - name: multicharmvdu-ns - short-name: multicharmvdu-ns - description: NS with 1 VNF - version: '1.0' - logo: osm.png - constituent-vnfd: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index: '1' - vld: - - id: datanet - name: datanet - short-name: datanet - type: ELAN - vnfd-connection-point-ref: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-data -""" - -VNFD_YAML = """ -vnfd:vnfd-catalog: - vnfd: - - id: multicharmvdu-vnf - name: multicharmvdu-vnf - short-name: multicharmvdu-vnf - version: '1.0' - description: A VNF consisting of 1 VDUs w/charm - logo: osm.png - connection-point: - - id: vnf-data - name: vnf-data - short-name: vnf-data - type: VPORT - mgmt-interface: - cp: vnf-data - internal-vld: - - id: internal - name: internal - short-name: internal - type: ELAN - internal-connection-point: - - id-ref: dataVM-internal - vdu: - - id: dataVM - name: dataVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: dataVM-eth0 - position: '1' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: dataVM-internal - - name: dataVM-xe0 - position: '2' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-data - internal-connection-point: - - id: dataVM-internal - name: dataVM-internal - short-name: dataVM-internal - type: VPORT - vdu-configuration: - juju: - charm: simple - initial-config-primitive: - - seq: '1' - name: config - parameter: - - name: ssh-hostname - data-type: STRING - value: - - name: ssh-username - data-type: STRING - value: ubuntu - - name: ssh-password - data-type: STRING - value: ubuntu - - seq: '2' - name: touch - parameter: - - name: filename - data-type: STRING - value: '/home/ubuntu/first-touch-dataVM' - - seq: '3' - name: testint - parameter: - - name: interval - data-type: INTEGER - value: 20 - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' -""" - -class PythonTest(unittest.TestCase): - n2vc = None - - def setUp(self): - - self.log = logging.getLogger() - self.log.level = logging.DEBUG - - self.loop = asyncio.get_event_loop() - - # self.loop = asyncio.new_event_loop() - # asyncio.set_event_loop(None) - - # Extract parameters from the environment in order to run our test - vca_host = os.getenv('VCA_HOST', '127.0.0.1') - vca_port = os.getenv('VCA_PORT', 17070) - vca_user = os.getenv('VCA_USER', 'admin') - vca_charms = os.getenv('VCA_CHARMS', None) - vca_secret = os.getenv('VCA_SECRET', None) - self.n2vc = N2VC( - log=self.log, - server=vca_host, - port=vca_port, - user=vca_user, - secret=vca_secret, - artifacts=vca_charms, - ) - - def tearDown(self): - self.loop.run_until_complete(self.n2vc.logout()) - - def get_descriptor(self, descriptor): - desc = None - try: - tmp = yaml.load(descriptor) - - # Remove the envelope - root = list(tmp.keys())[0] - if root == "nsd:nsd-catalog": - desc = tmp['nsd:nsd-catalog']['nsd'][0] - elif root == "vnfd:vnfd-catalog": - desc = tmp['vnfd:vnfd-catalog']['vnfd'][0] - except ValueError: - assert False - return desc - - def n2vc_callback(self, model_name, application_name, workload_status, task=None): - """We pass the vnfd when setting up the callback, so expect it to be - returned as a tuple.""" - if workload_status and not task: - self.log.debug("Callback: workload status \"{}\"".format(workload_status)) - - if workload_status in ["blocked"]: - task = asyncio.ensure_future( - self.n2vc.ExecutePrimitive( - model_name, - application_name, - "config", - None, - params={ - 'ssh-hostname': '10.195.8.78', - 'ssh-username': 'ubuntu', - 'ssh-password': 'ubuntu' - } - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - pass - elif workload_status in ["active"]: - self.log.debug("Removing charm") - task = asyncio.ensure_future( - self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - - def test_deploy_application(self): - stream_handler = logging.StreamHandler(sys.stdout) - self.log.addHandler(stream_handler) - try: - self.log.info("Log handler installed") - nsd = self.get_descriptor(NSD_YAML) - vnfd = self.get_descriptor(VNFD_YAML) - - if nsd and vnfd: - - vca_charms = os.getenv('VCA_CHARMS', None) - - params = {} - vnf_index = 0 - - def deploy(): - """An inner function to do the deployment of a charm from - either a vdu or vnf. - """ - charm_dir = "{}/{}".format(vca_charms, charm) - - # Setting this to an IP that will fail the initial config. - # This will be detected in the callback, which will execute - # the "config" primitive with the right IP address. - params['rw_mgmt_ip'] = '10.195.8.78' - - # self.loop.run_until_complete(n.CreateNetworkService(nsd)) - ns_name = "default" - - vnf_name = self.n2vc.FormatApplicationName( - ns_name, - vnfd['name'], - str(vnf_index), - ) - - self.loop.run_until_complete( - self.n2vc.DeployCharms( - ns_name, - vnf_name, - vnfd, - charm_dir, - params, - {}, - self.n2vc_callback - ) - ) - - # Check if the VDUs in this VNF have a charm - for vdu in vnfd['vdu']: - vdu_config = vdu.get('vdu-configuration') - if vdu_config: - juju = vdu_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vdu_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - # Check if this VNF has a charm - vnf_config = vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vnf_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - self.loop.run_forever() - - # self.loop.run_until_complete(n.GetMetrics(vnfd, nsd=nsd)) - - # Test actions - # ExecutePrimitive(self, nsd, vnfd, vnf_member_index, primitive, callback, *callback_args, **params): - - # self.loop.run_until_complete(n.DestroyNetworkService(nsd)) - - # self.loop.run_until_complete(self.n2vc.logout()) - finally: - self.log.removeHandler(stream_handler) diff --git a/tests/test_python.py b/tests/test_python.py deleted file mode 100755 index 5bb4325..0000000 --- a/tests/test_python.py +++ /dev/null @@ -1,347 +0,0 @@ -# A simple test to exercise the libraries' functionality -import asyncio -import functools -import os -import sys -import logging -import unittest -import yaml -from n2vc.vnf import N2VC - -NSD_YAML = """ -nsd:nsd-catalog: - nsd: - - id: multicharmvdu-ns - name: multicharmvdu-ns - short-name: multicharmvdu-ns - description: NS with 2 VNFs multicharmvdu-vnf connected by datanet and mgmtnet VLs - version: '1.0' - logo: osm.png - constituent-vnfd: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index: '1' - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index: '2' - vld: - - id: mgmtnet - name: mgmtnet - short-name: mgmtnet - type: ELAN - mgmt-network: 'true' - vim-network-name: mgmt - vnfd-connection-point-ref: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-mgmt - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-mgmt - - id: datanet - name: datanet - short-name: datanet - type: ELAN - vnfd-connection-point-ref: - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-data - - vnfd-id-ref: multicharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-data -""" - -VNFD_YAML = """ -vnfd:vnfd-catalog: - vnfd: - - id: multicharmvdu-vnf - name: multicharmvdu-vnf - short-name: multicharmvdu-vnf - version: '1.0' - description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init - logo: osm.png - connection-point: - - id: vnf-mgmt - name: vnf-mgmt - short-name: vnf-mgmt - type: VPORT - - id: vnf-data - name: vnf-data - short-name: vnf-data - type: VPORT - mgmt-interface: - cp: vnf-mgmt - internal-vld: - - id: internal - name: internal - short-name: internal - type: ELAN - internal-connection-point: - - id-ref: mgmtVM-internal - - id-ref: dataVM-internal - vdu: - - id: mgmtVM - name: mgmtVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: mgmtVM-eth0 - position: '1' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-mgmt - - name: mgmtVM-eth1 - position: '2' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: mgmtVM-internal - internal-connection-point: - - id: mgmtVM-internal - name: mgmtVM-internal - short-name: mgmtVM-internal - type: VPORT - cloud-init-file: cloud-config.txt - vdu-configuration: - juju: - charm: simple - initial-config-primitive: - - seq: '1' - name: config - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: osm4u - - seq: '2' - name: touch - parameter: - - name: filename - value: '/home/ubuntu/first-touch-mgmtVM' - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' - - - id: dataVM - name: dataVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: dataVM-eth0 - position: '1' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: dataVM-internal - - name: dataVM-xe0 - position: '2' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-data - internal-connection-point: - - id: dataVM-internal - name: dataVM-internal - short-name: dataVM-internal - type: VPORT - vdu-configuration: - juju: - charm: simple - initial-config-primitive: - - seq: '1' - name: config - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: osm4u - - seq: '2' - name: touch - parameter: - - name: filename - value: '/home/ubuntu/first-touch-dataVM' - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' -""" - -class PythonTest(unittest.TestCase): - n2vc = None - - def setUp(self): - - self.log = logging.getLogger() - self.log.level = logging.DEBUG - - self.loop = asyncio.get_event_loop() - - # self.loop = asyncio.new_event_loop() - # asyncio.set_event_loop(None) - - # Extract parameters from the environment in order to run our test - vca_host = os.getenv('VCA_HOST', '127.0.0.1') - vca_port = os.getenv('VCA_PORT', 17070) - vca_user = os.getenv('VCA_USER', 'admin') - vca_charms = os.getenv('VCA_CHARMS', None) - vca_secret = os.getenv('VCA_SECRET', None) - self.n2vc = N2VC( - log=self.log, - server=vca_host, - port=vca_port, - user=vca_user, - secret=vca_secret, - artifacts=vca_charms, - ) - - def tearDown(self): - self.loop.run_until_complete(self.n2vc.logout()) - - def get_descriptor(self, descriptor): - desc = None - try: - tmp = yaml.load(descriptor) - - # Remove the envelope - root = list(tmp.keys())[0] - if root == "nsd:nsd-catalog": - desc = tmp['nsd:nsd-catalog']['nsd'][0] - elif root == "vnfd:vnfd-catalog": - desc = tmp['vnfd:vnfd-catalog']['vnfd'][0] - except ValueError: - assert False - return desc - - def n2vc_callback(self, model_name, application_name, workload_status, task=None): - """We pass the vnfd when setting up the callback, so expect it to be - returned as a tuple.""" - if workload_status and not task: - self.log.debug("Callback: workload status \"{}\"".format(workload_status)) - - if workload_status in ["blocked"]: - task = asyncio.ensure_future( - self.n2vc.ExecutePrimitive( - model_name, - application_name, - "config", - None, - params={ - 'ssh-hostname': '10.195.8.78', - 'ssh-username': 'ubuntu', - 'ssh-password': 'ubuntu' - } - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - pass - elif workload_status in ["active"]: - self.log.debug("Removing charm") - task = asyncio.ensure_future( - self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - - def test_deploy_application(self): - stream_handler = logging.StreamHandler(sys.stdout) - self.log.addHandler(stream_handler) - try: - self.log.info("Log handler installed") - nsd = self.get_descriptor(NSD_YAML) - vnfd = self.get_descriptor(VNFD_YAML) - - if nsd and vnfd: - - vca_charms = os.getenv('VCA_CHARMS', None) - - params = {} - vnf_index = 0 - - def deploy(): - """An inner function to do the deployment of a charm from - either a vdu or vnf. - """ - charm_dir = "{}/{}".format(vca_charms, charm) - - # Setting this to an IP that will fail the initial config. - # This will be detected in the callback, which will execute - # the "config" primitive with the right IP address. - params['rw_mgmt_ip'] = '10.195.8.78' - - # self.loop.run_until_complete(n.CreateNetworkService(nsd)) - ns_name = "default" - - vnf_name = self.n2vc.FormatApplicationName( - ns_name, - vnfd['name'], - str(vnf_index), - ) - - self.loop.run_until_complete( - self.n2vc.DeployCharms( - ns_name, - vnf_name, - vnfd, - charm_dir, - params, - {}, - self.n2vc_callback - ) - ) - - # Check if the VDUs in this VNF have a charm - for vdu in vnfd['vdu']: - vdu_config = vdu.get('vdu-configuration') - if vdu_config: - juju = vdu_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vdu_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - # Check if this VNF has a charm - vnf_config = vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vnf_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - self.loop.run_forever() - - # self.loop.run_until_complete(n.GetMetrics(vnfd, nsd=nsd)) - - # Test actions - # ExecutePrimitive(self, nsd, vnfd, vnf_member_index, primitive, callback, *callback_args, **params): - - # self.loop.run_until_complete(n.DestroyNetworkService(nsd)) - - # self.loop.run_until_complete(self.n2vc.logout()) - finally: - self.log.removeHandler(stream_handler) diff --git a/tests/test_single_vdu_proxy_charm.py b/tests/test_single_vdu_proxy_charm.py deleted file mode 100644 index a971872..0000000 --- a/tests/test_single_vdu_proxy_charm.py +++ /dev/null @@ -1,351 +0,0 @@ -"""Test the deployment and configuration of a proxy charm. - 1. Deploy proxy charm to a unit - 2. Execute 'get-ssh-public-key' primitive and get returned value - 3. Create LXD container with unit's public ssh key - 4. Verify SSH works between unit and container - 5. Destroy Juju unit - 6. Stop and Destroy LXD container -""" -import asyncio -import functools -import os -import sys -import logging -import unittest -from . import utils -import yaml -from n2vc.vnf import N2VC - -NSD_YAML = """ -nsd:nsd-catalog: - nsd: - - id: singlecharmvdu-ns - name: singlecharmvdu-ns - short-name: singlecharmvdu-ns - description: NS with 1 VNFs singlecharmvdu-vnf connected by datanet and mgmtnet VLs - version: '1.0' - logo: osm.png - constituent-vnfd: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index: '1' - vld: - - id: mgmtnet - name: mgmtnet - short-name: mgmtnet - type: ELAN - mgmt-network: 'true' - vim-network-name: mgmt - vnfd-connection-point-ref: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-mgmt - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-mgmt - - id: datanet - name: datanet - short-name: datanet - type: ELAN - vnfd-connection-point-ref: - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '1' - vnfd-connection-point-ref: vnf-data - - vnfd-id-ref: singlecharmvdu-vnf - member-vnf-index-ref: '2' - vnfd-connection-point-ref: vnf-data -""" - -VNFD_YAML = """ -vnfd:vnfd-catalog: - vnfd: - - id: singlecharmvdu-vnf - name: singlecharmvdu-vnf - short-name: singlecharmvdu-vnf - version: '1.0' - description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init - logo: osm.png - connection-point: - - id: vnf-mgmt - name: vnf-mgmt - short-name: vnf-mgmt - type: VPORT - - id: vnf-data - name: vnf-data - short-name: vnf-data - type: VPORT - mgmt-interface: - cp: vnf-mgmt - internal-vld: - - id: internal - name: internal - short-name: internal - type: ELAN - internal-connection-point: - - id-ref: mgmtVM-internal - - id-ref: dataVM-internal - vdu: - - id: mgmtVM - name: mgmtVM - image: xenial - count: '1' - vm-flavor: - vcpu-count: '1' - memory-mb: '1024' - storage-gb: '10' - interface: - - name: mgmtVM-eth0 - position: '1' - type: EXTERNAL - virtual-interface: - type: VIRTIO - external-connection-point-ref: vnf-mgmt - - name: mgmtVM-eth1 - position: '2' - type: INTERNAL - virtual-interface: - type: VIRTIO - internal-connection-point-ref: mgmtVM-internal - internal-connection-point: - - id: mgmtVM-internal - name: mgmtVM-internal - short-name: mgmtVM-internal - type: VPORT - cloud-init-file: cloud-config.txt - vdu-configuration: - juju: - charm: simple - initial-config-primitive: - - seq: '1' - name: config - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: ubuntu - - seq: '2' - name: touch - parameter: - - name: filename - value: '/home/ubuntu/first-touch-mgmtVM' - config-primitive: - - name: touch - parameter: - - name: filename - data-type: STRING - default-value: '/home/ubuntu/touched' - -""" - - -class PythonTest(unittest.TestCase): - n2vc = None - container = None - - def setUp(self): - self.log = logging.getLogger() - self.log.level = logging.DEBUG - - self.loop = asyncio.get_event_loop() - - # self.container = utils.create_lxd_container() - self.n2vc = utils.get_n2vc() - - def tearDown(self): - if self.container: - self.container.stop() - self.container.delete() - - self.loop.run_until_complete(self.n2vc.logout()) - - def n2vc_callback(self, model_name, application_name, workload_status, workload_message, task=None): - """We pass the vnfd when setting up the callback, so expect it to be - returned as a tuple.""" - self.log.debug("[Callback] Workload status '{}' for application {}".format(workload_status, application_name)) - self.log.debug("[Callback] Task: \"{}\"".format(task)) - - if workload_status == "exec_primitive" and task: - self.log.debug("Getting Primitive Status") - # get the uuid from the task - uuid = task.result() - - # get the status of the action - task = asyncio.ensure_future( - self.n2vc.GetPrimitiveStatus( - model_name, - uuid, - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, model_name, application_name, "primitive_status", task)) - - if workload_status == "primitive_status" and task and not self.container: - self.log.debug("Creating LXD container") - # Get the ssh key - result = task.result() - pubkey = result['pubkey'] - - self.container = utils.create_lxd_container(pubkey) - mgmtaddr = self.container.state().network['eth0']['addresses'] - - self.log.debug("Setting config ssh-hostname={}".format(mgmtaddr[0]['address'])) - task = asyncio.ensure_future( - self.n2vc.ExecutePrimitive( - model_name, - application_name, - "config", - None, - params={ - 'ssh-hostname': mgmtaddr[0]['address'], - } - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, model_name, application_name, None, None)) - - if workload_status and not task: - self.log.debug("Callback: workload status \"{}\"".format(workload_status)) - - if workload_status in ["blocked"] and not self.container: - self.log.debug("Getting public SSH key") - - # Execute 'get-ssh-public-key' primitive and get returned value - task = asyncio.ensure_future( - self.n2vc.ExecutePrimitive( - model_name, - application_name, - "get-ssh-public-key", - None, - params={ - 'ssh-hostname': '10.195.8.78', - 'ssh-username': 'ubuntu', - 'ssh-password': 'ubuntu' - } - ) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, model_name, application_name, "exec_primitive", task)) - - - # task = asyncio.ensure_future( - # self.n2vc.ExecutePrimitive( - # model_name, - # application_name, - # "config", - # None, - # params={ - # 'ssh-hostname': '10.195.8.78', - # 'ssh-username': 'ubuntu', - # 'ssh-password': 'ubuntu' - # } - # ) - # ) - # task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - pass - elif workload_status in ["active"]: - self.log.debug("Removing charm") - task = asyncio.ensure_future( - self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback) - ) - task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None)) - - if self.container: - utils.destroy_lxd_container(self.container) - self.container = None - - # Stop the test - self.loop.call_soon_threadsafe(self.loop.stop) - - def test_deploy_application(self): - """Deploy proxy charm to a unit.""" - stream_handler = logging.StreamHandler(sys.stdout) - self.log.addHandler(stream_handler) - try: - self.log.info("Log handler installed") - nsd = utils.get_descriptor(NSD_YAML) - vnfd = utils.get_descriptor(VNFD_YAML) - - if nsd and vnfd: - - vca_charms = os.getenv('VCA_CHARMS', None) - - params = {} - vnf_index = 0 - - def deploy(): - """An inner function to do the deployment of a charm from - either a vdu or vnf. - """ - charm_dir = "{}/{}".format(vca_charms, charm) - - # Setting this to an IP that will fail the initial config. - # This will be detected in the callback, which will execute - # the "config" primitive with the right IP address. - # mgmtaddr = self.container.state().network['eth0']['addresses'] - # params['rw_mgmt_ip'] = mgmtaddr[0]['address'] - - # Legacy method is to set the ssh-private-key config - # with open(utils.get_juju_private_key(), "r") as f: - # pkey = f.readline() - # params['ssh-private-key'] = pkey - - ns_name = "default" - - vnf_name = self.n2vc.FormatApplicationName( - ns_name, - vnfd['name'], - str(vnf_index), - ) - - self.loop.run_until_complete( - self.n2vc.DeployCharms( - ns_name, - vnf_name, - vnfd, - charm_dir, - params, - {}, - self.n2vc_callback - ) - ) - - # Check if the VDUs in this VNF have a charm - for vdu in vnfd['vdu']: - vdu_config = vdu.get('vdu-configuration') - if vdu_config: - juju = vdu_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vdu_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - # Check if this VNF has a charm - vnf_config = vnfd.get("vnf-configuration") - if vnf_config: - juju = vnf_config['juju'] - self.assertIsNotNone(juju) - - charm = juju['charm'] - self.assertIsNotNone(charm) - - params['initial-config-primitive'] = vnf_config['initial-config-primitive'] - - deploy() - vnf_index += 1 - - self.loop.run_forever() - # while self.loop.is_running(): - # # await asyncio.sleep(1) - # time.sleep(1) - - # Test actions - # ExecutePrimitive(self, nsd, vnfd, vnf_member_index, primitive, callback, *callback_args, **params): - - # self.loop.run_until_complete(n.DestroyNetworkService(nsd)) - - # self.loop.run_until_complete(self.n2vc.logout()) - finally: - self.log.removeHandler(stream_handler) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index d86d6f5..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import n2vc.vnf -import pylxd -import os -import shlex -import subprocess -import time -import uuid -import yaml - -# Disable InsecureRequestWarning w/LXD -import urllib3 -urllib3.disable_warnings() - -here = os.path.dirname(os.path.realpath(__file__)) - - -def get_charm_path(): - return "{}/charms".format(here) - - -def get_layer_path(): - return "{}/charms/layers".format(here) - - -def parse_metrics(application, results): - """Parse the returned metrics into a dict.""" - - # We'll receive the results for all units, to look for the one we want - # Caveat: we're grabbing results from the first unit of the application, - # which is enough for testing, since we're only deploying a single unit. - retval = {} - for unit in results: - if unit.startswith(application): - for result in results[unit]: - retval[result['key']] = result['value'] - return retval - -def collect_metrics(application): - """Invoke Juju's metrics collector. - - Caveat: this shells out to the `juju collect-metrics` command, rather than - making an API call. At the time of writing, that API is not exposed through - the client library. - """ - - try: - logging.debug("Collecting metrics") - subprocess.check_call(['juju', 'collect-metrics', application]) - except subprocess.CalledProcessError as e: - raise Exception("Unable to collect metrics: {}".format(e)) - - -def build_charm(charm): - """Build a test charm. - - Builds one of the charms in tests/charms/layers and returns the path - to the compiled charm. The calling test is responsible for removing - the charm artifact during cleanup. - """ - # stream_handler = logging.StreamHandler(sys.stdout) - # log.addHandler(stream_handler) - - # Make sure the charm snap is installed - try: - logging.debug("Looking for charm-tools") - subprocess.check_call(['which', 'charm']) - except subprocess.CalledProcessError as e: - raise Exception("charm snap not installed.") - - try: - builds = get_charm_path() - - cmd = "charm build {}/{} -o {}/".format( - get_layer_path(), - charm, - builds, - ) - subprocess.check_call(shlex.split(cmd)) - return "{}/{}".format(builds, charm) - except subprocess.CalledProcessError as e: - raise Exception("charm build failed: {}.".format(e)) - - return None - - -def get_descriptor(descriptor): - desc = None - try: - tmp = yaml.load(descriptor) - - # Remove the envelope - root = list(tmp.keys())[0] - if root == "nsd:nsd-catalog": - desc = tmp['nsd:nsd-catalog']['nsd'][0] - elif root == "vnfd:vnfd-catalog": - desc = tmp['vnfd:vnfd-catalog']['vnfd'][0] - except ValueError: - assert False - return desc - -def get_n2vc(): - """Return an instance of N2VC.VNF.""" - log = logging.getLogger() - log.level = logging.DEBUG - - # Extract parameters from the environment in order to run our test - vca_host = os.getenv('VCA_HOST', '127.0.0.1') - vca_port = os.getenv('VCA_PORT', 17070) - vca_user = os.getenv('VCA_USER', 'admin') - vca_charms = os.getenv('VCA_CHARMS', None) - vca_secret = os.getenv('VCA_SECRET', None) - client = n2vc.vnf.N2VC( - log=log, - server=vca_host, - port=vca_port, - user=vca_user, - secret=vca_secret, - artifacts=vca_charms, - ) - return client - -def create_lxd_container(public_key=None): - """ - Returns a container object - - If public_key isn't set, we'll use the Juju ssh key - """ - - client = get_lxd_client() - test_machine = "test-{}-add-manual-machine-ssh".format( - uuid.uuid4().hex[-4:] - ) - - private_key_path, public_key_path = find_juju_ssh_keys() - # private_key_path = os.path.expanduser( - # "~/.local/share/juju/ssh/juju_id_rsa" - # ) - # public_key_path = os.path.expanduser( - # "~/.local/share/juju/ssh/juju_id_rsa.pub" - # ) - - # Use the self-signed cert generated by lxc on first run - crt = os.path.expanduser('~/snap/lxd/current/.config/lxc/client.crt') - assert os.path.exists(crt) - - key = os.path.expanduser('~/snap/lxd/current/.config/lxc/client.key') - assert os.path.exists(key) - - # create profile w/cloud-init and juju ssh key - if not public_key: - public_key = "" - with open(public_key_path, "r") as f: - public_key = f.readline() - - profile = client.profiles.create( - test_machine, - config={'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key)}, - devices={ - 'root': {'path': '/', 'pool': 'default', 'type': 'disk'}, - 'eth0': { - 'nictype': 'bridged', - 'parent': 'lxdbr0', - 'type': 'nic' - } - } - ) - - # create lxc machine - config = { - 'name': test_machine, - 'source': { - 'type': 'image', - 'alias': 'xenial', - 'mode': 'pull', - 'protocol': 'simplestreams', - 'server': 'https://cloud-images.ubuntu.com/releases', - }, - 'profiles': [test_machine], - } - container = client.containers.create(config, wait=True) - container.start(wait=True) - - def wait_for_network(container, timeout=30): - """Wait for eth0 to have an ipv4 address.""" - starttime = time.time() - while(time.time() < starttime + timeout): - time.sleep(1) - if 'eth0' in container.state().network: - addresses = container.state().network['eth0']['addresses'] - if len(addresses) > 0: - if addresses[0]['family'] == 'inet': - return addresses[0] - return None - - host = wait_for_network(container) - - # HACK: We need to give sshd a chance to bind to the interface, - # and pylxd's container.execute seems to be broken and fails and/or - # hangs trying to properly check if the service is up. - time.sleep(5) - - return container - - -def destroy_lxd_container(container): - """Stop and delete a LXD container.""" - container.stop(wait=True) - container.delete() - - -def find_lxd_config(): - """Find the LXD configuration directory.""" - paths = [] - paths.append(os.path.expanduser("~/.config/lxc")) - paths.append(os.path.expanduser("~/snap/lxd/current/.config/lxc")) - - for path in paths: - if os.path.exists(path): - crt = os.path.expanduser("{}/client.crt".format(path)) - key = os.path.expanduser("{}/client.key".format(path)) - if os.path.exists(crt) and os.path.exists(key): - return (crt, key) - return (None, None) - - -def find_juju_ssh_keys(): - """Find the Juju ssh keys.""" - - paths = [] - paths.append(os.path.expanduser("~/.local/share/juju/ssh/")) - - for path in paths: - if os.path.exists(path): - private = os.path.expanduser("{}/juju_id_rsa".format(path)) - public = os.path.expanduser("{}/juju_id_rsa.pub".format(path)) - if os.path.exists(private) and os.path.exists(public): - return (private, public) - return (None, None) - - -def get_juju_private_key(): - keys = find_juju_ssh_keys() - return keys[0] - - -def get_lxd_client(host="127.0.0.1", port="8443", verify=False): - """ Get the LXD client.""" - client = None - (crt, key) = find_lxd_config() - - if crt and key: - client = pylxd.Client( - endpoint="https://{}:{}".format(host, port), - cert=(crt, key), - verify=verify, - ) - - return client diff --git a/tox.ini b/tox.ini index 502214f..9ef529f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,28 +4,54 @@ # and then run "tox" from this directory. [tox] -envlist = lint,py35 +envlist = py3,lint,integration skipsdist=True +[pytest] +markers = + serial: mark a test that must run by itself + [testenv] basepython=python3 usedevelop=True # for testing with other python versions -commands=nosetests +commands = py.test --ignore modules/ --tb native -ra -v -s -n auto -k 'not integration' -m 'not serial' {posargs} +passenv = + HOME + VCA_HOST + VCA_PORT + VCA_USER + VCA_SECRET + # These are needed so executing `charm build` succeeds + TERM + TERMINFO deps = - nose mock - pyyaml + pyyaml + pytest + pytest-asyncio + pytest-xdist + paramiko + pylxd + +[testenv:py3] +# default tox env, excludes integration and serial tests +commands = + pytest --ignore modules/ --tb native -ra -v -s -n auto -k 'not integration' -m 'not serial' {posargs} [testenv:lint] -envdir = {toxworkdir}/py35 +envdir = {toxworkdir}/py3 commands = - flake8 --ignore E501 {posargs} juju tests + flake8 --ignore E501,E402 {posargs} n2vc tests deps = flake8 +[testenv:integration] +envdir = {toxworkdir}/py3 +commands = py.test --ignore modules/ --tb native -ra -v -s -n 1 -k 'integration' -m 'serial' {posargs} + [testenv:build] -basepython = python3 -deps = stdeb - setuptools-version-command +deps = + stdeb + setuptools-version-command commands = python3 setup.py --command-packages=stdeb.command bdist_deb -- 2.25.1