From: Adam Israel Date: Wed, 8 Aug 2018 16:54:55 +0000 (-0400) Subject: Integration test for metrics + bug fix X-Git-Tag: v5.0.0~13^2 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=commitdiff_plain;h=5afe05434f65dabf514636eff28cbb2069ccb419 Integration test for metrics + bug fix This commit: - adds the beginnings of an integration testing framework - adds an integration test to exercise metric collection - adds a test charm with metrics collection - fixes a potential bug that can cause N2VC to fail if no initial-config-primitive is specified in the VNF descriptor Signed-off-by: Adam Israel --- diff --git a/n2vc/vnf.py b/n2vc/vnf.py index 7c39fa1..d3ad90c 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -332,6 +332,10 @@ class N2VC: if 'rw_mgmt_ip' in params: rw_mgmt_ip = params['rw_mgmt_ip'] + # initial_config = {} + if 'initial-config-primitive' not in params: + params['initial-config-primitive'] = {} + initial_config = self._get_config_from_dict( params['initial-config-primitive'], {'': rw_mgmt_ip} diff --git a/tests/charms/layers/metrics-ci/README.ex b/tests/charms/layers/metrics-ci/README.ex new file mode 100755 index 0000000..b6816b2 --- /dev/null +++ b/tests/charms/layers/metrics-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-ci/config.yaml b/tests/charms/layers/metrics-ci/config.yaml new file mode 100755 index 0000000..51f2ce4 --- /dev/null +++ b/tests/charms/layers/metrics-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-ci/deps/layer/basic b/tests/charms/layers/metrics-ci/deps/layer/basic new file mode 160000 index 0000000..d59d361 --- /dev/null +++ b/tests/charms/layers/metrics-ci/deps/layer/basic @@ -0,0 +1 @@ +Subproject commit d59d3613006a5afe1b9322aed9d77b5945b44356 diff --git a/tests/charms/layers/metrics-ci/deps/layer/metrics b/tests/charms/layers/metrics-ci/deps/layer/metrics new file mode 160000 index 0000000..6861ce3 --- /dev/null +++ b/tests/charms/layers/metrics-ci/deps/layer/metrics @@ -0,0 +1 @@ +Subproject commit 6861ce384f0dcf4e3eb1eaddf421143f4f76e64e diff --git a/tests/charms/layers/metrics-ci/deps/layer/options b/tests/charms/layers/metrics-ci/deps/layer/options new file mode 160000 index 0000000..fcdcea4 --- /dev/null +++ b/tests/charms/layers/metrics-ci/deps/layer/options @@ -0,0 +1 @@ +Subproject commit fcdcea4e5de3e1556c24e6704607862d0ba00a56 diff --git a/tests/charms/layers/metrics-ci/icon.svg b/tests/charms/layers/metrics-ci/icon.svg new file mode 100755 index 0000000..e092eef --- /dev/null +++ b/tests/charms/layers/metrics-ci/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/tests/charms/layers/metrics-ci/layer.yaml b/tests/charms/layers/metrics-ci/layer.yaml new file mode 100755 index 0000000..bd3a2b9 --- /dev/null +++ b/tests/charms/layers/metrics-ci/layer.yaml @@ -0,0 +1 @@ +includes: ['layer:basic', 'layer:metrics'] # if you use any interfaces, add them here diff --git a/tests/charms/layers/metrics-ci/metadata.yaml b/tests/charms/layers/metrics-ci/metadata.yaml new file mode 100755 index 0000000..060274d --- /dev/null +++ b/tests/charms/layers/metrics-ci/metadata.yaml @@ -0,0 +1,12 @@ +name: metrics-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-ci/metrics.yaml b/tests/charms/layers/metrics-ci/metrics.yaml new file mode 100755 index 0000000..dae092f --- /dev/null +++ b/tests/charms/layers/metrics-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-ci/reactive/metrics_ci.py b/tests/charms/layers/metrics-ci/reactive/metrics_ci.py new file mode 100755 index 0000000..9217be4 --- /dev/null +++ b/tests/charms/layers/metrics-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('active', "Ready!") + set_flag('metrics-ci.installed') diff --git a/tests/charms/layers/metrics-ci/tests/00-setup b/tests/charms/layers/metrics-ci/tests/00-setup new file mode 100755 index 0000000..f0616a5 --- /dev/null +++ b/tests/charms/layers/metrics-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-ci/tests/10-deploy b/tests/charms/layers/metrics-ci/tests/10-deploy new file mode 100755 index 0000000..7595ecf --- /dev/null +++ b/tests/charms/layers/metrics-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/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_metrics.py b/tests/integration/test_metrics.py new file mode 100644 index 0000000..1151d46 --- /dev/null +++ b/tests/integration/test_metrics.py @@ -0,0 +1,315 @@ +"""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/utils.py b/tests/utils.py index 9f9000e..d86d6f5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,6 +4,8 @@ import logging import n2vc.vnf import pylxd import os +import shlex +import subprocess import time import uuid import yaml @@ -12,6 +14,77 @@ import yaml 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