MPLS support + loop_vm_arp test fix
[nfvbench.git] / nfvbench / chaining.py
index 60f3832..b9ed48b 100644 (file)
@@ -54,13 +54,16 @@ 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)
 
@@ -222,6 +245,8 @@ class ChainNetwork(object):
         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:
@@ -294,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
@@ -342,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)
@@ -388,6 +419,8 @@ class ChainVnf(object):
         # 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
@@ -397,7 +430,10 @@ class ChainVnf(object):
         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()
@@ -406,25 +442,81 @@ 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.
 
@@ -504,46 +596,100 @@ class ChainVnf(object):
         # 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']})
@@ -570,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()
@@ -651,6 +797,8 @@ 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:
@@ -658,6 +806,7 @@ class ChainVnf(object):
             for network in self.idle_networks:
                 network.delete()
 
+
 class Chain(object):
     """A class to manage a single chain.
 
@@ -719,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.
@@ -790,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.
 
@@ -965,6 +1129,16 @@ 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
@@ -1088,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:
@@ -1151,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:
@@ -1221,7 +1403,7 @@ class ChainManager(object):
         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:
@@ -1266,6 +1448,18 @@ class ChainManager(object):
         # no 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.
 
@@ -1323,7 +1517,6 @@ class ChainManager(object):
             if hypervisor:
                 LOG.info('Found hypervisor for EXT chain: %s', hypervisor.hypervisor_hostname)
                 return[':' + hypervisor.hypervisor_hostname]
-
         # no openstack = no chains
         return []
 
@@ -1333,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()