#!/usr/bin/env python # Copyright 2016 Cisco Systems, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import json import logging import os import sys import pytest from attrdict import AttrDict from nfvbench.config import config_loads 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 __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) @pytest.fixture def openstack_vxlan_spec(): return AttrDict( { 'openstack': AttrDict({ 'vswitch': "VTS", 'encaps': Encaps.VxLAN}), 'run_spec': AttrDict({ 'use_vpp': True }) } ) # ========================================================================= # PVP Chain tests # ========================================================================= def test_chain_interface(): iface = Interface('testname', 'vpp', tx_packets=1234, rx_packets=4321) assert iface.name == 'testname' assert iface.device == 'vpp' assert iface.get_packet_count('tx') == 1234 assert iface.get_packet_count('rx') == 4321 assert iface.get_packet_count('wrong_key') == 0 # pylint: disable=redefined-outer-name @pytest.fixture(scope='session') def iface1(): return Interface('iface1', 'trex', tx_packets=10000, rx_packets=1234) @pytest.fixture(scope='session') def iface2(): return Interface('iface2', 'n9k', tx_packets=1234, rx_packets=9901) @pytest.fixture(scope='session') def iface3(): return Interface('iface3', 'n9k', tx_packets=9900, rx_packets=1234) @pytest.fixture(scope='session') def iface4(): return Interface('iface4', 'vpp', tx_packets=1234, rx_packets=9801) @pytest.fixture(scope='session') def net1(iface1, iface2, iface3, iface4): return Network([iface1, iface2, iface3, iface4], reverse=False) @pytest.fixture(scope='session') def net2(iface1, iface2, iface3): return Network([iface1, iface2, iface3], reverse=True) def test_chain_network(net1, net2, iface1, iface2, iface3, iface4): assert [iface1, iface2, iface3, iface4] == net1.get_interfaces() assert [iface3, iface2, iface1] == net2.get_interfaces() net2.add_interface(iface4) assert [iface4, iface3, iface2, iface1] == net2.get_interfaces() # pylint: enable=redefined-outer-name # pylint: disable=pointless-string-statement """ def test_chain_analysis(net1, monkeypatch, openstack_vxlan_spec): def mock_empty(self, *args, **kwargs): pass monkeypatch.setattr(ServiceChain, '_setup', mock_empty) f = ServiceChain(AttrDict({'service_chain': 'DUMMY'}), [], {'tor': {}}, openstack_vxlan_spec, lambda x, y, z: None) result = f.get_analysis([net1]) assert result[1]['packet_drop_count'] == 99 assert result[1]['packet_drop_percentage'] == 0.99 assert result[2]['packet_drop_count'] == 1 assert result[2]['packet_drop_percentage'] == 0.01 assert result[3]['packet_drop_count'] == 99 assert result[3]['packet_drop_percentage'] == 0.99 net1.reverse = True result = f.get_analysis([net1]) assert result[1]['packet_drop_count'] == 0 assert result[1]['packet_drop_percentage'] == 0.0 assert result[2]['packet_drop_count'] == 0 assert result[2]['packet_drop_percentage'] == 0.0 assert result[3]['packet_drop_count'] == 0 assert result[3]['packet_drop_percentage'] == 0.0 @pytest.fixture def pvp_chain(monkeypatch, openstack_vxlan_spec): tor_vni1 = Interface('vni-4097', 'n9k', 50, 77) vsw_vni1 = Interface('vxlan_tunnel0', 'vpp', 77, 48) vsw_vif1 = Interface('VirtualEthernet0/0/2', 'vpp', 48, 77) vsw_vif2 = Interface('VirtualEthernet0/0/3', 'vpp', 77, 47) vsw_vni2 = Interface('vxlan_tunnel1', 'vpp', 43, 77) tor_vni2 = Interface('vni-4098', 'n9k', 77, 40) def mock_init(self, *args, **kwargs): self.vni_ports = [4097, 4098] self.specs = openstack_vxlan_spec self.clients = { 'vpp': AttrDict({ 'set_interface_counters': lambda: None, }) } self.worker = AttrDict({ 'run': lambda: None, }) def mock_empty(self, *args, **kwargs): pass def mock_get_network(self, traffic_port, vni_id, reverse=False): if vni_id == 0: return Network([tor_vni1, vsw_vni1, vsw_vif1], reverse) else: return Network([tor_vni2, vsw_vni2, vsw_vif2], reverse) def mock_get_data(self): return {} monkeypatch.setattr(PVPChain, '_get_network', mock_get_network) monkeypatch.setattr(PVPChain, '_get_data', mock_get_data) monkeypatch.setattr(PVPChain, '_setup', mock_empty) monkeypatch.setattr(VxLANWorker, '_clear_interfaces', mock_empty) monkeypatch.setattr(PVPChain, '_generate_traffic', mock_empty) monkeypatch.setattr(PVPChain, '__init__', mock_init) return PVPChain(None, None, {'vm': None, 'vpp': None, 'tor': None, 'traffic': None}, None) def test_pvp_chain_run(pvp_chain): result = pvp_chain.run() expected_result = { 'raw_data': {}, 'stats': None, 'packet_analysis': { 'direction-forward': [ OrderedDict([ ('interface', 'vni-4097'), ('device', 'n9k'), ('packet_count', 50) ]), OrderedDict([ ('interface', 'vxlan_tunnel0'), ('device', 'vpp'), ('packet_count', 48), ('packet_drop_count', 2), ('packet_drop_percentage', 4.0) ]), OrderedDict([ ('interface', 'VirtualEthernet0/0/2'), ('device', 'vpp'), ('packet_count', 48), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]), OrderedDict([ ('interface', 'VirtualEthernet0/0/3'), ('device', 'vpp'), ('packet_count', 47), ('packet_drop_count', 1), ('packet_drop_percentage', 2.0) ]), OrderedDict([ ('interface', 'vxlan_tunnel1'), ('device', 'vpp'), ('packet_count', 43), ('packet_drop_count', 4), ('packet_drop_percentage', 8.0) ]), OrderedDict([ ('interface', 'vni-4098'), ('device', 'n9k'), ('packet_count', 40), ('packet_drop_count', 3), ('packet_drop_percentage', 6.0) ]) ], 'direction-reverse': [ OrderedDict([ ('interface', 'vni-4098'), ('device', 'n9k'), ('packet_count', 77) ]), OrderedDict([ ('interface', 'vxlan_tunnel1'), ('device', 'vpp'), ('packet_count', 77), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]), OrderedDict([ ('interface', 'VirtualEthernet0/0/3'), ('device', 'vpp'), ('packet_count', 77), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]), OrderedDict([ ('interface', 'VirtualEthernet0/0/2'), ('device', 'vpp'), ('packet_count', 77), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]), OrderedDict([ ('interface', 'vxlan_tunnel0'), ('device', 'vpp'), ('packet_count', 77), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]), OrderedDict([ ('interface', 'vni-4097'), ('device', 'n9k'), ('packet_count', 77), ('packet_drop_count', 0), ('packet_drop_percentage', 0.0) ]) ] } } assert result == expected_result """ # ========================================================================= # Traffic client tests # ========================================================================= def test_parse_rate_str(): parse_rate_str = traffic_utils.parse_rate_str try: assert parse_rate_str('100%') == {'rate_percent': '100.0'} assert parse_rate_str('37.5%') == {'rate_percent': '37.5'} assert parse_rate_str('100%') == {'rate_percent': '100.0'} assert parse_rate_str('60pps') == {'rate_pps': '60'} assert parse_rate_str('60kpps') == {'rate_pps': '60000'} assert parse_rate_str('6Mpps') == {'rate_pps': '6000000'} assert parse_rate_str('6gpps') == {'rate_pps': '6000000000'} assert parse_rate_str('80bps') == {'rate_bps': '80'} assert parse_rate_str('80bps') == {'rate_bps': '80'} assert parse_rate_str('80kbps') == {'rate_bps': '80000'} assert parse_rate_str('80kBps') == {'rate_bps': '640000'} assert parse_rate_str('80Mbps') == {'rate_bps': '80000000'} assert parse_rate_str('80 MBps') == {'rate_bps': '640000000'} assert parse_rate_str('80Gbps') == {'rate_bps': '80000000000'} except Exception as exc: assert False, exc.message def should_raise_error(str): try: parse_rate_str(str) except Exception: return True else: return False return False assert should_raise_error('101') assert should_raise_error('201%') assert should_raise_error('10Kbps') assert should_raise_error('0kbps') assert should_raise_error('0pps') assert should_raise_error('-1bps') def test_rate_conversion(): assert traffic_utils.load_to_bps(50, 10000000000) == pytest.approx(5000000000.0) assert traffic_utils.load_to_bps(37, 10000000000) == pytest.approx(3700000000.0) assert traffic_utils.load_to_bps(100, 10000000000) == pytest.approx(10000000000.0) assert traffic_utils.bps_to_load(5000000000.0, 10000000000) == pytest.approx(50.0) assert traffic_utils.bps_to_load(3700000000.0, 10000000000) == pytest.approx(37.0) assert traffic_utils.bps_to_load(10000000000.0, 10000000000) == pytest.approx(100.0) assert traffic_utils.bps_to_pps(500000, 64) == pytest.approx(744.047619048) assert traffic_utils.bps_to_pps(388888, 1518) == pytest.approx(31.6066319896) assert traffic_utils.bps_to_pps(9298322222, 340.3) == pytest.approx(3225895.85831) assert traffic_utils.pps_to_bps(744.047619048, 64) == pytest.approx(500000) assert traffic_utils.pps_to_bps(31.6066319896, 1518) == pytest.approx(388888) assert traffic_utils.pps_to_bps(3225895.85831, 340.3) == pytest.approx(9298322222) # pps at 10Gbps line rate for 64 byte frames LR_64B_PPS = 14880952 LR_1518B_PPS = 812743 def assert_equivalence(reference, value, allowance_pct=1): '''Asserts if a value is equivalent to a reference value with given margin :param float reference: reference value to compare to :param float value: value to compare to reference :param float allowance_pct: max allowed percentage of margin 0 : requires exact match 1 : must be equal within 1% of the reference value ... 100: always true ''' if reference == 0: assert value == 0 else: assert abs(value - reference) * 100 / reference <= allowance_pct def test_load_from_rate(): assert traffic_utils.get_load_from_rate('100%') == 100 assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_64B_PPS) + 'pps')) assert_equivalence(50, traffic_utils.get_load_from_rate(str(LR_64B_PPS / 2) + 'pps')) assert_equivalence(100, traffic_utils.get_load_from_rate('10Gbps')) assert_equivalence(50, traffic_utils.get_load_from_rate('5000Mbps')) assert_equivalence(1, traffic_utils.get_load_from_rate('100Mbps')) assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS) + 'pps', avg_frame_size=1518)) assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS * 2) + 'pps', avg_frame_size=1518, line_rate='20Gbps')) """ @pytest.fixture def traffic_client(monkeypatch): def mock_init(self, *args, **kwargs): self.run_config = { 'bidirectional': False, 'l2frame_size': '64', 'duration_sec': 30, 'rates': [{'rate_percent': '10'}, {'rate_pps': '1'}] } def mock_modify_load(self, load): self.run_config['rates'][0] = {'rate_percent': str(load)} self.current_load = load monkeypatch.setattr(TrafficClient, '__init__', mock_init) monkeypatch.setattr(TrafficClient, 'modify_load', mock_modify_load) return TrafficClient() """ # pylint: enable=pointless-string-statement # ========================================================================= # Other tests # ========================================================================= def setup_module(module): nfvbench.log.setup(mute_stdout=True) def test_no_credentials(): cred = Credentials('/completely/wrong/path/openrc', None, False) if cred.rc_auth_url: # shouldn't get valid data unless user set environment variables assert False else: assert True # 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 assert 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 # pylint: disable=wrong-import-position,ungrouped-imports from nfvbench.traffic_client import Device from nfvbench.traffic_client import IpBlock from nfvbench.traffic_client import TrafficClient from nfvbench.traffic_client import TrafficGeneratorFactory 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, mac): 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, dst_mac=mac) 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' mac = ['00:11:22:33:44:55'] * cc dev0 = create_device(fc, cc, ip0, gip, tggip, step_ip, mac) dev1 = create_device(fc, cc, ip1, gip, tggip, step_ip, mac) 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 ip_range_overlaps(ip0, ip1, flows): tggip = '50.0.0.0' gip = '60.0.0.0' mac = ['00:11:22:33:44:55'] * 10 dev0 = create_device(flows, 10, ip0, gip, tggip, '0.0.0.1', mac) dev1 = create_device(flows, 10, ip1, gip, tggip, '0.0.0.1', mac) dev0.set_destination(dev1) return dev0.ip_range_overlaps() assert not ip_range_overlaps('10.0.0.0', '20.0.0.0', 10000) assert ip_range_overlaps('10.0.0.0', '10.0.1.0', 10000) assert ip_range_overlaps('10.0.0.0', '10.0.1.0', 257) assert ip_range_overlaps('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} res2 = {1: 100, 2: {21: 1000, 22: 200}, 3: None} res3 = {1: 100, 2: {21: 100, 22: 200}, 3: "abc"} assert config_loads("{}", refcfg) == refcfg assert config_loads("{1: 10}", refcfg) == res1 assert config_loads("{2: {21: 1000}}", refcfg) == res2 assert config_loads('{3: "abc"}', refcfg) == res3 # correctly fails # pairs of input string and expected subset (None if identical) fail_pairs = [ ["{4: 0}", None], ["{2: {21: 100, 30: 50}}", "{2: {30: 50}}"], ["{2: {0: 1, 1: 2}, 5: 5}", None], ["{1: 'abc', 2: {21: 0}}", "{1: 'abc'}"], ["{2: 100}", None] ] for fail_pair in fail_pairs: with pytest.raises(Exception) as e_info: config_loads(fail_pair[0], refcfg) expected = fail_pair[1] if expected is None: expected = fail_pair[0] assert expected in e_info.value.message # whitelist keys flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0, 'extra_specs': {'hw:cpu_policy': 'dedicated'}}} new_flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0, 'extra_specs': {'hw:cpu_policy': 'dedicated', 'hw:numa_nodes': 2}}} assert config_loads("{'flavor': {'extra_specs': {'hw:numa_nodes': 2}}}", flavor, whitelist_keys=['alpha', 'extra_specs']) == new_flavor def test_fluentd(): logger = logging.getLogger('fluent-logger') class FluentdConfig(dict): def __getattr__(self, attr): return self.get(attr) fluentd_configs = [ FluentdConfig({ 'logging_tag': 'nfvbench', 'result_tag': 'resultnfvbench', 'ip': '127.0.0.1', 'port': 7081 }), FluentdConfig({ 'logging_tag': 'nfvbench', 'result_tag': 'resultnfvbench', 'ip': '127.0.0.1', 'port': 24224 }), FluentdConfig({ 'logging_tag': None, 'result_tag': 'resultnfvbench', 'ip': '127.0.0.1', 'port': 7082 }), FluentdConfig({ 'logging_tag': 'nfvbench', 'result_tag': None, 'ip': '127.0.0.1', 'port': 7083 }) ] handler = FluentLogHandler(fluentd_configs=fluentd_configs) logger.addHandler(handler) logger.setLevel(logging.INFO) logger.info('test') logger.warning('test %d', 100) try: raise Exception("test") except Exception: logger.exception("got exception") def assert_ndr_pdr(stats, ndr, ndr_dr, pdr, pdr_dr): assert stats['ndr']['rate_percent'] == ndr assert stats['ndr']['stats']['overall']['drop_percentage'] == ndr_dr assert_equivalence(pdr, stats['pdr']['rate_percent']) assert_equivalence(pdr_dr, stats['pdr']['stats']['overall']['drop_percentage']) def get_dummy_tg_config(chain_type, rate): return AttrDict({ 'traffic_generator': {'host_name': 'nfvbench_tg', 'default_profile': 'dummy', 'generator_profile': [{'name': 'dummy', 'tool': 'dummy', 'ip': '127.0.0.1', 'intf_speed': '10Gbps', 'interfaces': [{'port': 0, 'pci': '0.0'}, {'port': 1, 'pci': '0.0'}]}], 'ip_addrs_step': '0.0.0.1', 'ip_addrs': ['10.0.0.0/8', '20.0.0.0/8'], 'tg_gateway_ip_addrs': ['1.1.0.100', '2.2.0.100'], 'tg_gateway_ip_addrs_step': '0.0.0.1', 'gateway_ip_addrs': ['1.1.0.2', '2.2.0.2'], 'gateway_ip_addrs_step': '0.0.0.1', 'mac_addrs_left': None, 'mac_addrs_right': None, 'udp_src_port': None, 'udp_dst_port': None}, 'service_chain': chain_type, 'service_chain_count': 1, 'flow_count': 10, 'vlan_tagging': True, 'no_arp': False, 'duration_sec': 1, 'interval_sec': 1, 'rate': rate, 'check_traffic_time_sec': 200, 'generic_poll_sec': 2, 'measurement': {'NDR': 0.001, 'PDR': 0.1, 'load_epsilon': 0.1}, }) def get_traffic_client(): config = get_dummy_tg_config('PVP', 'ndr_pdr') config['ndr_run'] = True config['pdr_run'] = True config['generator_profile'] = 'dummy' config['single_run'] = False generator_factory = TrafficGeneratorFactory(config) config.generator_config = generator_factory.get_generator_config(config.generator_profile) traffic_client = TrafficClient(config, skip_sleep=True) traffic_client.start_traffic_generator() traffic_client.set_traffic('64', True) return traffic_client def test_ndr_at_lr(): traffic_client = get_traffic_client() tg = traffic_client.gen # this is a perfect sut with no loss at LR tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=100, max_11_tx=100) # tx packets should be line rate for 64B and no drops... assert tg.get_tx_pps_dropped_pps(100) == (LR_64B_PPS, 0) # NDR and PDR should be at 100% traffic_client.ensure_end_to_end() results = traffic_client.get_ndr_and_pdr() assert_ndr_pdr(results, 200.0, 0.0, 200.0, 0.0) def test_ndr_at_50(): traffic_client = get_traffic_client() tg = traffic_client.gen # this is a sut with an NDR of 50% and linear drop rate after NDR up to 20% drops at LR # (meaning that if you send 100% TX, you will only receive 80% RX) # the tg requested TX/actual TX ratio is 1up to 50%, after 50% # is linear up 80% actuak TX when requesting 100% tg.set_response_curve(lr_dr=20, ndr=50, max_actual_tx=80, max_11_tx=50) # tx packets should be half line rate for 64B and no drops... assert tg.get_tx_pps_dropped_pps(50) == (LR_64B_PPS / 2, 0) # at 100% TX requested, actual TX is 80% where the drop rate is 3/5 of 20% of the actual TX assert tg.get_tx_pps_dropped_pps(100) == (int(LR_64B_PPS * 0.8), int(LR_64B_PPS * 0.8 * 0.6 * 0.2)) results = traffic_client.get_ndr_and_pdr() assert_ndr_pdr(results, 100.0, 0.0, 100.781, 0.09374) def test_ndr_pdr_low_cpu(): traffic_client = get_traffic_client() tg = traffic_client.gen # This test is for the case where the TG is underpowered and cannot send fast enough for the NDR # true NDR=40%, actual TX at 50% = 30%, actual measured DR is 0% # The ndr/pdr should bail out with a warning and a best effort measured NDR of 30% tg.set_response_curve(lr_dr=50, ndr=40, max_actual_tx=60, max_11_tx=0) # tx packets should be 30% at requested half line rate for 64B and no drops... assert tg.get_tx_pps_dropped_pps(50) == (int(LR_64B_PPS * 0.3), 0) results = traffic_client.get_ndr_and_pdr() assert results # import pprint # pp = pprint.PrettyPrinter(indent=4) # pp.pprint(results) import nfvbench.nfvbench def test_no_openstack(): config = get_dummy_tg_config('EXT', '1000pps') config.openrc_file = None old_argv = sys.argv sys.argv = [old_argv[0], '-c', json.dumps(config)] nfvbench.nfvbench.main() sys.argv = old_argv