NFVBENCH-5 NFVBENCH-39 Fix long prep time with large number of flows 77/44977/1
authorahothan <ahothan@cisco.com>
Fri, 13 Oct 2017 07:30:22 +0000 (00:30 -0700)
committerahothan <ahothan@cisco.com>
Fri, 13 Oct 2017 07:30:22 +0000 (00:30 -0700)
Fix bad ip addr count with step>0.0.0.1
Add unit test code

Change-Id: Ia1a3ae5188984aa0ed1f31954f17063a6f2925fd
Signed-off-by: ahothan <ahothan@cisco.com>
nfvbench/chain_clients.py
nfvbench/traffic_client.py
test/test_nfvbench.py

index c6df08a..4277049 100644 (file)
@@ -313,8 +313,8 @@ class BasicStageClient(object):
         with open(boot_script_file, 'r') as boot_script:
             content = boot_script.read()
 
-        g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
-        g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
+        g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
+        g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
 
         vm_config = {
             'forwarder': self.config.vm_forwarder,
index 2a42b87..7aa557a 100644 (file)
@@ -81,6 +81,35 @@ class TrafficRunner(object):
             self.stop()
         return self.client.get_stats()
 
+class IpBlock(object):
+    def __init__(self, base_ip, step_ip, count_ip):
+        self.base_ip_int = Device.ip_to_int(base_ip)
+        self.step = Device.ip_to_int(step_ip)
+        self.max_available = count_ip
+        self.next_free = 0
+
+    def get_ip(self, index=0):
+        '''Return the IP address at given index
+        '''
+        if index < 0 or index >= self.max_available:
+            raise IndexError('Index out of bounds')
+        return Device.int_to_ip(self.base_ip_int + index * self.step)
+
+    def reserve_ip_range(self, count):
+        '''Reserve a range of count consecutive IP addresses spaced by step
+        '''
+        if self.next_free + count > self.max_available:
+            raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d',
+                              self.next_free,
+                              self.max_available,
+                              count)
+        first_ip = self.get_ip(self.next_free)
+        last_ip = self.get_ip(self.next_free + count - 1)
+        self.next_free += count
+        return (first_ip, last_ip)
+
+    def reset_reservation(self):
+        self.next_free = 0
 
 class Device(object):
 
@@ -105,15 +134,15 @@ class Device(object):
         self.ip_addrs_step = ip_addrs_step
         self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
         self.gateway_ip_addrs_step = gateway_ip_addrs_step
-        self.ip_list = self.expand_ip(self.ip, self.ip_addrs_step, self.flow_count)
         self.gateway_ip = gateway_ip
-        self.gateway_ip_list = self.expand_ip(self.gateway_ip,
-                                              self.gateway_ip_addrs_step,
-                                              self.chain_count)
         self.tg_gateway_ip = tg_gateway_ip
-        self.tg_gateway_ip_list = self.expand_ip(self.tg_gateway_ip,
-                                                 self.tg_gateway_ip_addrs_step,
-                                                 self.chain_count)
+        self.ip_block = IpBlock(self.ip, ip_addrs_step, flow_count)
+        self.gw_ip_block = IpBlock(gateway_ip,
+                                   gateway_ip_addrs_step,
+                                   chain_count) 
+        self.tg_gw_ip_block = IpBlock(tg_gateway_ip,
+                                      tg_gateway_ip_addrs_step,
+                                      chain_count) 
         self.udp_src_port = udp_src_port
         self.udp_dst_port = udp_dst_port
 
@@ -133,55 +162,59 @@ class Device(object):
             raise TrafficClientException('Trying to set VLAN tag as None')
         self.vlan_tag = vlan_tag
 
+    def get_gw_ip(self, chain_index):
+        '''Retrieve the IP address assigned for the gateway of a given chain
+        '''
+        return self.gw_ip_block.get_ip(chain_index)
+
     def get_stream_configs(self, service_chain):
         configs = []
