MPLS support + loop_vm_arp test fix
[nfvbench.git] / nfvbench / chaining.py
index 1a977da..b9ed48b 100644 (file)
@@ -49,18 +49,21 @@ import os
 import re
 import time
 
-from glanceclient.v2 import client as glanceclient
+import 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
-
+from .chain_router import ChainRouter
+from . import compute
+from .log import LOG
+from .specs import ChainType
 # Left and right index for network and port lists
 LEFT = 0
 RIGHT = 1
+# L3 traffic edge networks are at the end of networks list
+EDGE_LEFT = -2
+EDGE_RIGHT = -1
 # Name of the VM config file
 NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
 # full pathame of the VM config in the VM
@@ -74,8 +77,6 @@ BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
 class ChainException(Exception):
     """Exception while operating the chains."""
 
-    pass
-
 class NetworkEncaps(object):
     """Network encapsulation."""
 
@@ -131,6 +132,7 @@ class ChainVnfPort(object):
         self.manager = vnf.manager
         self.reuse = False
         self.port = None
+        self.floating_ip = None
         if vnf.instance:
             # VNF instance is reused, we need to find an existing port that matches this instance
             # and network
@@ -174,18 +176,39 @@ class ChainVnfPort(object):
         """Get the MAC address for this port."""
         return self.port['mac_address']
 
+    def get_ip(self):
+        """Get the IP address for this port."""
+        return self.port['fixed_ips'][0]['ip_address']
+
+    def set_floating_ip(self, chain_network):
+        # create and add floating ip to port
+        try:
+            self.floating_ip = self.manager.neutron_client.create_floatingip({
+                'floatingip': {
+                    'floating_network_id': chain_network.get_uuid(),
+                    'port_id': self.port['id'],
+                    'description': 'nfvbench floating ip for port:' + self.port['name'],
+                }})['floatingip']
+            LOG.info('Floating IP %s created and associated on port %s',
+                     self.floating_ip['floating_ip_address'], self.name)
+            return self.floating_ip['floating_ip_address']
+        except Exception:
+            LOG.info('Failed to created and associated floating ip on port %s (ignored)', self.name)
+            return self.port['fixed_ips'][0]['ip_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:
+        for _ in range(0, self.manager.config.generic_retry_count):
             try:
                 self.manager.neutron_client.delete_port(self.port['id'])
                 LOG.info("Deleted port %s", self.name)
+                if self.floating_ip:
+                    self.manager.neutron_client.delete_floatingip(self.floating_ip['id'])
+                    LOG.info("Deleted floating IP %s", self.floating_ip['description'])
                 return
             except Exception:
-                retry += 1
                 time.sleep(self.manager.config.generic_poll_sec)
         LOG.error('Unable to delete port: %s', self.name)
 
@@ -193,24 +216,37 @@ class ChainVnfPort(object):
 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):
+    def __init__(self, manager, network_config, chain_id=None, lookup_only=False,
+                 suffix=None):
         """Create a network for given chain.
 
         network_config: a dict containing the network properties
-                        (segmentation_id and physical_network)
+                        (name, segmentation_id and physical_network)
         chain_id: to which chain the networks belong.
                   a None value will mean that these networks are shared by all chains
+        suffix: a suffix to add to the network name (if not None)
         """
         self.manager = manager
-        self.name = network_config.name
+        if chain_id is None:
+            self.name = network_config.name
+        else:
+            # the name itself can be either a string or a list of names indexed by chain ID
+            if isinstance(network_config.name, tuple):
+                self.name = network_config.name[chain_id]
+            else:
+                # network_config.name is a prefix string
+                self.name = network_config.name + str(chain_id)
+        if suffix:
+            self.name = self.name + suffix
         self.segmentation_id = self._get_item(network_config.segmentation_id,
                                               chain_id, auto_index=True)
         self.physical_network = self._get_item(network_config.physical_network, chain_id)
-        if chain_id is not None:
-            self.name += str(chain_id)
+
         self.reuse = False
         self.network = None
         self.vlan = None
+        if manager.config.l3_router and hasattr(network_config, 'router_name'):
+            self.router_name = network_config.router_name
         try:
             self._setup(network_config, lookup_only)
         except Exception:
@@ -283,7 +319,7 @@ class ChainNetwork(object):
                 'network': {
                     'name': self.name,
                     'admin_state_up': True
-                    }
+                }
             }
             if network_config.network_type:
                 body['network']['provider:network_type'] = network_config.network_type
@@ -292,6 +328,8 @@ class ChainNetwork(object):
             if self.physical_network:
                 body['network']['provider:physical_network'] = self.physical_network
             self.network = self.manager.neutron_client.create_network(body)['network']
+            # create associated subnet, all subnets have the same name (which is ok since
+            # we do not need to address them directly by name)
             body = {
                 'subnet': {'name': network_config.subnet,
                            'cidr': network_config.cidr,
@@ -329,24 +367,30 @@ class ChainNetwork(object):
 
         :return: VNI ID for this network
         """
-        if 'vxlan' not in self.network['provider:network_type']:
-            raise ChainException('Trying to retrieve VNI for non VXLAN network')
+
+        return self.network['provider:segmentation_id']
+
+    def get_mpls_inner_label(self):
+        """
+        Extract MPLS VPN Label for this network.
+
+        :return: MPLS VPN Label for this 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:
+            for retry in range(0, 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,
+                             retry + 1,
                              self.manager.config.generic_retry_count)
                     time.sleep(self.manager.config.generic_poll_sec)
             LOG.error('Unable to delete network: %s', self.name)
@@ -369,15 +413,27 @@ class ChainVnf(object):
         if len(networks) > 2:
             # we will have more than 1 VM in each chain
             self.name += '-' + str(vnf_id)
+        # A list of ports for this chain
+        # There are normally 2 ports carrying traffic (index 0, and index 1) and
+        # potentially multiple idle ports not carrying traffic (index 2 and up)
+        # For example if 7 idle interfaces are requested, the corresp. ports will be
+        # at index 2 to 8
         self.ports = []
+        self.management_port = None
+        self.routers = []
         self.status = None
         self.instance = None
         self.reuse = False
         self.host_ip = None
+        self.idle_networks = []
+        self.idle_ports = []
         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])
+            if self.manager.config.l3_router:
+                self._setup(networks[vnf_id:vnf_id + 4])
+            else:
+                self._setup(networks[vnf_id:vnf_id + 2])
         except Exception:
             LOG.error("Error creating VNF %s", self.name)
             self.delete()
@@ -386,29 +442,85 @@ class ChainVnf(object):
     def _get_vm_config(self, remote_mac_pair):
         config = self.manager.config
         devices = self.manager.generator_config.devices
+
+        if config.l3_router:
+            tg_gateway1_ip = self.routers[LEFT].ports[1]['fixed_ips'][0][
+                'ip_address']  # router edge ip left
+            tg_gateway2_ip = self.routers[RIGHT].ports[1]['fixed_ips'][0][
+                'ip_address']  # router edge ip right
+            tg_mac1 = self.routers[LEFT].ports[1]['mac_address']  # router edge mac left
+            tg_mac2 = self.routers[RIGHT].ports[1]['mac_address']  # router edge mac right
+            # edge cidr mask left
+            vnf_gateway1_cidr = \
+                self.ports[LEFT].get_ip() + self.__get_network_mask(
+                    self.manager.config.edge_networks.left.cidr)
+            # edge cidr mask right
+            vnf_gateway2_cidr = \
+                self.ports[RIGHT].get_ip() + self.__get_network_mask(
+                    self.manager.config.edge_networks.right.cidr)
+            if config.vm_forwarder != 'vpp':
+                raise ChainException(
+                    'L3 router mode imply to set VPP as VM forwarder.'
+                    'Please update your config file with: vm_forwarder: vpp')
+        else:
+            tg_gateway1_ip = devices[LEFT].tg_gateway_ip_addrs
+            tg_gateway2_ip = devices[RIGHT].tg_gateway_ip_addrs
+            if not config.loop_vm_arp:
+                tg_mac1 = remote_mac_pair[0]
+                tg_mac2 = remote_mac_pair[1]
+            else:
+                tg_mac1 = ""
+                tg_mac2 = ""
+
+            g1cidr = devices[LEFT].get_gw_ip(
+                self.chain.chain_id) + self.__get_network_mask(
+                    self.manager.config.internal_networks.left.cidr)
+            g2cidr = devices[RIGHT].get_gw_ip(
+                self.chain.chain_id) + self.__get_network_mask(
+                    self.manager.config.internal_networks.right.cidr)
+
+            vnf_gateway1_cidr = g1cidr
+            vnf_gateway2_cidr = g2cidr
+
         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_gateway1_ip': tg_gateway1_ip,
