Fix bug 601 25/7325/1
authorAdam Israel <adam.israel@canonical.com>
Fri, 15 Mar 2019 19:28:47 +0000 (15:28 -0400)
committerAdam Israel <adam.israel@canonical.com>
Fri, 15 Mar 2019 19:28:47 +0000 (15:28 -0400)
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 <adam.israel@canonical.com>
14 files changed:
n2vc/vnf.py
tests/base.py
tests/charms/layers/broken/README.md [new file with mode: 0644]
tests/charms/layers/broken/actions.yaml [new file with mode: 0644]
tests/charms/layers/broken/actions/touch [new file with mode: 0755]
tests/charms/layers/broken/config.yaml [new file with mode: 0644]
tests/charms/layers/broken/icon.svg [new file with mode: 0644]
tests/charms/layers/broken/layer.yaml [new file with mode: 0644]
tests/charms/layers/broken/metadata.yaml [new file with mode: 0644]
tests/charms/layers/broken/metrics.yaml [new file with mode: 0644]
tests/charms/layers/broken/reactive/simple.py [new file with mode: 0644]
tests/charms/layers/broken/tests/00-setup [new file with mode: 0644]
tests/charms/layers/broken/tests/10-deploy [new file with mode: 0644]
tests/integration/test_broken_charm.py [new file with mode: 0644]

