Improve N2VC performance/functionality 60/5960/2
authorAdam Israel <adam.israel@canonical.com>
Tue, 10 Apr 2018 19:04:57 +0000 (13:04 -0600)
committerAdam Israel <adam.israel@canonical.com>
Wed, 11 Apr 2018 06:06:45 +0000 (00:06 -0600)
- Refactor test to support multi-vdu charms.
- Add support for deploying multi-vdu charms.
- Execute the initial-config-primitives

This is the first commit to add support for executing the
initial-config-primitive(s). There are some decisions that still need to
be made: how do we report primitive execution back to the
caller/callback in a meaningful way.

Signed-off-by: Adam Israel <adam.israel@canonical.com>
Change-Id: Icc0ce41d256930b337c9097af9edcae2694207e8

n2vc/vnf.py
tests/test_python.py

index c606dda..7244b22 100644 (file)
@@ -36,6 +36,9 @@ class JujuCharmNotFound(Exception):
 class JujuApplicationExists(Exception):
     """The Application already exists."""
 
 class JujuApplicationExists(Exception):
     """The Application already exists."""
 
+class N2VCPrimitiveExecutionFailed(Exception):
+    """Something failed while attempting to execute a primitive."""
+
 
 # Quiet the debug logging
 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
 
 # Quiet the debug logging
 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
@@ -67,7 +70,9 @@ class VCAMonitor(ModelObserver):
                 if old and new:
                     old_status = old.workload_status
                     new_status = new.workload_status
                 if old and new:
                     old_status = old.workload_status
                     new_status = new.workload_status
