NFVBENCH-5 NFVBENCH-39 Fix long prep time with large number of flows 95/45995/1
authorahothan <ahothan@cisco.com>
Fri, 13 Oct 2017 07:30:22 +0000 (00:30 -0700)
committerahothan <ahothan@cisco.com>
Fri, 20 Oct 2017 07:37:14 +0000 (00:37 -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 01bf435..ac95247 100644 (file)
@@ -310,8 +310,8 @@ class BasicStageClient(object):
         with open(boot_script_file, 'r') as boot_script:
             content = boot_script.read()
 
         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,
 
         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()
 
             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):
 
 
 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_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 = 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 = 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
 
         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
 
             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 = []
     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({
             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],
                 '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,
                 '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
             })
                 '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
 
         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):
 
     @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]
 
     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."""
 
 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 \
         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):
 
     @property
     def devices(self):
index ff4625b..3eb1cb2 100644 (file)
 from attrdict import AttrDict
 import logging
 from nfvbench.config import config_loads
 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.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
 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__)))
 
 
                                              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():
 
 @pytest.fixture
 def openstack_vxlan_spec():
@@ -646,6 +630,102 @@ def test_no_credentials():
     else:
         assert True
 
     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}
 def test_config():
     refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
     res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}