+            'tg_gateway2_ip': tg_gateway2_ip,
             '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]
+            'vnf_gateway1_cidr': vnf_gateway1_cidr,
+            'vnf_gateway2_cidr': vnf_gateway2_cidr,
+            'tg_mac1': tg_mac1,
+            'tg_mac2': tg_mac2,
+            'vif_mq_size': config.vif_multiqueue_size,
+            'num_mbufs': config.num_mbufs
         }
+        if self.manager.config.use_management_port:
+            mgmt_ip = self.management_port.port['fixed_ips'][0]['ip_address']
+            mgmt_mask = self.__get_network_mask(self.manager.config.management_network.cidr)
+            vm_config['intf_mgmt_cidr'] = mgmt_ip + mgmt_mask
+            vm_config['intf_mgmt_ip_gw'] = self.manager.config.management_network.gateway
+            vm_config['intf_mac_mgmt'] = self.management_port.port['mac_address']
+        else:
+            # Interface management config left empty to avoid error in VM spawn
+            # if nfvbench config has values for management network but use_management_port=false
+            vm_config['intf_mgmt_cidr'] = ''
+            vm_config['intf_mgmt_ip_gw'] = ''
+            vm_config['intf_mac_mgmt'] = ''
         return content.format(**vm_config)
 
+    @staticmethod
+    def __get_network_mask(network):
+        return '/' + network.split('/')[1]
+
     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
