From 7bf2f4d5ba51d8a6909a8709aeda200ddb153b03 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 15 Mar 2019 15:28:47 -0400 Subject: [PATCH] Fix bug 601 This fixes bug 601, where a charm in a broken state would fail to be removed. This builds of of the new DestroyNetworkService method, which will remove a model containing a network service. There is no way, currently, to resolve errors on an individual charm through the Juju API (client), but removing the model will force the removal of a broken charm. Change-Id: I47f41991ed444395061b5a20e5a51059950e5200 Signed-off-by: Adam Israel --- n2vc/vnf.py | 16 +- tests/base.py | 5 + tests/charms/layers/broken/README.md | 3 + tests/charms/layers/broken/actions.yaml | 9 + tests/charms/layers/broken/actions/touch | 33 +++ tests/charms/layers/broken/config.yaml | 14 + tests/charms/layers/broken/icon.svg | 279 ++++++++++++++++++ tests/charms/layers/broken/layer.yaml | 4 + tests/charms/layers/broken/metadata.yaml | 5 + tests/charms/layers/broken/metrics.yaml | 5 + tests/charms/layers/broken/reactive/simple.py | 45 +++ tests/charms/layers/broken/tests/00-setup | 5 + tests/charms/layers/broken/tests/10-deploy | 35 +++ tests/integration/test_broken_charm.py | 177 +++++++++++ 14 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 tests/charms/layers/broken/README.md create mode 100644 tests/charms/layers/broken/actions.yaml create mode 100755 tests/charms/layers/broken/actions/touch create mode 100644 tests/charms/layers/broken/config.yaml create mode 100644 tests/charms/layers/broken/icon.svg create mode 100644 tests/charms/layers/broken/layer.yaml create mode 100644 tests/charms/layers/broken/metadata.yaml create mode 100644 tests/charms/layers/broken/metrics.yaml create mode 100644 tests/charms/layers/broken/reactive/simple.py create mode 100644 tests/charms/layers/broken/tests/00-setup create mode 100644 tests/charms/layers/broken/tests/10-deploy create mode 100644 tests/integration/test_broken_charm.py diff --git a/n2vc/vnf.py b/n2vc/vnf.py index 6e4aaf3..9cdbb33 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -805,6 +805,10 @@ class N2VC: except JujuError as e: if "already exists" not in e.message: raise e + + # Create an observer for this model + await self.create_model_monitor(ns_uuid) + return True async def DestroyNetworkService(self, ns_uuid): @@ -1061,10 +1065,20 @@ class N2VC: self.refcount['model'] += 1 # Create an observer for this model + await self.create_model_monitor(model_name) + + return self.models[model_name] + + async def create_model_monitor(self, model_name): + """Create a monitor for the model, if none exists.""" + if not self.authenticated: + await self.login() + + if model_name not in self.monitors: self.monitors[model_name] = VCAMonitor(model_name) self.models[model_name].add_observer(self.monitors[model_name]) - return self.models[model_name] + return True async def login(self): """Login to the Juju controller.""" diff --git a/tests/base.py b/tests/base.py index e4111eb..41fa191 100644 --- a/tests/base.py +++ b/tests/base.py @@ -586,6 +586,9 @@ class TestN2VC(object): if not self.n2vc: self.n2vc = get_n2vc(loop=loop) + debug("Creating model for Network Service {}".format(self.ns_name)) + await self.n2vc.CreateNetworkService(self.ns_name) + application = self.n2vc.FormatApplicationName( self.ns_name, self.vnf_name, @@ -889,6 +892,8 @@ class TestN2VC(object): try: await self.n2vc.RemoveCharms(self.ns_name, application) + await self.n2vc.DestroyNetworkService(self.ns_name) + while True: # Wait for the application to be removed await asyncio.sleep(10) diff --git a/tests/charms/layers/broken/README.md b/tests/charms/layers/broken/README.md new file mode 100644 index 0000000..9234e57 --- /dev/null +++ b/tests/charms/layers/broken/README.md @@ -0,0 +1,3 @@ +# Overview + +This charm is intended to install and break, requiring it to be removed. diff --git a/tests/charms/layers/broken/actions.yaml b/tests/charms/layers/broken/actions.yaml new file mode 100644 index 0000000..6cd6f8c --- /dev/null +++ b/tests/charms/layers/broken/actions.yaml @@ -0,0 +1,9 @@ +touch: + description: "Touch a file on the VNF." + params: + filename: + description: "The name of the file to touch." + type: string + default: "" + required: + - filename diff --git a/tests/charms/layers/broken/actions/touch b/tests/charms/layers/broken/actions/touch new file mode 100755 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/broken/actions/touch @@ -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/broken/config.yaml b/tests/charms/layers/broken/config.yaml new file mode 100644 index 0000000..51f2ce4 --- /dev/null +++ b/tests/charms/layers/broken/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/broken/icon.svg b/tests/charms/layers/broken/icon.svg new file mode 100644 index 0000000..e092eef --- /dev/null +++ b/tests/charms/layers/broken/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/tests/charms/layers/broken/layer.yaml b/tests/charms/layers/broken/layer.yaml new file mode 100644 index 0000000..3fed5e2 --- /dev/null +++ b/tests/charms/layers/broken/layer.yaml @@ -0,0 +1,4 @@ +includes: ['layer:basic', 'layer:vnfproxy'] +options: + basic: + use_venv: false diff --git a/tests/charms/layers/broken/metadata.yaml b/tests/charms/layers/broken/metadata.yaml new file mode 100644 index 0000000..1780d3f --- /dev/null +++ b/tests/charms/layers/broken/metadata.yaml @@ -0,0 +1,5 @@ +name: broken +summary: A (broken) simple VNF proxy charm +maintainer: Adam Israel +subordinate: false +series: ['xenial'] diff --git a/tests/charms/layers/broken/metrics.yaml b/tests/charms/layers/broken/metrics.yaml new file mode 100644 index 0000000..6ebb605 --- /dev/null +++ b/tests/charms/layers/broken/metrics.yaml @@ -0,0 +1,5 @@ +metrics: + uptime: + type: gauge + description: "Uptime of the VNF" + command: awk '{print $1}' /proc/uptime diff --git a/tests/charms/layers/broken/reactive/simple.py b/tests/charms/layers/broken/reactive/simple.py new file mode 100644 index 0000000..1529eee --- /dev/null +++ b/tests/charms/layers/broken/reactive/simple.py @@ -0,0 +1,45 @@ +from charmhelpers.core.hookenv import ( + action_get, + action_fail, + action_set, + status_set, +) +from charms.reactive import ( + clear_flag, + set_flag, + when, + when_not, +) +import charms.sshproxy + + +@when('sshproxy.configured') +@when_not('simple.installed') +def install_simple_proxy_charm(): + """Post-install actions. + + This function will run when two conditions are met: + 1. The 'sshproxy.configured' state is set + 2. The 'simple.installed' state is not set + + This ensures that the workload status is set to active only when the SSH + proxy is properly configured. + """ + set_flag('simple.installed') + status_set('active', 'Ready!') + + +@when('actions.touch') +def touch(): + raise Exception("I am broken.") + err = '' + try: + filename = action_get('filename') + cmd = ['touch {}'.format(filename)] + result, err = charms.sshproxy._run(cmd) + except Exception: + action_fail('command failed:' + err) + else: + action_set({'output': result}) + finally: + clear_flag('actions.touch') diff --git a/tests/charms/layers/broken/tests/00-setup b/tests/charms/layers/broken/tests/00-setup new file mode 100644 index 0000000..f0616a5 --- /dev/null +++ b/tests/charms/layers/broken/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/broken/tests/10-deploy b/tests/charms/layers/broken/tests/10-deploy new file mode 100644 index 0000000..9a26117 --- /dev/null +++ b/tests/charms/layers/broken/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('simple') + self.d.expose('simple') + + self.d.setup(timeout=900) + self.d.sentry.wait() + + self.unit = self.d.sentry['simple'][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/test_broken_charm.py b/tests/integration/test_broken_charm.py new file mode 100644 index 0000000..296096f --- /dev/null +++ b/tests/integration/test_broken_charm.py @@ -0,0 +1,177 @@ +""" +Test a charm that breaks post-deployment +""" + +import asyncio +import logging +import pytest +from .. import base + + +# @pytest.mark.serial +class TestCharm(base.TestN2VC): + + NSD_YAML = """ + nsd:nsd-catalog: + nsd: + - id: brokencharm-ns + name: brokencharm-ns + short-name: brokencharm-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: hackfest-simplecharm-vnf + name: hackfest-simplecharm-vnf + short-name: hackfest-simplecharm-vnf + version: '1.0' + description: A VNF consisting of 2 VDUs 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: hackfest3-mgmt + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: mgmtVM-eth0 + position: '1' + type: EXTERNAL + virtual-interface: + type: PARAVIRT + external-connection-point-ref: vnf-mgmt + - name: mgmtVM-eth1 + position: '2' + type: INTERNAL + virtual-interface: + type: PARAVIRT + 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 + - id: dataVM + name: dataVM + image: hackfest3-mgmt + count: '1' + vm-flavor: + vcpu-count: '1' + memory-mb: '1024' + storage-gb: '10' + interface: + - name: dataVM-eth0 + position: '1' + type: INTERNAL + virtual-interface: + type: PARAVIRT + internal-connection-point-ref: dataVM-internal + - name: dataVM-xe0 + position: '2' + type: EXTERNAL + virtual-interface: + type: PARAVIRT + external-connection-point-ref: vnf-data + internal-connection-point: + - id: dataVM-internal + name: dataVM-internal + short-name: dataVM-internal + type: VPORT + vnf-configuration: + juju: + charm: broken + proxy: true + initial-config-primitive: + - seq: '1' + name: touch + parameter: + - name: filename + value: '/home/ubuntu/first-touch' + config-primitive: + - name: touch + parameter: + - name: filename + data-type: STRING + default-value: '/home/ubuntu/touched' + """ + + # @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 await self.running(): + print("Waiting for test to finish...") + await asyncio.sleep(15) + logging.debug("test_charm_proxy stopped") + + return 'ok' -- 2.25.1