index 6e4aaf3..9cdbb33 100644 (file)
@@ -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."""
index e4111eb..41fa191 100644 (file)
@@ -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 (file)
index 0000000..9234e57
--- /dev/null
@@ -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 (file)
index 0000000..6cd6f8c
--- /dev/null
@@ -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 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -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 (file)
index 0000000..51f2ce4
--- /dev/null
@@ -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 (file)
index 0000000..e092eef
--- /dev/null
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\r
+<!-- Created with Inkscape (http://www.inkscape.org/) -->\r
+\r
+<svg\r
+   xmlns:dc="http://purl.org/dc/elements/1.1/"\r
+   xmlns:cc="http://creativecommons.org/ns#"\r
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r
+   xmlns:svg="http://www.w3.org/2000/svg"\r
+   xmlns="http://www.w3.org/2000/svg"\r
+   xmlns:xlink="http://www.w3.org/1999/xlink"\r
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\r
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\r
+   width="96"\r
+   height="96"\r
+   id="svg6517"\r
+   version="1.1"\r
+   inkscape:version="0.48+devel r12274"\r
+   sodipodi:docname="Juju_charm_icon_template.svg">\r
+  <defs\r
+     id="defs6519">\r
+    <linearGradient\r
+       inkscape:collect="always"\r
+       xlink:href="#Background"\r
+       id="linearGradient6461"\r
+       gradientUnits="userSpaceOnUse"\r
+       x1="0"\r
+       y1="970.29498"\r
+       x2="144"\r
+       y2="970.29498"\r
+       gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />\r
+    <linearGradient\r
+       id="Background">\r
+      <stop\r
+         id="stop4178"\r
+         offset="0"\r
+         style="stop-color:#b8b8b8;stop-opacity:1" />\r
+      <stop\r
+         id="stop4180"\r
+         offset="1"\r
+         style="stop-color:#c9c9c9;stop-opacity:1" />\r
+    </linearGradient>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Inner Shadow"\r
+       id="filter1121">\r
+      <feFlood\r
+         flood-opacity="0.59999999999999998"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood1123" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="out"\r
+         result="composite1"\r
+         id="feComposite1125" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur1127" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="2"\r
+         result="offset"\r
+         id="feOffset1129" />\r
+      <feComposite\r
+         in="offset"\r
+         in2="SourceGraphic"\r
+         operator="atop"\r
+         result="composite2"\r
+         id="feComposite1131" />\r
+    </filter>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Drop Shadow"\r
+       id="filter950">\r
+      <feFlood\r
+         flood-opacity="0.25"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood952" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="in"\r
+         result="composite1"\r
+         id="feComposite954" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur956" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="1"\r
+         result="offset"\r
+         id="feOffset958" />\r
+      <feComposite\r
+         in="SourceGraphic"\r
+         in2="offset"\r
+         operator="over"\r
+         result="composite2"\r
+         id="feComposite960" />\r
+    </filter>\r
+    <clipPath\r
+       clipPathUnits="userSpaceOnUse"\r
+       id="clipPath873">\r
+      <g\r
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"\r
+         id="g875"\r
+         inkscape:label="Layer 1"\r
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">\r
+        <path\r
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"\r
+           d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"\r
+           id="path877"\r
+           inkscape:connector-curvature="0"\r
+           sodipodi:nodetypes="sssssssss" />\r
+      </g>\r
+    </clipPath>\r
+    <filter\r
+       inkscape:collect="always"\r
+       id="filter891"\r
+       inkscape:label="Badge Shadow">\r
+      <feGaussianBlur\r
+         inkscape:collect="always"\r
+         stdDeviation="0.71999962"\r
+         id="feGaussianBlur893" />\r
+    </filter>\r
+  </defs>\r
+  <sodipodi:namedview\r
+     id="base"\r
+     pagecolor="#ffffff"\r
+     bordercolor="#666666"\r
+     borderopacity="1.0"\r
+     inkscape:pageopacity="0.0"\r
+     inkscape:pageshadow="2"\r
+     inkscape:zoom="4.0745362"\r
+     inkscape:cx="18.514671"\r
+     inkscape:cy="49.018169"\r
+     inkscape:document-units="px"\r
+     inkscape:current-layer="layer1"\r
+     showgrid="true"\r
+     fit-margin-top="0"\r
+     fit-margin-left="0"\r
+     fit-margin-right="0"\r
+     fit-margin-bottom="0"\r
+     inkscape:window-width="1920"\r
+     inkscape:window-height="1029"\r
+     inkscape:window-x="0"\r
+     inkscape:window-y="24"\r
+     inkscape:window-maximized="1"\r
+     showborder="true"\r
+     showguides="true"\r
+     inkscape:guide-bbox="true"\r
+     inkscape:showpageshadow="false">\r
+    <inkscape:grid\r
+       type="xygrid"\r
+       id="grid821" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="16,48"\r
+       id="guide823" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,80"\r
+       id="guide825" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="80,40"\r
+       id="guide827" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,16"\r
+       id="guide829" />\r
+  </sodipodi:namedview>\r
+  <metadata\r
+     id="metadata6522">\r
+    <rdf:RDF>\r
+      <cc:Work\r
+         rdf:about="">\r
+        <dc:format>image/svg+xml</dc:format>\r
+        <dc:type\r
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\r
+        <dc:title></dc:title>\r
+      </cc:Work>\r
+    </rdf:RDF>\r
+  </metadata>\r
+  <g\r
+     inkscape:label="BACKGROUND"\r
+     inkscape:groupmode="layer"\r
+     id="layer1"\r
+     transform="translate(268,-635.29076)"\r
+     style="display:inline">\r
+    <path\r
+       style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"\r
+       d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"\r
+       id="path6455"\r
+       inkscape:connector-curvature="0"\r
+       sodipodi:nodetypes="sssssssss" />\r
+  </g>\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer3"\r
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"\r
+     style="display:inline" />\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer2"\r
+     inkscape:label="BADGE"\r
+     style="display:none"\r
+     sodipodi:insensitive="true">\r
+    <g\r
+       style="display:inline"\r
+       transform="translate(-340.00001,-581)"\r
+       id="g4394"\r
+       clip-path="none">\r
+      <g\r
+         id="g855">\r
+        <g\r
+           inkscape:groupmode="maskhelper"\r
+           id="g870"\r
+           clip-path="url(#clipPath873)"\r
+           style="opacity:0.6;filter:url(#filter891)">\r
+          <path\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path844"\r
+             style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+        </g>\r
+        <g\r
+           id="g862">\r
+          <path\r
+             sodipodi:type="arc"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4398"\r
+             sodipodi:cx="252"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:rx="12"\r
+             sodipodi:ry="12"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />\r
+          <path\r
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path4400"\r
+             style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+          <path\r
+             sodipodi:type="star"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4459"\r
+             sodipodi:sides="5"\r
+             sodipodi:cx="666.19574"\r
+             sodipodi:cy="589.50385"\r
+             sodipodi:r1="7.2431178"\r
+             sodipodi:r2="4.3458705"\r
+             sodipodi:arg1="1.0471976"\r
+             sodipodi:arg2="1.6755161"\r
+             inkscape:flatsided="false"\r
+             inkscape:rounded="0.1"\r
+             inkscape:randomized="0"\r
+             d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"\r
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />\r
+        </g>\r
+      </g>\r
+    </g>\r
+  </g>\r
+</svg>\r
diff --git a/tests/charms/layers/broken/layer.yaml b/tests/charms/layers/broken/layer.yaml
new file mode 100644 (file)
index 0000000..3fed5e2
--- /dev/null
@@ -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 (file)
index 0000000..1780d3f
--- /dev/null
@@ -0,0 +1,5 @@
+name: broken
+summary: A (broken) simple VNF proxy charm
+maintainer: Adam Israel <adam.israel@canonical.com>
+subordinate: false
+series: ['xenial']
diff --git a/tests/charms/layers/broken/metrics.yaml b/tests/charms/layers/broken/metrics.yaml
new file mode 100644 (file)
index 0000000..6ebb605
--- /dev/null
@@ -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 (file)
index 0000000..1529eee
--- /dev/null
@@ -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 (file)
index 0000000..f0616a5
--- /dev/null
@@ -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 (file)
index 0000000..9a26117
--- /dev/null
@@ -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 (file)
index 0000000..296096f
--- /dev/null
@@ -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'