-        flow_idx = 0
-        for chain_idx in xrange(self.chain_count):
-            current_flow_count = (self.flow_count - flow_idx) / (self.chain_count - chain_idx)
-            max_idx = flow_idx + current_flow_count - 1
-            ip_src_count = self.ip_to_int(self.ip_list[max_idx]) - \
-                self.ip_to_int(self.ip_list[flow_idx]) + 1
-            ip_dst_count = self.ip_to_int(self.dst.ip_list[max_idx]) - \
-                self.ip_to_int(self.dst.ip_list[flow_idx]) + 1
+        # exact flow count for each chain is calculated as follows:
+        # - all chains except the first will have the same flow count
+        #   calculated as (total_flows + chain_count - 1) / chain_count
+        # - the first chain will have the remainder  
+        # example 11 flows and 3 chains => 3, 4, 4
+        flows_per_chain = (self.flow_count + self.chain_count -1) / self.chain_count
+        cur_chain_flow_count = self.flow_count - flows_per_chain * (self.chain_count - 1)
 
+        self.ip_block.reset_reservation()
+        self.dst.ip_block.reset_reservation()
+
+        for chain_idx in xrange(self.chain_count):
+            src_ip_first, src_ip_last = self.ip_block.reserve_ip_range(cur_chain_flow_count)
+            dst_ip_first, dst_ip_last = self.dst.ip_block.reserve_ip_range(cur_chain_flow_count)
             configs.append({
-                'count': current_flow_count,
+                'count': cur_chain_flow_count,
                 'mac_src': self.mac,
                 'mac_dst': self.dst.mac if service_chain == ChainType.EXT
                 else self.vm_mac_list[chain_idx],
-                'ip_src_addr': self.ip_list[flow_idx],
-                'ip_src_addr_max': self.ip_list[max_idx],
-                'ip_src_count': ip_src_count,
-                'ip_dst_addr': self.dst.ip_list[flow_idx],
-                'ip_dst_addr_max': self.dst.ip_list[max_idx],
-                'ip_dst_count': ip_dst_count,
+                'ip_src_addr': src_ip_first,
+                'ip_src_addr_max': src_ip_last,
+                'ip_src_count': cur_chain_flow_count,
+                'ip_dst_addr': dst_ip_first,
+                'ip_dst_addr_max': dst_ip_last,
+                'ip_dst_count': cur_chain_flow_count,
                 'ip_addrs_step': self.ip_addrs_step,
                 'udp_src_port': self.udp_src_port,
                 'udp_dst_port': self.udp_dst_port,
-                'mac_discovery_gw': self.gateway_ip_list[chain_idx],
-                'ip_src_tg_gw': self.tg_gateway_ip_list[chain_idx],
-                'ip_dst_tg_gw': self.dst.tg_gateway_ip_list[chain_idx],
+                'mac_discovery_gw': self.get_gw_ip(chain_idx),
+                'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
+                'ip_dst_tg_gw': self.dst.tg_gw_ip_block.get_ip(chain_idx),
                 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
             })
+            # after first chain, fall back to the flow count for all other chains
+            cur_chain_flow_count = flows_per_chain
 
-            flow_idx += current_flow_count
         return configs
 
-    @classmethod
-    def expand_ip(cls, ip, step_ip, count):
-        if step_ip == 'random':
-            # Repeatable Random will used in the stream src/dst IP pairs, but we still need
-            # to expand the IP based on the number of chains and flows configured. So we use
-            # "0.0.0.1" as the step to have the exact IP flow ranges for every chain.
-            step_ip = '0.0.0.1'
-
-        step_ip_in_int = cls.ip_to_int(step_ip)
-        subnet = IPNetwork(ip)
-        ip_list = []
-        for _ in xrange(count):
-            ip_list.append(subnet.ip.format())
-            subnet = subnet.next(step_ip_in_int)
-        return ip_list
+    def ip_range_overlaps(self):
+        '''Check if this device ip range is overlapping with the dst device ip range
+        '''
+        src_base_ip = Device.ip_to_int(self.ip)
+        dst_base_ip = Device.ip_to_int(self.dst.ip)
+        src_last_ip = src_base_ip + self.flow_count - 1
+        dst_last_ip = dst_base_ip + self.flow_count - 1
+        return dst_last_ip >= src_base_ip and src_last_ip >= dst_base_ip
 
     @staticmethod
     def mac_to_int(mac):