+        If SR-IOV is specified, middle ports in multi-VNF chains
         can use vswitch or SR-IOV based on config.use_sriov_middle_net
         """
         if self.manager.config.sriov:
@@ -423,45 +535,164 @@ class ChainVnf(object):
                 return 'direct'
         return 'normal'
 
+    def _get_idle_networks_ports(self):
+        """Get the idle networks for PVP or PVVP chain (non shared net only)
+
+        For EXT packet path or shared net, returns empty list.
+        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
+        """
+        networks = []
+        ports = []
+        config = self.manager.config
+        chain_id = self.chain.chain_id
+        idle_interfaces_per_vm = config.idle_interfaces_per_vm
+        if config.service_chain == ChainType.EXT or chain_id is None or \
+           idle_interfaces_per_vm == 0:
+            return
+
+        # Make a copy of the idle networks dict as we may have to modify the
+        # segmentation ID
+        idle_network_cfg = AttrDict(config.idle_networks)
+        if idle_network_cfg.segmentation_id:
+            segmentation_id = idle_network_cfg.segmentation_id + \
+                chain_id * idle_interfaces_per_vm
+        else:
+            segmentation_id = None
+        try:
+            # create as many idle networks and ports as requested
+            for idle_index in range(idle_interfaces_per_vm):
+                if config.service_chain == ChainType.PVP:
+                    suffix = '.%d' % (idle_index)
+                else:
+                    suffix = '.%d.%d' % (self.vnf_id, idle_index)
+                port_name = self.name + '-idle' + str(idle_index)
+                # update the segmentation id based on chain id and idle index
+                if segmentation_id:
+                    idle_network_cfg.segmentation_id = segmentation_id + idle_index
+                    port_name = port_name + "." + str(segmentation_id)
+
+                networks.append(ChainNetwork(self.manager,
+                                             idle_network_cfg,
+                                             chain_id,
+                                             suffix=suffix))
+                ports.append(ChainVnfPort(port_name,
+                                          self,
+                                          networks[idle_index],
+                                          'normal'))
+        except Exception:
+            # need to cleanup all successful networks
+            for net in networks:
+                net.delete()
+            for port in ports:
+                port.delete()
+            raise
+        self.idle_networks = networks
+        self.idle_ports = ports
+
     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:
+                instance_left = LEFT
+                instance_right = RIGHT
+                # In case of L3 traffic instance use edge networks
+                if self.manager.config.l3_router:
+                    instance_left = EDGE_LEFT
+                    instance_right = EDGE_RIGHT
                 # 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)
+                if not networks[instance_left].reuse:
+                    self._reuse_exception('network %s is new' % networks[instance_left].name)
+                if not networks[instance_right].reuse:
+                    self._reuse_exception('network %s is new' % networks[instance_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:
+                if networks[instance_left].name not in instance.networks:
                     self._reuse_exception('Left network mismatch')
-                if networks[RIGHT].name not in instance.networks:
+                if networks[instance_right].name not in instance.networks:
                     self._reuse_exception('Right network mismatch')
 
                 self.reuse = True
                 self.instance = instance
                 LOG.info('Reusing existing instance %s on %s',
                          self.name, self.get_hypervisor_name())
+        # create management port if needed
+        if self.manager.config.use_management_port:
+            self.management_port = ChainVnfPort(self.name + '-mgmt', self,
+                                                self.manager.management_network, 'normal')
+            ip = self.management_port.port['fixed_ips'][0]['ip_address']
+            if self.manager.config.use_floating_ip:
+                ip = self.management_port.set_floating_ip(self.manager.floating_ip_network)
+            LOG.info("Management interface will be active using IP: %s, "
+                     "and you can connect over SSH with login: nfvbench and password: nfvbench", ip)
         # 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 self.manager.config.l3_router:
+            for index in [0, 1]:
+                self.ports.append(ChainVnfPort(self.name + '-' + str(index),
+                                               self,
+                                               networks[index + 2],
+                                               self._get_vnic_type(index)))
+        else:
+            for index in [0, 1]:
+                self.ports.append(ChainVnfPort(self.name + '-' + str(index),
+                                               self,
+                                               networks[index],
+                                               self._get_vnic_type(index)))
+
+        # create idle networks and ports only if instance is not reused
+        # if reused, we do not care about idle networks/ports
+        if not self.reuse:
+            self._get_idle_networks_ports()
+
+        # Create neutron routers for L3 traffic use case
+        if self.manager.config.l3_router and self.manager.openstack:
+            internal_nets = networks[:2]
+            if self.manager.config.service_chain == ChainType.PVP:
+                edge_nets = networks[2:]
+            else:
+                edge_nets = networks[3:]
+            subnets_left = [internal_nets[0], edge_nets[0]]
+            routes_left = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
+                            'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
+                                0]},
+                           {'destination': self.manager.config.traffic_generator.ip_addrs[1],
+                            'nexthop': self.ports[0].get_ip()}]
+            self.routers.append(
+                ChainRouter(self.manager, edge_nets[0].router_name, subnets_left, routes_left))
+            subnets_right = [internal_nets[1], edge_nets[1]]
+            routes_right = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
+                             'nexthop': self.ports[1].get_ip()},
+                            {'destination': self.manager.config.traffic_generator.ip_addrs[1],
+                             'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
+                                 1]}]
+            self.routers.append(
+                ChainRouter(self.manager, edge_nets[1].router_name, subnets_right, routes_right))
+            # Overload gateway_ips property with router ip address for ARP and traffic calls
+            self.manager.generator_config.devices[LEFT].set_gw_ip(
+                self.routers[LEFT].ports[0]['fixed_ips'][0]['ip_address'])  # router edge ip left)
+            self.manager.generator_config.devices[RIGHT].set_gw_ip(
+                self.routers[RIGHT].ports[0]['fixed_ips'][0]['ip_address'])  # router edge ip right)
+
         # 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 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]
+            port_ids = []
+            if self.manager.config.use_management_port:
+                port_ids.append({'port-id': self.management_port.port['id']})
+            port_ids.extend([{'port-id': vnf_port.port['id']} for vnf_port in self.ports])
+            # add idle ports
+            for idle_port in self.idle_ports:
+                port_ids.append({'port-id': idle_port.port['id']})
             vm_config = self._get_vm_config(remote_mac_pair)
             az = self.manager.placer.get_required_az()
             server = self.manager.comp.create_server(self.name,
@@ -485,8 +716,8 @@ class ChainVnf(object):
                     # here we MUST wait until this instance is resolved otherwise subsequent
                     # VNF creation can be placed in other hypervisors!
                     config = self.manager.config
-                    max_retries = (config.check_traffic_time_sec +
-                                   config.generic_poll_sec - 1) / config.generic_poll_sec
+                    max_retries = int((config.check_traffic_time_sec +
+                                       config.generic_poll_sec - 1) / config.generic_poll_sec)
                     retry = 0
                     for retry in range(max_retries):
                         status = self.get_status()
@@ -566,8 +797,15 @@ class ChainVnf(object):
             if self.instance:
                 self.manager.comp.delete_server(self.instance)
                 LOG.info("Deleted instance %s", self.name)
+            if self.manager.config.use_management_port:
+                self.management_port.delete()
             for port in self.ports:
                 port.delete()
+            for port in self.idle_ports:
+                port.delete()
+            for network in self.idle_networks:
+                network.delete()
+
 
 class Chain(object):
     """A class to manage a single chain.