+
                     if old_status == new_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."""
@@ -79,7 +84,23 @@ class VCAMonitor(ModelObserver):
                                 *self.callback_args)
             except Exception as e:
                 self.log.debug("[1] notify_callback exception {}".format(e))
                                 *self.callback_args)
             except Exception as 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
+
+            # uuid = delta.data['id']     # The Action's unique id
+            # msg = delta.data['message'] # The output of the action
+            #
+            # if delta.data['status'] == "pending":
+            #     # The action is queued
+            #     pass
+            # elif delta.data['status'] == "completed""
+            #     # The action was successful
+            #     pass
+            # elif delta.data['status'] == "failed":
+            #     # The action failed.
+            #     pass
 
 
+            pass
 
 ########
 # TODO
 
 ########
 # TODO
@@ -170,6 +191,7 @@ class N2VC:
                 callback(model_name, application_name, status, *callback_args)
         except Exception as e:
             self.log.error("[0] notify_callback exception {}".format(e))
                 callback(model_name, application_name, status, *callback_args)
         except Exception as e:
             self.log.error("[0] notify_callback exception {}".format(e))
+            raise e
         return True
 
     # Public methods
         return True
 
     # Public methods
@@ -207,7 +229,9 @@ class N2VC:
         :param dict params: A dictionary of runtime parameters
           Examples::
           {
         :param dict params: A dictionary of runtime parameters
           Examples::
           {
-            'rw_mgmt_ip': '1.2.3.4'
+            'rw_mgmt_ip': '1.2.3.4',
+            # 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
           Examples::
           }
         :param dict machine_spec: A dictionary describing the machine to install to
           Examples::
@@ -241,18 +265,6 @@ class N2VC:
         # service. In the meantime, we will always use the 'default' model.
         model_name = 'default'
         model = await self.get_model(model_name)
         # service. In the meantime, we will always use the 'default' model.
         model_name = 'default'
         model = await self.get_model(model_name)
-        # if model_name not in self.models:
-        #     self.log.debug("Getting model {}".format(model_name))
-        #     self.models[model_name] = await self.controller.get_model(model_name)
-        # model = await self.CreateNetworkService(ns_name)
-
-        ###################################################
-        # Get the name of the charm and its configuration #
-        ###################################################
-        config_dict = vnfd['vnf-configuration']
-        juju = config_dict['juju']
-        charm = juju['charm']
-        self.log.debug("Charm: {}".format(charm))
 
         ########################################
         # Verify the application doesn't exist #
 
         ########################################
         # Verify the application doesn't exist #
@@ -269,7 +281,6 @@ class N2VC:
             self.monitors[application_name] = VCAMonitor(model_name, application_name, callback, *callback_args)
             model.add_observer(self.monitors[application_name])
 
             self.monitors[application_name] = VCAMonitor(model_name, application_name, callback, *callback_args)
             model.add_observer(self.monitors[application_name])
 
-
         ########################################################
         # Check for specific machine placement (native charms) #
         ########################################################
         ########################################################
         # Check for specific machine placement (native charms) #
         ########################################################
@@ -294,12 +305,11 @@ class N2VC:
             rw_mgmt_ip = params['rw_mgmt_ip']
 
         initial_config = self._get_config_from_dict(
             rw_mgmt_ip = params['rw_mgmt_ip']
 
         initial_config = self._get_config_from_dict(
-            config_dict['initial-config-primitive'],
+            params['initial-config-primitive'],
             {'<rw_mgmt_ip>': rw_mgmt_ip}
         )
 
             {'<rw_mgmt_ip>': rw_mgmt_ip}
         )
 
-        self.log.debug("JujuApi: Deploying charm {} ({}) from {}".format(
-            charm,
+        self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
             application_name,
             charm_path,
             to=to,
             application_name,
             charm_path,
             to=to,
@@ -309,13 +319,59 @@ class N2VC:
         # Deploy the charm and apply the initial configuration #
         ########################################################
         app = await model.deploy(
         # Deploy the charm and apply the initial configuration #
         ########################################################
         app = await model.deploy(
+            # We expect charm_path to be either the path to the charm on disk
+            # or in the format of cs:series/name
             charm_path,
             charm_path,
+            # This is the formatted, unique name for this charm
             application_name=application_name,
             application_name=application_name,
+            # Proxy charms should use the current LTS. This will need to be
+            # changed for native charms.
             series='xenial',
             series='xenial',
+            # Apply the initial 'config' primitive during deployment
             config=initial_config,
             config=initial_config,
+            # TBD: Where to deploy the charm to.
             to=None,
         )
 
             to=None,
         )
 
+        # #######################################
+        # # Execute initial config primitive(s) #
+        # #######################################
+        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:
+                    # TODO: We need to sort by seq, and queue the actions in order.
+
+                    seq = primitive['seq']
+
+                    primitives[seq] = {
+                        'name': primitive['name'],
+                        'parameters': self._map_primitive_parameters(
+                            primitive['parameter'],
+                            {'<rw_mgmt_ip>': 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.debug.log(
+                    "[N2VC] Exception executing primitive: {}".format(e)
+                )
+                raise
+
     async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
         try:
             if not self.authenticated:
     async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
         try:
             if not self.authenticated:
@@ -336,7 +392,7 @@ class N2VC:
                     if unit:
                         self.log.debug("Executing primitive {}".format(primitive))
                         action = await unit.run_action(primitive, **params)
                     if unit:
                         self.log.debug("Executing primitive {}".format(primitive))
                         action = await unit.run_action(primitive, **params)
-                        action = await action.wait()
+                        action = await action.wait()
                 await model.disconnect()
         except Exception as e:
             self.log.debug("Caught exception while executing primitive: {}".format(e))
                 await model.disconnect()
         except Exception as e:
             self.log.debug("Caught exception while executing primitive: {}".format(e))
@@ -355,6 +411,7 @@ class N2VC:
                 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
         except Exception as e:
             print("Caught exception: {}".format(e))
                 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
         except Exception as e:
             print("Caught exception: {}".format(e))
+            self.log.debug(e)
             raise e
 
     async def DestroyNetworkService(self, nsd):
             raise e
 
     async def DestroyNetworkService(self, nsd):
@@ -391,10 +448,18 @@ class N2VC:
         return await self.set_config(application=application, config=config)
 
     def _get_config_from_dict(self, config_primitive, values):
         return await self.set_config(application=application, config=config)
 
     def _get_config_from_dict(self, config_primitive, values):
-        """Transform the yang config primitive to dict."""
+        """Transform the yang config primitive to dict.
+
+        Expected result:
+
+            config = {
+                'config':
+            }
+        """
         config = {}
         for primitive in config_primitive:
             if primitive['name'] == 'config':
         config = {}
         for primitive in config_primitive:
             if primitive['name'] == 'config':
+                # config = self._map_primitive_parameters()
                 for parameter in primitive['parameter']:
                     param = str(parameter['name'])
                     if parameter['value'] == "<rw_mgmt_ip>":
                 for parameter in primitive['parameter']:
                     param = str(parameter['name'])
                     if parameter['value'] == "<rw_mgmt_ip>":
@@ -404,6 +469,16 @@ class N2VC:
 
         return config
 
 
         return config
 
+    def _map_primitive_parameters(self, parameters, values):
+        params = {}
+        for parameter in parameters:
+            param = str(parameter['name'])
+            if parameter['value'] == "<rw_mgmt_ip>":
+                params[param] = str(values[parameter['value']])
+            else:
+                params[param] = str(parameter['value'])
+        return params
+
     def _get_config_from_yang(self, config_primitive, values):
         """Transform the yang config primitive to dict."""
         config = {}
     def _get_config_from_yang(self, config_primitive, values):
         """Transform the yang config primitive to dict."""
         config = {}
@@ -521,6 +596,11 @@ class N2VC:
             # current_controller no longer exists
             # self.log.debug("Connecting to current controller...")
             # await self.controller.connect_current()
             # current_controller no longer exists
             # self.log.debug("Connecting to current controller...")
             # await self.controller.connect_current()
+            # await self.controller.connect(
+            #     endpoint=self.endpoint,
+            #     username=self.user,
+            #     cacert=cacert,
+            # )
             self.log.fatal("VCA credentials not configured.")
 
         self.authenticated = True
             self.log.fatal("VCA credentials not configured.")
 
         self.authenticated = True
index b770a01..5bb4325 100755 (executable)
@@ -4,216 +4,184 @@ import functools
 import os
 import sys
 import logging
 import os
 import sys
 import logging
-import osm_im.vnfd as vnfd_catalog
-import osm_im.nsd as nsd_catalog
-from pyangbind.lib.serialise import pybindJSONDecoder
 import unittest
 import yaml
 from n2vc.vnf import N2VC
 
 NSD_YAML = """
 import unittest
 import yaml
 from n2vc.vnf import N2VC
 
 NSD_YAML = """
-nsd-catalog:
+nsd:nsd-catalog:
     nsd:
     nsd:
-     -  id: rift_ping_pong_ns
-        logo: rift_logo.png
-        name: ping_pong_ns
-        short-name: ping_pong_ns
-        vendor: RIFT.io
-        version: '1.1'
-        description: RIFT.io sample ping pong network service
+    -   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:
         constituent-vnfd:
-        -   member-vnf-index: '1'
-            vnfd-id-ref: rift_ping_vnf
-        -   member-vnf-index: '2'
-            vnfd-id-ref: rift_pong_vnf
-        initial-service-primitive:
-        -   name: start traffic
-            parameter:
-            -   name: port
-                value: 5555
-            -   name: ssh-username
-                value: fedora
-            -   name: ssh-password
-                value: fedora
-            seq: '1'
-            user-defined-script: start_traffic.py
-        input-parameter-xpath:
-        -   xpath: /nsd:nsd-catalog/nsd:nsd/nsd:vendor
-        ip-profiles:
-        -   description: Inter VNF Link
-            ip-profile-params:
-                gateway-address: 31.31.31.210
-                ip-version: ipv4
-                subnet-address: 31.31.31.0/24
-                dhcp-params:
-                  count: 200
-                  start-address: 31.31.31.2
-            name: InterVNFLink
-        placement-groups:
-        -   member-vnfd:
-            -   member-vnf-index-ref: '1'
-                vnfd-id-ref: rift_ping_vnf
-            -   member-vnf-index-ref: '2'
-                vnfd-id-ref: rift_pong_vnf
-            name: Orcus
-            requirement: Place this VM on the Kuiper belt object Orcus
-            strategy: COLOCATION
-        -   member-vnfd:
-            -   member-vnf-index-ref: '1'
-                vnfd-id-ref: rift_ping_vnf
-            -   member-vnf-index-ref: '2'
-                vnfd-id-ref: rift_pong_vnf
-            name: Quaoar
-            requirement: Place this VM on the Kuiper belt object Quaoar
-            strategy: COLOCATION
+        -   vnfd-id-ref: multicharmvdu-vnf
+            member-vnf-index: '1'
+        -   vnfd-id-ref: multicharmvdu-vnf
+            member-vnf-index: '2'
         vld:
         vld:
-        -   id: mgmt_vl
-            description: Management VL
-            name: mgmt_vl
-            short-name: mgmt_vl
-            vim-network-name: mgmt
+        -   id: mgmtnet
+            name: mgmtnet
+            short-name: mgmtnet
             type: ELAN
             type: ELAN
-            vendor: RIFT.io
-            version: '1.0'
             mgmt-network: 'true'
             mgmt-network: 'true'
+            vim-network-name: mgmt
             vnfd-connection-point-ref:
             vnfd-connection-point-ref:
-            -   member-vnf-index-ref: '1'
-                vnfd-connection-point-ref: ping_vnfd/cp0
-                vnfd-id-ref: rift_ping_vnf
-            -   member-vnf-index-ref: '2'
-                vnfd-connection-point-ref: pong_vnfd/cp0
-                vnfd-id-ref: rift_pong_vnf
-        -   id: ping_pong_vl1
-            description: Data VL
-            ip-profile-ref: InterVNFLink
-            name: data_vl
-            short-name: data_vl
+            -   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
             type: ELAN
-            vendor: RIFT.io
-            version: '1.0'
             vnfd-connection-point-ref:
             vnfd-connection-point-ref:
-            -   member-vnf-index-ref: '1'
-                vnfd-connection-point-ref: ping_vnfd/cp1
-                vnfd-id-ref: rift_ping_vnf
-            -   member-vnf-index-ref: '2'
-                vnfd-connection-point-ref: pong_vnfd/cp1
-                vnfd-id-ref: rift_pong_vnf
+            -   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_VCA_YAML = """
-vnfd-catalog:
+VNFD_YAML = """
+vnfd:vnfd-catalog:
     vnfd:
     vnfd:
-     -  id: rift_ping_vnf
-        name: ping_vnf
-        short-name: ping_vnf
-        logo: rift_logo.png
-        vendor: RIFT.io
-        version: '1.1'
-        description: This is an example RIFT.ware VNF
+    -   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:
         connection-point:
-        -   name: ping_vnfd/cp0
+        -   id: vnf-mgmt
+            name: vnf-mgmt
+            short-name: vnf-mgmt
             type: VPORT
             type: VPORT
-        -   name: ping_vnfd/cp1
+        -   id: vnf-data
+            name: vnf-data
+            short-name: vnf-data
             type: VPORT
             type: VPORT
-        http-endpoint:
-        -   path: api/v1/ping/stats
-            port: '18888'
         mgmt-interface:
         mgmt-interface:
-            dashboard-params:
-                path: api/v1/ping/stats
-                port: '18888'
-            port: '18888'
-            cp: ping_vnfd/cp0
-        placement-groups:
-        -   member-vdus:
-            -   member-vdu-ref: iovdu_0
-            name: Eris
-            requirement: Place this VM on the Kuiper belt object Eris
-            strategy: COLOCATION
+            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:
         vdu:
-        -   cloud-init-file: ping_cloud_init.cfg
+        -   id: mgmtVM
+            name: mgmtVM
+            image: xenial
             count: '1'
             count: '1'
+            vm-flavor:
+                vcpu-count: '1'
+                memory-mb: '1024'
+                storage-gb: '10'
             interface:
             interface:
-            -   name: eth0
-                position: 0
+            -   name: mgmtVM-eth0
+                position: '1'
                 type: EXTERNAL
                 virtual-interface:
                     type: VIRTIO
                 type: EXTERNAL
                 virtual-interface:
                     type: VIRTIO
-                external-connection-point-ref: ping_vnfd/cp0
-            -   name: eth1
-                position: 1
-                type: EXTERNAL
+                external-connection-point-ref: vnf-mgmt
+            -   name: mgmtVM-eth1
+                position: '2'
+                type: INTERNAL
                 virtual-interface:
                     type: VIRTIO
                 virtual-interface:
                     type: VIRTIO
-                external-connection-point-ref: ping_vnfd/cp1
-            id: iovdu_0
-            image: Fedora-x86_64-20-20131211.1-sda-ping.qcow2
-            name: iovdu_0
+                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: <rw_mgmt_ip>
+                    -   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:
             vm-flavor:
-                memory-mb: '512'
-                storage-gb: '4'
                 vcpu-count: '1'
                 vcpu-count: '1'
-        vnf-configuration:
-            config-primitive:
-            -   name: start
-            -   name: stop
-            -   name: restart
-            -   name: config
-                parameter:
-                -   data-type: STRING
-                    default-value: <rw_mgmt_ip>
-                    name: ssh-hostname
-                -   data-type: STRING
-                    default-value: fedora
-                    name: ssh-username
-                -   data-type: STRING
-                    default-value: fedora
-                    name: ssh-password
-                -   data-type: STRING
-                    name: ssh-private-key
-                -   data-type: STRING
-                    default-value: ping
-                    name: mode
-                    read-only: 'true'
-            -   name: set-server
-                parameter:
-                -   data-type: STRING
-                    name: server-ip
-                -   data-type: INTEGER
-                    name: server-port
-            -   name: set-rate
-                parameter:
-                -   data-type: INTEGER
-                    default-value: '5'
-                    name: rate
-            -   name: start-traffic
-            -   name: stop-traffic
-            initial-config-primitive:
-            -   name: config
-                parameter:
-                -   name: ssh-hostname
-                    value: <rw_mgmt_ip>
-                -   name: ssh-username
-                    value: fedora
-                -   name: ssh-password
-                    value: fedora
-                -   name: mode
-                    value: ping
-                seq: '1'
-            -   name: start
-                seq: '2'
-            juju:
-                charm: pingpong
+                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: <rw_mgmt_ip>
+                    -   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'
 """
 
 """
 
-NSD_DICT = {'name': 'ping_pong_ns', 'short-name': 'ping_pong_ns', 'ip-profiles': [{'ip-profile-params': {'ip-version': 'ipv4', 'dhcp-params': {'count': 200, 'start-address': '31.31.31.2'}, 'subnet-address': '31.31.31.0/24', 'gateway-address': '31.31.31.210'}, 'description': 'Inter VNF Link', 'name': 'InterVNFLink'}], 'logo': 'rift_logo.png', 'description': 'RIFT.io sample ping pong network service', '_admin': {'storage': {'folder': 'd9bc2e64-dfec-4c36-a8bb-c4667e8d7a53', 'tarfile': 'pkg', 'file': 'ping_pong_ns', 'path': '/app/storage/', 'fs': 'local'}, 'created': 1521127984.6414561, 'modified': 1521127984.6414561, 'projects_write': ['admin'], 'projects_read': ['admin']}, 'placement-groups': [{'member-vnfd': [{'member-vnf-index-ref': '1', 'vnfd-id-ref': 'rift_ping_vnf'}, {'member-vnf-index-ref': '2', 'vnfd-id-ref': 'rift_pong_vnf'}], 'name': 'Orcus', 'strategy': 'COLOCATION', 'requirement': 'Place this VM on the Kuiper belt object Orcus'}, {'member-vnfd': [{'member-vnf-index-ref': '1', 'vnfd-id-ref': 'rift_ping_vnf'}, {'member-vnf-index-ref': '2', 'vnfd-id-ref': 'rift_pong_vnf'}], 'name': 'Quaoar', 'strategy': 'COLOCATION', 'requirement': 'Place this VM on the Kuiper belt object Quaoar'}], 'input-parameter-xpath': [{'xpath': '/nsd:nsd-catalog/nsd:nsd/nsd:vendor'}], 'version': '1.1', 'vld': [{'name': 'mgmt', 'short-name': 'mgmt', 'mgmt-network': 'true', 'vim-network-name': 'mgmt', 'version': '1.0', 'vnfd-connection-point-ref': [{'vnfd-connection-point-ref': 'ping_vnfd/cp0', 'member-vnf-index-ref': '1', 'vnfd-id-ref': 'rift_ping_vnf'}, {'vnfd-connection-point-ref': 'pong_vnfd/cp0', 'member-vnf-index-ref': '2', 'vnfd-id-ref': 'rift_pong_vnf'}], 'description': 'Management VL', 'vendor': 'RIFT.io', 'type': 'ELAN', 'id': 'mgmt'}, {'ip-profile-ref': 'InterVNFLink', 'name': 'data_vl', 'short-name': 'data_vl', 'version': '1.0', 'vnfd-connection-point-ref': [{'vnfd-connection-point-ref': 'ping_vnfd/cp1', 'member-vnf-index-ref': '1', 'vnfd-id-ref': 'rift_ping_vnf'}, {'vnfd-connection-point-ref': 'pong_vnfd/cp1', 'member-vnf-index-ref': '2', 'vnfd-id-ref': 'rift_pong_vnf'}], 'description': 'Data VL', 'vendor': 'RIFT.io', 'type': 'ELAN', 'id': 'ping_pong_vl1'}], 'constituent-vnfd': [{'member-vnf-index': '1', 'vnfd-id-ref': 'rift_ping_vnf'}, {'member-vnf-index': '2', 'vnfd-id-ref': 'rift_pong_vnf'}], '_id': 'd9bc2e64-dfec-4c36-a8bb-c4667e8d7a53', 'vendor': 'RIFT.io', 'id': 'rift_ping_pong_ns', 'initial-service-primitive': [{'parameter': [{'name': 'port', 'value': 5555}, {'name': 'ssh-username', 'value': 'fedora'}, {'name': 'ssh-password', 'value': 'fedora'}], 'name': 'start traffic', 'seq': '1', 'user-defined-script': 'start_traffic.py'}]}
-
-
-VNFD_PING_DICT = {'name': 'ping_vnf', 'short-name': 'ping_vnf', 'mgmt-interface': {'port': '18888', 'cp': 'ping_vnfd/cp0', 'dashboard-params': {'port': '18888', 'path': 'api/v1/ping/stats'}}, 'description': 'This is an example RIFT.ware VNF', 'connection-point': [{'type': 'VPORT', 'name': 'ping_vnfd/cp0'}, {'type': 'VPORT', 'name': 'ping_vnfd/cp1'}], '_admin': {'storage': {'folder': '9ad8de93-cfcc-4da9-9795-d7dc5fec184e', 'fs': 'local', 'file': 'ping_vnf', 'path': '/app/storage/', 'tarfile': 'pkg'}, 'created': 1521127972.1878572, 'modified': 1521127972.1878572, 'projects_write': ['admin'], 'projects_read': ['admin']}, 'vnf-configuration': {'initial-config-primitive': [{'parameter': [{'name': 'ssh-hostname', 'value': '<rw_mgmt_ip>'}, {'name': 'ssh-username', 'value': 'ubuntu'}, {'name': 'ssh-password', 'value': 'ubuntu'}, {'name': 'mode', 'value': 'ping'}], 'name': 'config', 'seq': '1'}, {'name': 'start', 'seq': '2'}], 'juju': {'charm': 'pingpong'}, 'config-primitive': [{'name': 'start'}, {'name': 'stop'}, {'name': 'restart'}, {'parameter': [{'data-type': 'STRING', 'name': 'ssh-hostname', 'default-value': '<rw_mgmt_ip>'}, {'data-type': 'STRING', 'name': 'ssh-username', 'default-value': 'ubuntu'}, {'data-type': 'STRING', 'name': 'ssh-password', 'default-value': 'ubuntu'}, {'data-type': 'STRING', 'name': 'ssh-private-key'}, {'data-type': 'STRING', 'name': 'mode', 'read-only': 'true', 'default-value': 'ping'}], 'name': 'config'}, {'parameter': [{'data-type': 'STRING', 'name': 'server-ip'}, {'data-type': 'INTEGER', 'name': 'server-port'}], 'name': 'set-server'}, {'parameter': [{'data-type': 'INTEGER', 'name': 'rate', 'default-value': '5'}], 'name': 'set-rate'}, {'name': 'start-traffic'}, {'name': 'stop-traffic'}]}, 'placement-groups': [{'member-vdus': [{'member-vdu-ref': 'iovdu_0'}], 'name': 'Eris', 'strategy': 'COLOCATION', 'requirement': 'Place this VM on the Kuiper belt object Eris'}], 'http-endpoint': [{'port': '18888', 'path': 'api/v1/ping/stats'}], 'version': '1.1', 'logo': 'rift_logo.png', 'vdu': [{'vm-flavor': {'vcpu-count': '1', 'storage-gb': '20', 'memory-mb': '2048'}, 'count': '1', 'interface': [{'type': 'EXTERNAL', 'name': 'eth0', 'position': 0, 'virtual-interface': {'type': 'VIRTIO'}, 'external-connection-point-ref': 'ping_vnfd/cp0'}, {'type': 'EXTERNAL', 'name': 'eth1', 'position': 1, 'virtual-interface': {'type': 'VIRTIO'}, 'external-connection-point-ref': 'ping_vnfd/cp1'}], 'name': 'iovdu_0', 'image': 'xenial', 'id': 'iovdu_0', 'cloud-init-file': 'ping_cloud_init.cfg'}], '_id': '9ad8de93-cfcc-4da9-9795-d7dc5fec184e', 'vendor': 'RIFT.io', 'id': 'rift_ping_vnf'}
-
-VNFD_PONG_DICT = {'name': 'pong_vnf', 'short-name': 'pong_vnf', 'mgmt-interface': {'port': '18888', 'cp': 'pong_vnfd/cp0', 'dashboard-params': {'port': '18888', 'path': 'api/v1/pong/stats'}}, 'description': 'This is an example RIFT.ware VNF', 'connection-point': [{'type': 'VPORT', 'name': 'pong_vnfd/cp0'}, {'type': 'VPORT', 'name': 'pong_vnfd/cp1'}], '_admin': {'storage': {'folder': '9ad8de93-cfcc-4da9-9795-d7dc5fec184e', 'fs': 'local', 'file': 'pong_vnf', 'path': '/app/storage/', 'tarfile': 'pkg'}, 'created': 1521127972.1878572, 'modified': 1521127972.1878572, 'projects_write': ['admin'], 'projects_read': ['admin']}, 'vnf-configuration': {'initial-config-primitive': [{'parameter': [{'name': 'ssh-hostname', 'value': '<rw_mgmt_ip>'}, {'name': 'ssh-username', 'value': 'ubuntu'}, {'name': 'ssh-password', 'value': 'ubuntu'}, {'name': 'mode', 'value': 'pong'}], 'name': 'config', 'seq': '1'}, {'name': 'start', 'seq': '2'}], 'juju': {'charm': 'pingpong'}, 'config-primitive': [{'name': 'start'}, {'name': 'stop'}, {'name': 'restart'}, {'parameter': [{'data-type': 'STRING', 'name': 'ssh-hostname', 'default-value': '<rw_mgmt_ip>'}, {'data-type': 'STRING', 'name': 'ssh-username', 'default-value': 'ubuntu'}, {'data-type': 'STRING', 'name': 'ssh-password', 'default-value': 'ubuntu'}, {'data-type': 'STRING', 'name': 'ssh-private-key'}, {'data-type': 'STRING', 'name': 'mode', 'read-only': 'true', 'default-value': 'pong'}], 'name': 'config'}, {'parameter': [{'data-type': 'STRING', 'name': 'server-ip'}, {'data-type': 'INTEGER', 'name': 'server-port'}], 'name': 'set-server'}, {'parameter': [{'data-type': 'INTEGER', 'name': 'rate', 'default-value': '5'}], 'name': 'set-rate'}, {'name': 'start-traffic'}, {'name': 'stop-traffic'}]}, 'placement-groups': [{'member-vdus': [{'member-vdu-ref': 'iovdu_0'}], 'name': 'Eris', 'strategy': 'COLOCATION', 'requirement': 'Place this VM on the Kuiper belt object Eris'}], 'http-endpoint': [{'port': '18888', 'path': 'api/v1/pong/stats'}], 'version': '1.1', 'logo': 'rift_logo.png', 'vdu': [{'vm-flavor': {'vcpu-count': '1', 'storage-gb': '20', 'memory-mb': '2048'}, 'count': '1', 'interface': [{'type': 'EXTERNAL', 'name': 'eth0', 'position': 0, 'virtual-interface': {'type': 'VIRTIO'}, 'external-connection-point-ref': 'ping_vnfd/cp0'}, {'type': 'EXTERNAL', 'name': 'eth1', 'position': 1, 'virtual-interface': {'type': 'VIRTIO'}, 'external-connection-point-ref': 'ping_vnfd/cp1'}], 'name': 'iovdu_0', 'image': 'xenial', 'id': 'iovdu_0', 'cloud-init-file': 'pong_cloud_init.cfg'}], '_id': '9ad8de93-cfcc-4da9-9795-d7dc5fec184e', 'vendor': 'RIFT.io', 'id': 'rift_pong_vnf'}
-
-
 class PythonTest(unittest.TestCase):
     n2vc = None
 
 class PythonTest(unittest.TestCase):
     n2vc = None
 
@@ -222,8 +190,10 @@ class PythonTest(unittest.TestCase):
         self.log = logging.getLogger()
         self.log.level = logging.DEBUG
 
         self.log = logging.getLogger()
         self.log.level = logging.DEBUG
 
-        self.loop = asyncio.new_event_loop()
-        asyncio.set_event_loop(None)
+        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')
 
         # Extract parameters from the environment in order to run our test
         vca_host = os.getenv('VCA_HOST', '127.0.0.1')
@@ -243,38 +213,20 @@ class PythonTest(unittest.TestCase):
     def tearDown(self):
         self.loop.run_until_complete(self.n2vc.logout())
 
     def tearDown(self):
         self.loop.run_until_complete(self.n2vc.logout())
 
-    def get_vnf_descriptor(self, descriptor):
-        vnfd = vnfd_catalog.vnfd()
-        try:
-            data = yaml.load(descriptor)
-            pybindJSONDecoder.load_ietf_json(data, None, None, obj=vnfd)
-        except ValueError:
-            assert False
-        return vnfd
-
-    def get_ns_descriptor(self, descriptor):
-        nsd = nsd_catalog.nsd()
+    def get_descriptor(self, descriptor):
+        desc = None
         try:
         try:
-            data = yaml.load(descriptor)
-            pybindJSONDecoder.load_ietf_json(data, None, None, obj=nsd)
+            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
         except ValueError:
             assert False
-        return nsd
-
-    # def test_descriptor(self):
-    #     """Test loading and parsing a descriptor."""
-    #     nsd = self.get_ns_descriptor(NSD_YAML)
-    #     vnfd = self.get_vnf_descriptor(VNFD_VCA_YAML)
-    #     if vnfd and nsd:
-    #         pass
-
-    # def test_yang_to_dict(self):
-    #     # Test the conversion of the native object returned by pybind to a dict
-    #     # Extract parameters from the environment in order to run our test
-    #
-    #     nsd = self.get_ns_descriptor(NSD_YAML)
-    #     new = yang_to_dict(nsd)
-    #     self.assertEqual(NSD_DICT, new)
+        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
 
     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
@@ -301,56 +253,90 @@ class PythonTest(unittest.TestCase):
             elif workload_status in ["active"]:
                 self.log.debug("Removing charm")
                 task = asyncio.ensure_future(
             elif workload_status in ["active"]:
                 self.log.debug("Removing charm")
                 task = asyncio.ensure_future(
-                    self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback, model_name, application_name)
+                    self.n2vc.RemoveCharms(model_name, application_name, self.n2vc_callback)
                 )
                 task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None))
                 )
                 task.add_done_callback(functools.partial(self.n2vc_callback, None, None, None))
-        elif task:
-            if task.done():
-                self.loop.stop()
 
     def test_deploy_application(self):
         stream_handler = logging.StreamHandler(sys.stdout)
         self.log.addHandler(stream_handler)
         try:
             self.log.info("Log handler installed")
 
     def test_deploy_application(self):
         stream_handler = logging.StreamHandler(sys.stdout)
         self.log.addHandler(stream_handler)
         try:
             self.log.info("Log handler installed")
-            nsd = NSD_DICT
-            vnfd_ping = VNFD_PING_DICT
-            vnfd_pong = VNFD_PONG_DICT
-            if nsd and vnfd_ping and vnfd_pong:
+            nsd = self.get_descriptor(NSD_YAML)
+            vnfd = self.get_descriptor(VNFD_YAML)
+
+            if nsd and vnfd:
+
                 vca_charms = os.getenv('VCA_CHARMS', None)
 
                 vca_charms = os.getenv('VCA_CHARMS', None)
 
-                config = vnfd_ping['vnf-configuration']
-                self.assertIsNotNone(config)
+                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'
 
 
-                juju = config['juju']
-                self.assertIsNotNone(juju)
+                    # self.loop.run_until_complete(n.CreateNetworkService(nsd))
+                    ns_name = "default"
 
 
-                charm = juju['charm']
-                self.assertIsNotNone(charm)
+                    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)
 
 
-                charm_dir = "{}/{}".format(vca_charms, charm)
-                # n.callback_status = self.callback_status
+                        params['initial-config-primitive'] = vdu_config['initial-config-primitive']
 
 
-                # 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': '127.0.0.1'
-                }
+                        deploy()
+                        vnf_index += 1
 
 
-                # self.loop.run_until_complete(n.CreateNetworkService(nsd))
-                # task = asyncio.ensure_future(n.DeployCharms(vnfd, nsd=nsd, artifacts=charm_dir))
-                # task = asyncio.ensure_future(n.DeployCharms(vnfd, nsd=nsd, artifacts=charm_dir))
-                ns_name = "default"
+                # Check if this VNF has a charm
+                vnf_config = vnfd.get("vnf-configuration")
+                if vnf_config:
+                    juju = vnf_config['juju']
+                    self.assertIsNotNone(juju)
 
 
-                ping_vnf_name = self.n2vc.FormatApplicationName(ns_name, vnfd_ping['name'])
-                pong_vnf_name = self.n2vc.FormatApplicationName(ns_name, vnfd_pong['name'])
+                    charm = juju['charm']
+                    self.assertIsNotNone(charm)
+
+                    params['initial-config-primitive'] = vnf_config['initial-config-primitive']
+
+                    deploy()
+                    vnf_index += 1
 
 
-                self.loop.run_until_complete(self.n2vc.DeployCharms(ns_name, ping_vnf_name, vnfd_ping, charm_dir, params, {}, self.n2vc_callback))
-                self.loop.run_until_complete(self.n2vc.DeployCharms(ns_name, pong_vnf_name, vnfd_pong, charm_dir, params, {}, self.n2vc_callback))
                 self.loop.run_forever()
 
                 # self.loop.run_until_complete(n.GetMetrics(vnfd, nsd=nsd))
                 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):
 
                 # Test actions
                 #  ExecutePrimitive(self, nsd, vnfd, vnf_member_index, primitive, callback, *callback_args, **params):
 
@@ -359,76 +345,3 @@ class PythonTest(unittest.TestCase):
                 # self.loop.run_until_complete(self.n2vc.logout())
         finally:
             self.log.removeHandler(stream_handler)
                 # self.loop.run_until_complete(self.n2vc.logout())
         finally:
             self.log.removeHandler(stream_handler)
-
-    # def test_deploy_application(self):
-    #
-    #     nsd = self.get_ns_descriptor(NSD_YAML)
-    #     vnfd = self.get_vnf_descriptor(VNFD_VCA_YAML)
-    #     if nsd and vnfd:
-    #         # 1) Test that we're parsing the data correctly
-    #         for vnfd_yang in vnfd.vnfd_catalog.vnfd.itervalues():
-    #             vnfd_rec = vnfd_yang.get()
-    #             # Each vnfd may have a charm
-    #             # print(vnfd)
-    #             config = vnfd_rec.get('vnf-configuration')
-    #             self.assertIsNotNone(config)
-    #
-    #             juju = config.get('juju')
-    #             self.assertIsNotNone(juju)
-    #
-    #             charm = juju.get('charm')
-    #             self.assertIsNotNone(charm)
-    #
-    #         # 2) Exercise the data by deploying the charms
-    #
-    #         # 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)
-    #         # n = N2VC(
-    #         #     server='10.195.8.254',
-    #         #     port=17070,
-    #         #     user='admin',
-    #         #     secret='74e7aa0cc9cb294de3af294bd76b4604'
-    #         # )
-    #         n = N2VC(
-    #             server=vca_host,
-    #             port=vca_port,
-    #             user=vca_user,
-    #             secret=vca_secret,
-    #             artifacts=vca_charms,
-    #         )
-    #
-    #         n.callback_status = self.callback_status
-    #
-    #         self.loop.run_until_complete(n.CreateNetworkService(nsd))
-    #         self.loop.run_until_complete(n.DeployCharms(vnfd, nsd=nsd))
-    #         # self.loop.run_until_complete(n.GetMetrics(vnfd, nsd=nsd))
-    #
-    #         # self.loop.run_until_complete(n.RemoveCharms(nsd, vnfd))
-    #         self.loop.run_until_complete(n.DestroyNetworkService(nsd))
-    #
-    #         self.loop.run_until_complete(n.logout())
-    #         n = None
-
-    def callback_status(self, nsd, vnfd, workload_status):
-        """An example callback.
-
-        This is an example of how a client using N2VC can receive periodic
-        updates on the status of a vnfd
-        """
-        # print(nsd)
-        # print(vnfd)
-        print("Workload status: {}".format(workload_status))
-
-    def test_deploy_multivdu_application(self):
-        """Deploy a multi-vdu vnf that uses multiple charms."""
-        pass
-
-    def test_remove_application(self):
-        pass
-
-    def test_get_metrics(self):
-        pass