@@ -197,6 +230,9 @@ class Device(object):
     def ip_to_int(addr):
         return struct.unpack("!I", socket.inet_aton(addr))[0]
 
+    @staticmethod
+    def int_to_ip(nvalue):
+        return socket.inet_ntoa(struct.pack("!I", nvalue))
 
 class RunningTrafficProfile(object):
     """Represents traffic configuration for currently running traffic profile."""
@@ -284,14 +320,11 @@ class RunningTrafficProfile(object):
         self.dst_device.set_destination(self.src_device)
 
         if self.service_chain == ChainType.EXT and not self.no_arp \
-                and not self.__are_unique(self.src_device.ip_list, self.dst_device.ip_list):
-            raise Exception('Computed IP addresses are not unique, choose different base. '
-                            'Start IPs: {start}. End IPs: {end}'
-                            .format(start=self.src_device.ip_list,
-                                    end=self.dst_device.ip_list))
-
-    def __are_unique(self, list1, list2):
-        return set(list1).isdisjoint(set(list2))
+                and self.src_device.ip_range_overlaps():
+            raise Exception('Overlapping IP address ranges src=%s dst=%d flows=%d' %
+                            self.src_device.ip,
+                            self.dst_device.ip,
+                            self.flow_count)
 
     @property
     def devices(self):
index ff4625b..3eb1cb2 100644 (file)
 from attrdict import AttrDict
 import logging
 from nfvbench.config import config_loads
-from nfvbench.connection import SSH
 from nfvbench.credentials import Credentials
 from nfvbench.fluentd import FluentLogHandler
 import nfvbench.log
 from nfvbench.network import Interface
 from nfvbench.network import Network
+from nfvbench.specs import ChainType
 from nfvbench.specs import Encaps
 import nfvbench.traffic_gen.traffic_utils as traffic_utils
 import os
@@ -32,22 +32,6 @@ __location__ = os.path.realpath(os.path.join(os.getcwd(),
                                              os.path.dirname(__file__)))
 
 
-@pytest.fixture
-def ssh(monkeypatch):
-    def mock_init(self, ssh_access, *args, **kwargs):
-        self.ssh_access = ssh_access
-        if ssh_access.private_key:
-            self.pkey = self._get_pkey(ssh_access.private_key)
-        else:
-            self.pkey = None
-        self._client = False
-        self.connect_timeout = 2
-        self.connect_retry_count = 1
-        self.connect_retry_wait_sec = 1
-        super(SSH, self).__init__()
-
-    monkeypatch.setattr(SSH, '__init__', mock_init)
-
 
 @pytest.fixture
 def openstack_vxlan_spec():
@@ -646,6 +630,102 @@ def test_no_credentials():
     else:
         assert True
 