@@ -630,7 +868,8 @@ class Chain(object):
 
     def get_length(self):
         """Get the number of VNF in the chain."""
-        return len(self.networks) - 1
+        # Take into account 2 edge networks for routers
+        return len(self.networks) - 3 if self.manager.config.l3_router else len(self.networks) - 1
 
     def _get_remote_mac_pairs(self):
         """Get the list of remote mac pairs for every VNF in the chain.
@@ -701,6 +940,20 @@ class Chain(object):
             port_index = -1
         return self.networks[port_index].get_vxlan()
 
+    def get_mpls_inner_label(self, port_index):
+        """Get the MPLS VPN Label on a given port.
+
+        port_index: left port is 0, right port is 1
+        return: the mpls_label_id or None if there is no mpls
+        """
+        # for port 1 we need to return the MPLS Label 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_mpls_inner_label()
+
     def get_dest_mac(self, port_index):
         """Get the dest MAC on a given port.
 
@@ -876,6 +1129,22 @@ class ChainManager(object):
                     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 management port is requested for VMs, create management network (shared)
+                    if self.config.use_management_port:
+                        self.management_network = ChainNetwork(self, self.config.management_network,
+                                                               None, False)
+                        # If floating IP is used for management, create and share
+                        # across chains the floating network
+                        if self.config.use_floating_ip:
+                            self.floating_ip_network = ChainNetwork(self,
+                                                                    self.config.floating_network,
+                                                                    None, False)
+                else:
+                    # For EXT chains, the external_networks left and right fields in the config
+                    # must be either a prefix string or a list of at least chain-count strings
+                    self._check_extnet('left', config.external_networks.left)
+                    self._check_extnet('right', config.external_networks.right)
+
                 # If networks are shared across chains, get the list of networks
                 if config.service_chain_shared_net:
                     self.networks = self.get_networks()
@@ -883,14 +1152,14 @@ class ChainManager(object):
                 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:
+                    # if EXT and no ARP or VxLAN we need to read dest MACs from config
+                    if config.no_arp or config.vxlan:
                         self._get_dest_macs_from_config()
                 else:
                     # Make sure all instances are active before proceeding
                     self._ensure_instances_active()
                 # network API call do not show VLANS ID if not admin read from config
-                if not self.is_admin:
+                if not self.is_admin and config.vlan_tagging:
                     self._get_config_vlans()
             except Exception:
                 self.delete()
@@ -908,6 +1177,14 @@ class ChainManager(object):
             if config.vxlan:
                 raise ChainException('VxLAN is only supported with OpenStack')
 
+    def _check_extnet(self, side, name):
+        if not name:
+            raise ChainException('external_networks.%s must contain a valid network'
+                                 ' name prefix or a list of network names' % side)
+        if isinstance(name, tuple) and len(name) < self.chain_count:
+            raise ChainException('external_networks.%s %s'
+                                 ' must have at least %d names' % (side, name, self.chain_count))
+
     def _get_config_vlans(self):
         re_vlan = "[0-9]*$"
         try:
