diff options
-rw-r--r-- | nfvbench/chain_clients.py | 4 | ||||
-rw-r--r-- | nfvbench/traffic_client.py | 131 | ||||
-rw-r--r-- | test/test_nfvbench.py | 114 |
3 files changed, 181 insertions, 68 deletions
diff --git a/nfvbench/chain_clients.py b/nfvbench/chain_clients.py index 01bf435..ac95247 100644 --- a/nfvbench/chain_clients.py +++ b/nfvbench/chain_clients.py @@ -310,8 +310,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, diff --git a/nfvbench/traffic_client.py b/nfvbench/traffic_client.py index 2a42b87..7aa557a 100644 --- a/nfvbench/traffic_client.py +++ b/nfvbench/traffic_client.py @@ -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): diff --git a/test/test_nfvbench.py b/test/test_nfvbench.py index ff4625b..3eb1cb2 100644 --- a/test/test_nfvbench.py +++ b/test/test_nfvbench.py @@ -17,12 +17,12 @@ 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} |