2.0 beta NFVBENCH-91 Allow multi-chaining with separate edge networks
[nfvbench.git] / nfvbench / chaining.py
diff --git a/nfvbench/chaining.py b/nfvbench/chaining.py
new file mode 100644 (file)
index 0000000..e5a9f0a
--- /dev/null
@@ -0,0 +1,988 @@
+#!/usr/bin/env python
+# Copyright 2018 Cisco Systems, Inc.  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.
+#
+
+# This module takes care of chaining networks, ports and vms
+#
+"""NFVBENCH CHAIN DISCOVERY/STAGING.
+
+This module takes care of staging/discovering all resources that are participating in a
+benchmarking session: flavors, networks, ports, VNF instances.
+If a resource is discovered with the same name, it will be reused.
+Otherwise it will be created.
+
+ChainManager: manages VM image, flavor, the staging discovery of all chains
+              has 1 or more chains
+Chain: manages one chain, has 2 or more networks and 1 or more instances
+ChainNetwork: manages 1 network in a chain
+ChainVnf: manages 1 VNF instance in a chain, has 2 ports
+ChainVnfPort: manages 1 instance port
+
+ChainManager-->Chain(*)
+Chain-->ChainNetwork(*),ChainVnf(*)
+ChainVnf-->ChainVnfPort(2)
+
+Once created/discovered, instances are checked to be in the active state (ready to pass traffic)
+Configuration parameters that will influence how these resources are staged/related:
+- openstack or no openstack
+- chain type
+- number of chains
+- number of VNF in each chain (PVP, PVVP)
+- SRIOV and middle port SRIOV for port types
+- whether networks are shared across chains or not
+
+There is not traffic generation involved in this module.
+"""
+import os
+import re
+import time
+
+from glanceclient.v2 import client as glanceclient
+from neutronclient.neutron import client as neutronclient
+from novaclient.client import Client
+
+from attrdict import AttrDict
+import compute
+from log import LOG
+from specs import ChainType
+
+# Left and right index for network and port lists
+LEFT = 0
+RIGHT = 1
+# Name of the VM config file
+NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
+# full pathame of the VM config in the VM
+NFVBENCH_CFG_VM_PATHNAME = os.path.join('/etc/', NFVBENCH_CFG_FILENAME)
+# full path of the boot shell script template file on the server where nfvbench runs
+BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                    'nfvbenchvm',
+                                    NFVBENCH_CFG_FILENAME)
+
+
+class ChainException(Exception):
+    """Exception while operating the chains."""
+
+    pass
+
+class NetworkEncaps(object):
+    """Network encapsulation."""
+
+
+class ChainFlavor(object):
+    """Class to manage the chain flavor."""
+
+    def __init__(self, flavor_name, flavor_dict, comp):
+        """Create a flavor."""
+        self.name = flavor_name
+        self.comp = comp
+        self.flavor = self.comp.find_flavor(flavor_name)
+        self.reuse = False
+        if self.flavor:
+            self.reuse = True
+            LOG.info("Reused flavor '%s'", flavor_name)
+        else:
+            extra_specs = flavor_dict.pop('extra_specs', None)
+
+            self.flavor = comp.create_flavor(flavor_name,
+                                             **flavor_dict)
+
+            LOG.info("Created flavor '%s'", flavor_name)
+            if extra_specs:
+                self.flavor.set_keys(extra_specs)
+
+    def delete(self):
+        """Delete this flavor."""
+        if not self.reuse and self.flavor:
+            self.flavor.delete()
+            LOG.info("Flavor '%s' deleted", self.name)
+
+
+class ChainVnfPort(object):
+    """A port associated to one VNF in the chain."""
+
+    def __init__(self, name, vnf, chain_network, vnic_type):
+        """Create or reuse a port on a given network.
+
+        if vnf.instance is None the VNF instance is not reused and this ChainVnfPort instance must
+        create a new port.
+        Otherwise vnf.instance is a reused VNF instance and this ChainVnfPort instance must
+        find an existing port to reuse that matches the port requirements: same attached network,
+        instance, name, vnic type
+
+        name: name for this port
+        vnf: ChainVNf instance that owns this port
+        chain_network: ChainNetwork instance where this port should attach
+        vnic_type: required vnic type for this port
+        """
+        self.name = name
+        self.vnf = vnf
+        self.manager = vnf.manager
+        self.reuse = False
+        self.port = None
+        if vnf.instance:
+            # VNF instance is reused, we need to find an existing port that matches this instance
+            # and network
+            # discover ports attached to this instance
+            port_list = self.manager.get_ports_from_network(chain_network)
+            for port in port_list:
+                if port['name'] != name:
+                    continue
+                if port['binding:vnic_type'] != vnic_type:
+                    continue
+                if port['device_id'] == vnf.get_uuid():
+                    self.port = port
+                    LOG.info('Reusing existing port %s mac=%s', name, port['mac_address'])
+                    break
+            else:
+                raise ChainException('Cannot find matching port')
+        else:
+            # VNF instance is not created yet, we need to create a new port
+            body = {
+                "port": {
+                    'name': name,
+                    'network_id': chain_network.get_uuid(),
+                    'binding:vnic_type': vnic_type
+                }
+            }
+            port = self.manager.neutron_client.create_port(body)
+            self.port = port['port']
+            LOG.info('Created port %s', name)
+            try:
+                self.manager.neutron_client.update_port(self.port['id'], {
+                    'port': {
+                        'security_groups': [],
+                        'port_security_enabled': False,
+                    }
+                })
+                LOG.info('Security disabled on port %s', name)
+            except Exception:
+                LOG.info('Failed to disable security on port %s (ignored)', name)
+
+    def get_mac(self):
+        """Get the MAC address for this port."""
+        return self.port['mac_address']
+
+    def delete(self):
+        """Delete this port instance."""
+        if self.reuse or not self.port:
+            return
+        retry = 0
+        while retry < self.manager.config.generic_retry_count:
+            try:
+                self.manager.neutron_client.delete_port(self.port['id'])
+                LOG.info("Deleted port %s", self.name)
+                return
+            except Exception:
+                retry += 1
+                time.sleep(self.manager.config.generic_poll_sec)
+        LOG.error('Unable to delete port: %s', self.name)
+
+
+class ChainNetwork(object):
+    """Could be a shared network across all chains or a chain private network."""
+
+    def __init__(self, manager, network_config, chain_id=None, lookup_only=False):
+        """Create a network for given chain."""
+        self.manager = manager
+        self.name = network_config.name
+        if chain_id is not None:
+            self.name += str(chain_id)
+        self.reuse = False
+        self.network = None
+        self.vlan = None
+        try:
+            self._setup(network_config, lookup_only)
+        except Exception:
+            if lookup_only:
+                LOG.error("Cannot find network %s", self.name)
+            else:
+                LOG.error("Error creating network %s", self.name)
+            self.delete()
+            raise
+
+    def _setup(self, network_config, lookup_only):
+        # Lookup if there is a matching network with same name
+        networks = self.manager.neutron_client.list_networks(name=self.name)
+        if networks['networks']:
+            network = networks['networks'][0]
+            # a network of same name already exists, we need to verify it has the same
+            # characteristics
+            if network_config.segmentation_id:
+                if network['provider:segmentation_id'] != network_config.segmentation_id:
+                    raise ChainException("Mismatch of 'segmentation_id' for reused "
+                                         "network '{net}'. Network has id '{seg_id1}', "
+                                         "configuration requires '{seg_id2}'."
+                                         .format(net=self.name,
+                                                 seg_id1=network['provider:segmentation_id'],
+                                                 seg_id2=network_config.segmentation_id))
+
+            if network_config.physical_network:
+                if network['provider:physical_network'] != network_config.physical_network:
+                    raise ChainException("Mismatch of 'physical_network' for reused "
+                                         "network '{net}'. Network has '{phys1}', "
+                                         "configuration requires '{phys2}'."
+                                         .format(net=self.name,
+                                                 phys1=network['provider:physical_network'],
+                                                 phys2=network_config.physical_network))
+
+            LOG.info('Reusing existing network %s', self.name)
+            self.reuse = True
+            self.network = network
+        else:
+            if lookup_only:
+                raise ChainException('Network %s not found' % self.name)
+            body = {
+                'network': {
+                    'name': self.name,
+                    'admin_state_up': True
+                    }
+            }
+            if network_config.network_type:
+                body['network']['provider:network_type'] = network_config.network_type
+                if network_config.segmentation_id:
+                    body['network']['provider:segmentation_id'] = network_config.segmentation_id
+                if network_config.physical_network:
+                    body['network']['provider:physical_network'] = network_config.physical_network
+
+            self.network = self.manager.neutron_client.create_network(body)['network']
+            body = {
+                'subnet': {'name': network_config.subnet,
+                           'cidr': network_config.cidr,
+                           'network_id': self.network['id'],
+                           'enable_dhcp': False,
+                           'ip_version': 4,
+                           'dns_nameservers': []}
+            }
+            subnet = self.manager.neutron_client.create_subnet(body)['subnet']
+            # add subnet id to the network dict since it has just been added
+            self.network['subnets'] = [subnet['id']]
+            LOG.info('Created network: %s.', self.name)
+
+    def get_uuid(self):
+        """
+        Extract UUID of this network.
+
+        :return: UUID of this network
+        """
+        return self.network['id']
+
+    def get_vlan(self):
+        """
+        Extract vlan for this network.
+
+        :return: vlan ID for this network
+        """
+        if self.network['provider:network_type'] != 'vlan':
+            raise ChainException('Trying to retrieve VLAN id for non VLAN network')
+        return self.network['provider:segmentation_id']
+
+    def delete(self):
+        """Delete this network."""
+        if not self.reuse and self.network:
+            retry = 0
+            while retry < self.manager.config.generic_retry_count:
+                try:
+                    self.manager.neutron_client.delete_network(self.network['id'])
+                    LOG.info("Deleted network: %s", self.name)
+                    return
+                except Exception:
+                    retry += 1
+                    LOG.info('Error deleting network %s (retry %d/%d)...',
+                             self.name,
+                             retry,
+                             self.manager.config.generic_retry_count)
+                    time.sleep(self.manager.config.generic_poll_sec)
+            LOG.error('Unable to delete network: %s', self.name)
+
+
+class ChainVnf(object):
+    """A class to represent a VNF in a chain."""
+
+    def __init__(self, chain, vnf_id, networks):
+        """Reuse a VNF instance with same characteristics or create a new VNF instance.
+
+        chain: the chain where this vnf belongs
+        vnf_id: indicates the index of this vnf in its chain (first vnf=0)
+        networks: the list of all networks (ChainNetwork) of the current chain
+        """
+        self.manager = chain.manager
+        self.chain = chain
+        self.vnf_id = vnf_id
+        self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
+        if len(networks) > 2:
+            # we will have more than 1 VM in each chain
+            self.name += '-' + str(vnf_id)
+        self.ports = []
+        self.status = None
+        self.instance = None
+        self.reuse = False
+        self.host_ip = None
+        try:
+            # the vnf_id is conveniently also the starting index in networks
+            # for the left and right networks associated to this VNF
+            self._setup(networks[vnf_id:vnf_id + 2])
+        except Exception:
+            LOG.error("Error creating VNF %s", self.name)
+            self.delete()
+            raise
+
+    def _get_vm_config(self, remote_mac_pair):
+        config = self.manager.config
+        devices = self.manager.generator_config.devices
+        with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
+            content = boot_script.read()
+        g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
+        g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
+        vm_config = {
+            'forwarder': config.vm_forwarder,
+            'intf_mac1': self.ports[LEFT].get_mac(),
+            'intf_mac2': self.ports[RIGHT].get_mac(),
+            'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
+            'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
+            'tg_net1': devices[LEFT].ip_addrs,
+            'tg_net2': devices[RIGHT].ip_addrs,
+            'vnf_gateway1_cidr': g1cidr,
+            'vnf_gateway2_cidr': g2cidr,
+            'tg_mac1': remote_mac_pair[0],
+            'tg_mac2': remote_mac_pair[1]
+        }
+        return content.format(**vm_config)
+
+    def _get_vnic_type(self, port_index):
+        """Get the right vnic type for given port indexself.
+
+        If SR-IOV is speficied, middle ports in multi-VNF chains
+        can use vswitch or SR-IOV based on config.use_sriov_middle_net
+        """
+        if self.manager.config.sriov:
+            if self.manager.config.use_sriov_middle_net:
+                return 'direct'
+            if self.vnf_id == 0:
+                # first VNF in chain must use sriov for left port
+                if port_index == 0:
+                    return 'direct'
+            elif (self.vnf_id == self.chain.get_length() - 1) and (port_index == 1):
+                # last VNF in chain must use sriov for right port
+                return 'direct'
+        return 'normal'
+
+    def _setup(self, networks):
+        flavor_id = self.manager.flavor.flavor.id
+        # Check if we can reuse an instance with same name
+        for instance in self.manager.existing_instances:
+            if instance.name == self.name:
+                # Verify that other instance characteristics match
+                if instance.flavor['id'] != flavor_id:
+                    self._reuse_exception('Flavor mismatch')
+                if instance.status != "ACTIVE":
+                    self._reuse_exception('Matching instance is not in ACTIVE state')
+                # The 2 networks for this instance must also be reused
+                if not networks[LEFT].reuse:
+                    self._reuse_exception('network %s is new' % networks[LEFT].name)
+                if not networks[RIGHT].reuse:
+                    self._reuse_exception('network %s is new' % networks[RIGHT].name)
+                # instance.networks have the network names as keys:
+                # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
+                if networks[LEFT].name not in instance.networks:
+                    self._reuse_exception('Left network mismatch')
+                if networks[RIGHT].name not in instance.networks:
+                    self._reuse_exception('Right network mismatch')
+                # Other checks not performed (yet)
+                # check if az and compute node match
+                self.reuse = True
+                self.instance = instance
+                LOG.info('Reusing existing instance %s on %s',
+                         self.name, self.get_hypervisor_name())
+        # create or reuse/discover 2 ports per instance
+        self.ports = [ChainVnfPort(self.name + '-' + str(index),
+                                   self,
+                                   networks[index],
+                                   self._get_vnic_type(index)) for index in [0, 1]]
+        # if no reuse, actual vm creation is deferred after all ports in the chain are created
+        # since we need to know the next mac in a multi-vnf chain
+
+    def get_az(self):
+        """Get the AZ associated to this VNF."""
+        return self.manager.az[0]
+
+    def create_vnf(self, remote_mac_pair):
+        """Create the VNF instance if it does not already exist."""
+        if self.instance is None:
+            port_ids = [{'port-id': vnf_port.port['id']}
+                        for vnf_port in self.ports]
+            vm_config = self._get_vm_config(remote_mac_pair)
+            az = self.get_az()
+            server = self.manager.comp.create_server(self.name,
+                                                     self.manager.image_instance,
+                                                     self.manager.flavor.flavor,
+                                                     None,
+                                                     port_ids,
+                                                     None,
+                                                     avail_zone=az,
+                                                     user_data=None,
+                                                     config_drive=True,
+                                                     files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
+            if server:
+                LOG.info('Created instance %s on %s', self.name, az)
+                self.instance = server
+                self.reuse = False
+            else:
+                raise ChainException('Unable to create instance: %s' % (self.name))
+
+    def _reuse_exception(self, reason):
+        raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
+
+    def get_status(self):
+        """Get the statis of this instance."""
+        if self.instance.status != 'ACTIVE':
+            self.instance = self.manager.comp.poll_server(self.instance)
+        return self.instance.status
+
+    def get_hostname(self):
+        """Get the hypervisor host name running this VNF instance."""
+        return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
+
+    def get_host_ip(self):
+        """Get the IP address of the host where this instance runs.
+
+        return: the IP address
+        """
+        if not self.host_ip:
+            self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
+        return self.host_ip
+
+    def get_hypervisor_name(self):
+        """Get hypervisor name (az:hostname) for this VNF instance."""
+        if self.instance:
+            az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
+            hostname = self.get_hostname()
+            if az:
+                return az + ':' + hostname
+            return hostname
+        return None
+
+    def get_uuid(self):
+        """Get the uuid for this instance."""
+        return self.instance.id
+
+    def delete(self, forced=False):
+        """Delete this VNF instance."""
+        if self.reuse:
+            LOG.info("Instance %s not deleted (reused)", self.name)
+        else:
+            if self.instance:
+                self.manager.comp.delete_server(self.instance)
+                LOG.info("Deleted instance %s", self.name)
+            for port in self.ports:
+                port.delete()
+
+class Chain(object):
+    """A class to manage a single chain.
+
+    Can handle any type of chain (EXT, PVP, PVVP)
+    """
+
+    def __init__(self, chain_id, manager):
+        """Create a new chain.
+
+        chain_id: chain index (first chain is 0)
+        manager: the chain manager that owns all chains
+        """
+        self.chain_id = chain_id
+        self.manager = manager
+        self.encaps = manager.encaps
+        self.networks = []
+        self.instances = []
+        try:
+            self.networks = manager.get_networks(chain_id)
+            # For external chain VNFs can only be discovered from their MAC addresses
+            # either from config or from ARP
+            if manager.config.service_chain != ChainType.EXT:
+                for chain_instance_index in range(self.get_length()):
+                    self.instances.append(ChainVnf(self,
+                                                   chain_instance_index,
+                                                   self.networks))
+                # now that all VNF ports are created we need to calculate the
+                # left/right remote MAC for each VNF in the chain
+                # before actually creating the VNF itself
+                rem_mac_pairs = self._get_remote_mac_pairs()
+                for instance in self.instances:
+                    rem_mac_pair = rem_mac_pairs.pop(0)
+                    instance.create_vnf(rem_mac_pair)
+        except Exception:
+            self.delete()
+            raise
+
+    def get_length(self):
+        """Get the number of VNF in the chain."""
+        return len(self.networks) - 1
+
+    def _get_remote_mac_pairs(self):
+        """Get the list of remote mac pairs for every VNF in the chain.
+
+        Traverse the chain from left to right and establish the
+        left/right remote MAC for each VNF in the chainself.
+
+        PVP case is simpler:
+        mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
+        must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
+        the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
+
+        PVVP:
+        tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
+        Must produce the following list:
+        [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
+
+        General case with 3 VMs in chain, the list of consecutive macs (left to right):
+        tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
+        Must produce the following list:
+        [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
+        or index: [[0, 3], [2, 5], [4, 7]]
+
+        The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
+        """
+        # line up all mac from left to right
+        mac_seq = [self.manager.generator_config.devices[LEFT].mac]
+        for instance in self.instances:
+            mac_seq.append(instance.ports[0].get_mac())
+            mac_seq.append(instance.ports[1].get_mac())
+        mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
+        base = 0
+        rem_mac_pairs = []
+        for _ in self.instances:
+            rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
+            base += 2
+        return rem_mac_pairs
+
+    def get_instances(self):
+        """Return all instances for this chain."""
+        return self.instances
+
+    def get_vlan(self, port_index):
+        """Get the VLAN id on a given port.
+
+        port_index: left port is 0, right port is 1
+        return: the vlan_id or None if there is no vlan tagging
+        """
+        # for port 1 we need to return the VLAN of the last network in the chain
+        # The networks array contains 2 networks for PVP [left, right]
+        # and 3 networks in the case of PVVP [left.middle,right]
+        if port_index:
+            # this will pick the last item in array
+            port_index = -1
+        return self.networks[port_index].get_vlan()
+
+    def get_dest_mac(self, port_index):
+        """Get the dest MAC on a given port.
+
+        port_index: left port is 0, right port is 1
+        return: the dest MAC
+        """
+        if port_index:
+            # for right port, use the right port MAC of the last (right most) VNF In chain
+            return self.instances[-1].ports[1].get_mac()
+        # for left port use the left port MAC of the first (left most) VNF in chain
+        return self.instances[0].ports[0].get_mac()
+
+    def get_network_uuids(self):
+        """Get UUID of networks in this chain from left to right (order is important).
+
+        :return: list of UUIDs of networks (2 or 3 elements)
+        """
+        return [net['id'] for net in self.networks]
+
+    def get_host_ips(self):
+        """Return the IP adresss(es) of the host compute nodes used for this chain.
+
+        :return: a list of 1 or 2 IP addresses
+        """
+        return [vnf.get_host_ip() for vnf in self.instances]
+
+    def get_compute_nodes(self):
+        """Return the name of the host compute nodes used for this chain.
+
+        :return: a list of 1 host name in the az:host format
+        """
+        # Since all chains go through the same compute node(s) we can just retrieve the
+        # compute node name(s) for the first chain
+        return [vnf.get_hypervisor_name() for vnf in self.instances]
+
+    def delete(self):
+        """Delete this chain."""
+        for instance in self.instances:
+            instance.delete()
+        # only delete if these are chain private networks (not shared)
+        if not self.manager.config.service_chain_shared_net:
+            for network in self.networks:
+                network.delete()
+
+
+class ChainManager(object):
+    """A class for managing all chains for a given run.
+
+    Supports openstack or no openstack.
+    Supports EXT, PVP and PVVP chains.
+    """
+
+    def __init__(self, chain_runner):
+        """Create a chain manager to take care of discovering or bringing up the requested chains.
+
+        A new instance must be created every time a new config is used.
+        config: the nfvbench config to use
+        cred: openstack credentials to use of None if there is no openstack
+        """
+        self.chain_runner = chain_runner
+        self.config = chain_runner.config
+        self.generator_config = chain_runner.traffic_client.generator_config
+        self.chains = []
+        self.image_instance = None
+        self.image_name = None
+        # Left and right networks shared across all chains (only if shared)
+        self.networks = []
+        self.encaps = None
+        self.flavor = None
+        self.comp = None
+        self.nova_client = None
+        self.neutron_client = None
+        self.glance_client = None
+        self.existing_instances = []
+        # existing ports keyed by the network uuid they belong to
+        self._existing_ports = {}
+        config = self.config
+        self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
+        self.chain_count = config.service_chain_count
+        if self.openstack:
+            # openstack only
+            session = chain_runner.cred.get_session()
+            self.nova_client = Client(2, session=session)
+            self.neutron_client = neutronclient.Client('2.0', session=session)
+            self.glance_client = glanceclient.Client('2', session=session)
+            self.comp = compute.Compute(self.nova_client,
+                                        self.glance_client,
+                                        config)
+            self.az = None
+            try:
+                if config.service_chain != ChainType.EXT:
+                    # we need to find 1 hypervisor
+                    az_list = self.comp.get_enabled_az_host_list(1)
+                    if not az_list:
+                        raise ChainException('No matching hypervisor found')
+                    self.az = az_list
+                    self._setup_image()
+                    self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
+                    # Get list of all existing instances to check if some instances can be reused
+                    self.existing_instances = self.comp.get_server_list()
+                # If networks are shared across chains, get the list of networks
+                if config.service_chain_shared_net:
+                    self.networks = self.get_networks()
+                # Reuse/create chains
+                for chain_id in range(self.chain_count):
+                    self.chains.append(Chain(chain_id, self))
+                if config.service_chain == ChainType.EXT:
+                    # if EXT and no ARP we need to read dest MACs from config
+                    if config.no_arp:
+                        self._get_dest_macs_from_config()
+                else:
+                    # Make sure all instances are active before proceeding
+                    self._ensure_instances_active()
+            except Exception:
+                self.delete()
+                raise
+        else:
+            # no openstack, no need to create chains
+            # make sure there at least as many entries as chains in each left/right list
+            if len(config.vlans) != 2:
+                raise ChainException('The config vlans property must be a list '
+                                     'with 2 lists of VLAN IDs')
+            if not config.l2_loopback:
+                self._get_dest_macs_from_config()
+
+            re_vlan = "[0-9]*$"
+            self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
+                          self._check_list('vlans[1]', config.vlans[1], re_vlan)]
+
+    def _get_dest_macs_from_config(self):
+        re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
+        tg_config = self.config.traffic_generator
+        self.dest_macs = [self._check_list("mac_addrs_left",
+                                           tg_config.mac_addrs_left, re_mac),
+                          self._check_list("mac_addrs_right",
+                                           tg_config.mac_addrs_right, re_mac)]
+
+    def _check_list(self, list_name, ll, pattern):
+        # if it is a single int or mac, make it a list of 1 int
+        if isinstance(ll, (int, str)):
+            ll = [ll]
+        if not ll or len(ll) < self.chain_count:
+            raise ChainException('%s=%s must be a list with 1 element per chain' % (list_name, ll))
+        for item in ll:
+            if not re.match(pattern, str(item)):
+                raise ChainException("Invalid format '{item}' specified in {fname}"
+                                     .format(item=item, fname=list_name))
+        return ll
+
+    def _setup_image(self):
+        # To avoid reuploading image in server mode, check whether image_name is set or not
+        if self.image_name:
+            self.image_instance = self.comp.find_image(self.image_name)
+        if self.image_instance:
+            LOG.info("Reusing image %s", self.image_name)
+        else:
+            image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
+            if self.config.vm_image_file:
+                match = re.search(image_name_search_pattern, self.config.vm_image_file)
+                if match:
+                    self.image_name = match.group(1)
+                    LOG.info('Using provided VM image file %s', self.config.vm_image_file)
+                else:
+                    raise ChainException('Provided VM image file name %s must start with '
+                                         '"nfvbenchvm-<version>"' % self.config.vm_image_file)
+            else:
+                pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+                for f in os.listdir(pkg_root):
+                    if re.search(image_name_search_pattern, f):
+                        self.config.vm_image_file = pkg_root + '/' + f
+                        self.image_name = f.replace('.qcow2', '')
+                        LOG.info('Found built-in VM image file %s', f)
+                        break
+                else:
+                    raise ChainException('Cannot find any built-in VM image file.')
+            if self.image_name:
+                self.image_instance = self.comp.find_image(self.image_name)
+            if not self.image_instance:
+                LOG.info('Uploading %s', self.image_name)
+                res = self.comp.upload_image_via_url(self.image_name,
+                                                     self.config.vm_image_file)
+
+                if not res:
+                    raise ChainException('Error uploading image %s from %s. ABORTING.' %
+                                         (self.image_name, self.config.vm_image_file))
+                LOG.info('Image %s successfully uploaded.', self.image_name)
+                self.image_instance = self.comp.find_image(self.image_name)
+
+    def _ensure_instances_active(self):
+        instances = []
+        for chain in self.chains:
+            instances.extend(chain.get_instances())
+        initial_instance_count = len(instances)
+        max_retries = (self.config.check_traffic_time_sec +
+                       self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
+        retry = 0
+        while instances:
+            remaining_instances = []
+            for instance in instances:
+                status = instance.get_status()
+                if status == 'ACTIVE':
+                    continue
+                if status == 'ERROR':
+                    raise ChainException('Instance %s creation error: %s' %
+                                         (instance.name,
+                                          instance.instance.fault['message']))
+                remaining_instances.append(instance)
+            if not remaining_instances:
+                break
+            retry += 1
+            if retry >= max_retries:
+                raise ChainException('Time-out: %d/%d instances still not active' %
+                                     (len(remaining_instances), initial_instance_count))
+            LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
+                     len(remaining_instances), initial_instance_count,
+                     retry, max_retries)
+            instances = remaining_instances
+            time.sleep(self.config.generic_poll_sec)
+        if initial_instance_count:
+            LOG.info('All instances are active')
+
+    def get_networks(self, chain_id=None):
+        """Get the networks for given EXT, PVP or PVVP chain.
+
+        For EXT packet path, these networks must pre-exist.
+        For PVP, PVVP these networks will be created if they do not exist.
+        chain_id: to which chain the networks belong.
+                  a None value will mean that these networks are shared by all chains
+        """
+        if self.networks:
+            # the only case where self.networks exists is when the networks are shared
+            # across all chains
+            return self.networks
+        if self.config.service_chain == ChainType.EXT:
+            lookup_only = True
+            ext_net = self.config.external_networks
+            net_cfg = [AttrDict({'name': name,
+                                 'segmentation_id': None,
+                                 'physical_network': None})
+                       for name in [ext_net.left, ext_net.right]]
+        else:
+            lookup_only = False
+            int_nets = self.config.internal_networks
+            if self.config.service_chain == ChainType.PVP:
+                net_cfg = [int_nets.left, int_nets.right]
+            else:
+                net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
+        networks = []
+        try:
+            for cfg in net_cfg:
+                networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
+        except Exception:
+            # need to cleanup all successful networks prior to bailing out
+            for net in networks:
+                net.delete()
+            raise
+        return networks
+
+    def get_existing_ports(self):
+        """Get the list of existing ports.
+
+        Lazy retrieval of ports as this can be costly if there are lots of ports and
+        is only needed when VM and network are being reused.
+
+        return: a dict of list of neutron ports indexed by the network uuid they are attached to
+
+        Each port is a dict with fields such as below:
+        {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
+         'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
+         'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
+         'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
+         'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
+         'security_groups': [],
+         'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
+                                 'vhostuser_mode': 'server'},
+         'binding:vif_type': 'vhostuser',
+         'mac_address': 'fa:16:3e:3c:63:04',
+         'project_id': '977ac76a63d7492f927fa80e86baff4c',
+         'status': 'ACTIVE',
+         'binding:host_id': 'a20-champagne-compute-1',
+         'description': '',
+         'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
+         'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
+         'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
+         'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
+         'created_at': '2018-10-06T07:15:10Z',
+         'binding:vnic_type': 'normal'}
+        """
+        if not self._existing_ports:
+            LOG.info('Loading list of all ports...')
+            existing_ports = self.neutron_client.list_ports()['ports']
+            # place all ports in the dict keyed by the port network uuid
+            for port in existing_ports:
+                port_list = self._existing_ports.setdefault(port['network_id'], [])
+                port_list.append(port)
+            LOG.info("Loaded %d ports attached to %d networks",
+                     len(existing_ports), len(self._existing_ports))
+        return self._existing_ports
+
+    def get_ports_from_network(self, chain_network):
+        """Get the list of existing ports that belong to a network.
+
+        Lazy retrieval of ports as this can be costly if there are lots of ports and
+        is only needed when VM and network are being reused.
+
+        chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
+        return: list of neutron ports attached to requested network
+        """
+        return self.get_existing_ports().get(chain_network.get_uuid(), None)
+
+    def get_host_ip_from_mac(self, mac):
+        """Get the host IP address matching a MAC.
+
+        mac: MAC address to look for
+        return: the IP address of the host where the matching port runs or None if not found
+        """
+        # _existing_ports is a dict of list of ports indexed by network id
+        for port_list in self.get_existing_ports().values():
+            for port in port_list:
+                try:
+                    if port['mac_address'] == mac:
+                        host_id = port['binding:host_id']
+                        return self.comp.get_hypervisor(host_id).host_ip
+                except KeyError:
+                    pass
+        return None
+
+    def get_chain_vlans(self, port_index):
+        """Get the list of per chain VLAN id on a given port.
+
+        port_index: left port is 0, right port is 1
+        return: a VLAN ID list indexed by the chain index or None if no vlan tagging
+        """
+        if self.chains:
+            return [self.chains[chain_index].get_vlan(port_index)
+                    for chain_index in range(self.chain_count)]
+        # no openstack
+        return self.vlans[port_index]
+
+    def get_dest_macs(self, port_index):
+        """Get the list of per chain dest MACs on a given port.
+
+        Should not be called if EXT+ARP is used (in that case the traffic gen will
+        have the ARP responses back from VNFs with the dest MAC to use).
+
+        port_index: left port is 0, right port is 1
+        return: a list of dest MACs indexed by the chain index
+        """
+        if self.chains and self.config.service_chain != ChainType.EXT:
+            return [self.chains[chain_index].get_dest_mac(port_index)
+                    for chain_index in range(self.chain_count)]
+        # no openstack or EXT+no-arp
+        return self.dest_macs[port_index]
+
+    def get_host_ips(self):
+        """Return the IP adresss(es) of the host compute nodes used for this run.
+
+        :return: a list of 1 IP address
+        """
+        # Since all chains go through the same compute node(s) we can just retrieve the
+        # compute node(s) for the first chain
+        if self.chains:
+            if self.config.service_chain != ChainType.EXT:
+                return self.chains[0].get_host_ips()
+            # in the case of EXT, the compute node must be retrieved from the port
+            # associated to any of the dest MACs
+            dst_macs = self.chain_runner.traffic_client.gen.get_dest_macs()
+            # dest MAC on port 0, chain 0
+            dst_mac = dst_macs[0][0]
+            host_ip = self.get_host_ip_from_mac(dst_mac)
+            if host_ip:
+                LOG.info('Found compute node IP for EXT chain: %s', host_ip)
+                return [host_ip]
+        return []
+
+    def get_compute_nodes(self):
+        """Return the name of the host compute nodes used for this run.
+
+        :return: a list of 0 or 1 host name in the az:host format
+        """
+        # Since all chains go through the same compute node(s) we can just retrieve the
+        # compute node name(s) for the first chain
+        if self.chains:
+            # in the case of EXT, the compute node must be retrieved from the port
+            # associated to any of the dest MACs
+            return self.chains[0].get_compute_nodes()
+        # no openstack = no chains
+        return []
+
+    def delete(self):
+        """Delete resources for all chains.
+
+        Will not delete any resource if no-cleanup has been requested.
+        """
+        if self.config.no_cleanup:
+            return
+        for chain in self.chains:
+            chain.delete()
+        for network in self.networks:
+            network.delete()
+        if self.flavor:
+            self.flavor.delete()