@@ -985,12 +1262,16 @@ class ChainManager(object):
                 LOG.info('Image %s successfully uploaded.', self.image_name)
                 self.image_instance = self.comp.find_image(self.image_name)
 
+        # image multiqueue property must be set according to the vif_multiqueue_size
+        # config value (defaults to 1 or disabled)
+        self.comp.image_set_multiqueue(self.image_instance, self.config.vif_multiqueue_size > 1)
+
     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 +
+        max_retries = (self.config.check_traffic_time_sec + (initial_instance_count - 1) * 10 +
                        self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
         retry = 0
         while instances:
@@ -1048,6 +1329,10 @@ class ChainManager(object):
                 net_cfg = [int_nets.left, int_nets.right]
             else:
                 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
+            if self.config.l3_router:
+                edge_nets = self.config.edge_networks
+                net_cfg.append(edge_nets.left)
+                net_cfg.append(edge_nets.right)
         networks = []
         try:
             for cfg in net_cfg:
@@ -1111,23 +1396,34 @@ class ChainManager(object):
         """
         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.
+    def get_hypervisor_from_mac(self, mac):
+        """Get the hypervisor that hosts a VM MAC.
 
         mac: MAC address to look for
-        return: the IP address of the host where the matching port runs or None if not found
+        return: the hypervisor 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_list in list(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
+                        return self.comp.get_hypervisor(host_id)
                 except KeyError:
                     pass
         return 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
+        """
+        hypervisor = self.get_hypervisor_from_mac(mac)
+        if hypervisor:
+            return hypervisor.host_ip
+        return None
+
     def get_chain_vlans(self, port_index):
         """Get the list of per chain VLAN id on a given port.
 
@@ -1146,11 +1442,23 @@ class ChainManager(object):
         port_index: left port is 0, right port is 1
         return: a VNIs ID list indexed by the chain index or None if no vlan tagging
         """
-        if self.chains:
+        if self.chains and self.is_admin:
             return [self.chains[chain_index].get_vxlan(port_index)
                     for chain_index in range(self.chain_count)]
         # no openstack
-        raise ChainException('VxLAN is only supported with OpenStack')
+        raise ChainException('VxLAN is only supported with OpenStack and with admin user')
+
+    def get_chain_mpls_inner_labels(self, port_index):
+        """Get the list of per chain MPLS VPN Labels on a given port.
+
+        port_index: left port is 0, right port is 1
+        return: a MPLSs ID list indexed by the chain index or None if no mpls
+        """
+        if self.chains and self.is_admin:
+            return [self.chains[chain_index].get_mpls_inner_label(port_index)
+                    for chain_index in range(self.chain_count)]
+        # no openstack
+        raise ChainException('MPLS is only supported with OpenStack and with admin user')
 
     def get_dest_macs(self, port_index):
         """Get the list of per chain dest MACs on a given port.
@@ -1198,7 +1506,17 @@ class ChainManager(object):
         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()
+            if self.config.service_chain != ChainType.EXT:
+                return self.chains[0].get_compute_nodes()
+            # in the case of EXT, the compute node must be retrieved from the port
+            # associated to any of the dest MACs
+            dst_macs = self.generator_config.get_dest_macs()
+            # dest MAC on port 0, chain 0
+            dst_mac = dst_macs[0][0]
+            hypervisor = self.get_hypervisor_from_mac(dst_mac)
+            if hypervisor:
+                LOG.info('Found hypervisor for EXT chain: %s', hypervisor.hypervisor_hostname)
+                return[':' + hypervisor.hypervisor_hostname]
         # no openstack = no chains
         return []
 
@@ -1208,5 +1526,9 @@ class ChainManager(object):
             chain.delete()
         for network in self.networks:
             network.delete()
+        if self.config.use_management_port and hasattr(self, 'management_network'):
+            self.management_network.delete()
+        if self.config.use_floating_ip and hasattr(self, 'floating_ip_network'):
+            self.floating_ip_network.delete()
         if self.flavor:
             self.flavor.delete()