+import socket
+import sys
+
+# Because trex_stl_lib may not be installed when running unit test
+# nfvbench.traffic_client will try to import STLError:
+# from trex_stl_lib.api import STLError
+# will raise ImportError: No module named trex_stl_lib.api
+try:
+    import trex_stl_lib.api
+except ImportError:
+    # Make up a trex_stl_lib.api.STLError class
+    class STLError(Exception):
+        pass
+    from types import ModuleType
+    stl_lib_mod = ModuleType('trex_stl_lib')
+    sys.modules['trex_stl_lib'] = stl_lib_mod
+    api_mod = ModuleType('trex_stl_lib.api')
+    stl_lib_mod.api = api_mod
+    sys.modules['trex_stl_lib.api'] = api_mod
+    api_mod.STLError = STLError
+
+from nfvbench.traffic_client import Device
+from nfvbench.traffic_client import IpBlock
+
+
+def test_ip_block():
+    ipb = IpBlock('10.0.0.0', '0.0.0.1', 256)
+    assert(ipb.get_ip() == '10.0.0.0')
+    assert(ipb.get_ip(255) == '10.0.0.255')
+    with pytest.raises(IndexError):
+        ipb.get_ip(256)
+    # verify with step larger than 1
+    ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
+    assert(ipb.get_ip() == '10.0.0.0')
+    assert(ipb.get_ip(1) == '10.0.0.2')
+    assert(ipb.get_ip(128) == '10.0.1.0')
+    assert(ipb.get_ip(255) == '10.0.1.254')
+    with pytest.raises(IndexError):
+        ipb.get_ip(256)
+
+def check_config(configs, cc, fc, src_ip, dst_ip, step_ip):
+    '''Verify that the range configs for each chain have adjacent IP ranges
+    of the right size and without holes between chains
+    '''
+    step = Device.ip_to_int(step_ip)
+    cfc = 0
+    sip = Device.ip_to_int(src_ip)
+    dip = Device.ip_to_int(dst_ip)
+    for index in range(cc):
+        config = configs[index]
+        assert(config['ip_src_count'] == config['ip_dst_count'])
+        assert(Device.ip_to_int(config['ip_src_addr']) == sip)
+        assert(Device.ip_to_int(config['ip_dst_addr']) == dip)
+        count = config['ip_src_count']
+        cfc += count
+        sip += count * step
+        dip += count * step
+    assert(cfc == fc)
+
+def create_device(fc, cc, ip, gip, tggip, step_ip):
+    return Device(0, 0, flow_count=fc, chain_count=cc, ip=ip, gateway_ip=gip, tg_gateway_ip=tggip,
+                     ip_addrs_step=step_ip,
+                     tg_gateway_ip_addrs_step=step_ip,
+                     gateway_ip_addrs_step=step_ip)
+
+def check_device_flow_config(step_ip):
+    fc = 99999
+    cc = 10
+    ip0 = '10.0.0.0'
+    ip1 = '20.0.0.0'
+    tggip = '50.0.0.0'
+    gip = '60.0.0.0'
+    dev0 = create_device(fc, cc, ip0, gip, tggip, step_ip)
+    dev1 = create_device(fc, cc, ip1, gip, tggip, step_ip)
+    dev0.set_destination(dev1)
+    configs = dev0.get_stream_configs(ChainType.EXT)
+    check_config(configs, cc, fc, ip0, ip1, step_ip)
+
+def test_device_flow_config():
+    check_device_flow_config('0.0.0.1')
+    check_device_flow_config('0.0.0.2')
+
+def test_device_ip_range():
+    def is_ip_range_disjoint(ip0, ip1, flows):
+        tggip = '50.0.0.0'
+        gip = '60.0.0.0'
+        dev0 = create_device(flows, 10, ip0, gip, tggip, '0.0.0.1')
+        dev1 = create_device(flows, 10, ip1, gip, tggip, '0.0.0.1')
+        dev0.set_destination(dev1)
+        return dev0.is_ip_range_disjoint()
+    assert(is_ip_range_disjoint('10.0.0.0', '20.0.0.0', 10000))
+    assert(not is_ip_range_disjoint('10.0.0.0', '10.0.1.0', 10000))
+    assert(not is_ip_range_disjoint('10.0.0.0', '10.0.1.0', 257))
+    assert(not is_ip_range_disjoint('10.0.1.0', '10.0.0.0', 257))
+
+
 def test_config():
     refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
     res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}