diff options
-rw-r--r-- | conf/03_traffic.conf | 31 | ||||
-rw-r--r-- | conf/__init__.py | 13 | ||||
-rw-r--r-- | conf/integration/01_testcases.conf | 159 | ||||
-rw-r--r-- | core/results/results_constants.py | 4 | ||||
-rw-r--r-- | core/traffic_controller.py | 7 | ||||
-rw-r--r-- | docs/testing/developer/devguide/design/vswitchperf_design.rst | 24 | ||||
-rw-r--r-- | docs/testing/user/configguide/installation.rst | 23 | ||||
-rw-r--r-- | docs/testing/user/configguide/trafficgen.rst | 14 | ||||
-rw-r--r-- | docs/testing/user/userguide/teststeps.rst | 4 | ||||
-rw-r--r-- | testcases/testcase.py | 5 | ||||
-rwxr-xr-x | tools/pkt_gen/ixia/ixia.py | 8 | ||||
-rwxr-xr-x | tools/pkt_gen/ixnet/ixnet.py | 8 | ||||
-rw-r--r-- | tools/pkt_gen/trex/trex.py | 70 | ||||
-rw-r--r-- | tools/tasks.py | 33 | ||||
-rw-r--r-- | tools/teststepstools.py | 17 | ||||
-rw-r--r-- | vswitches/ovs_dpdk_vhost.py | 5 | ||||
-rw-r--r-- | vswitches/ovs_vanilla.py | 6 | ||||
-rw-r--r-- | vswitches/vpp_dpdk_vhost.py | 3 |
18 files changed, 400 insertions, 34 deletions
diff --git a/conf/03_traffic.conf b/conf/03_traffic.conf index 3c7bd2f5..67318893 100644 --- a/conf/03_traffic.conf +++ b/conf/03_traffic.conf @@ -147,6 +147,30 @@ LOG_FILE_TRAFFIC_GEN = 'traffic-gen.log' # congestion (DEI header field). # Data type: int (NOTE: must fit to 1 bit) # Default value: 0 +# 'capture' - A dictionary with traffic capture configuration. +# NOTE: It is supported only by T-Rex traffic generator. +# 'enabled' - Specifies if traffic should be captured +# Data type: bool +# Default value: False +# 'tx_ports' - A list of ports, where frames transmitted towards DUT will +# be captured. Ports have numbers 0 and 1. TX packet capture +# is disabled if list of ports is empty. +# Data type: list +# Default value: [0] +# 'rx_ports' - A list of ports, where frames received from DUT will +# be captured. Ports have numbers 0 and 1. RX packet capture +# is disabled if list of ports is empty. +# Data type: list +# Default value: [1] +# 'count' - A number of frames to be captured. The same count value +# is applied to both TX and RX captures. +# Data type: int +# Default value: 1 +# 'filter' - An expression used to filter TX and RX packets. It uses the same +# syntax as pcap library. See pcap-filter man page for additional +# details. +# Data type: str +# Default value: '' TRAFFIC = { 'traffic_type' : 'rfc2544_throughput', 'frame_rate' : 100, @@ -179,6 +203,13 @@ TRAFFIC = { 'priority': 0, 'cfi': 0, }, + 'capture': { + 'enabled': False, + 'tx_ports' : [0], + 'rx_ports' : [1], + 'count': 1, + 'filter': '', + }, } #path to traffic generators directory. diff --git a/conf/__init__.py b/conf/__init__.py index a7c0ee5d..d5d26757 100644 --- a/conf/__init__.py +++ b/conf/__init__.py @@ -124,6 +124,13 @@ class Settings(object): if name is not None and value is not None: super(Settings, self).__setattr__(name, value) + def resetValue(self, attr): + """If parameter was overridden by TEST_PARAMS, then it will + be set to its original value. + """ + if attr in self.__dict__['TEST_PARAMS']: + self.__dict__['TEST_PARAMS'].pop(attr) + def load_from_file(self, path): """Update ``settings`` with values found in module at ``path``. """ @@ -324,6 +331,12 @@ class Settings(object): assert value == self.__dict__[name] return True + def validate_resetValue(self, dummy_result, attr): + """Verifies, that value was correctly reset + """ + return 'TEST_PARAMS' not in self.__dict__ or \ + attr not in self.__dict__['TEST_PARAMS'] + settings = Settings() def get_test_param(key, default=None): diff --git a/conf/integration/01_testcases.conf b/conf/integration/01_testcases.conf index dfc8a4c2..bb2809b8 100644 --- a/conf/integration/01_testcases.conf +++ b/conf/integration/01_testcases.conf @@ -1000,6 +1000,165 @@ INTEGRATION_TESTS = [ # END of VPP tests used by VERIFY and MERGE jobs by OPNFV Jenkins # + # + # Examples of functional testcases with traffic capture validation + # + # Capture Example 1 - Traffic capture inside VM (PVP scenario) + # This TestCase will modify VLAN ID set by the traffic generator to the new value. + # Correct VLAN ID settings is verified by inspection of captured frames. + { + "Name": "capture_pvp_modify_vid", + "Deployment": "pvp", + "Description": "Test and verify VLAN ID modification by Open vSwitch", + "Parameters" : { + "VSWITCH" : "OvsDpdkVhost", # works also for Vanilla OVS + "TRAFFICGEN_DURATION" : 5, + "TRAFFIC" : { + "traffic_type" : "rfc2544_continuous", + "frame_rate" : 100, + 'vlan': { + 'enabled': True, + 'id': 8, + 'priority': 1, + 'cfi': 0, + }, + }, + "GUEST_LOOPBACK" : ['linux_bridge'], + }, + "TestSteps": [ + # replace original flows with vlan ID modification + ['!vswitch', 'add_flow', 'br0', {'in_port': '1', 'actions': ['mod_vlan_vid:4','output:3']}], + ['!vswitch', 'add_flow', 'br0', {'in_port': '2', 'actions': ['mod_vlan_vid:4','output:4']}], + ['vswitch', 'dump_flows', 'br0'], + # verify that received frames have modified vlan ID + ['VNF0', 'execute_and_wait', 'tcpdump -i eth0 -c 5 -w dump.pcap vlan 4 &'], + ['trafficgen', 'send_traffic',{}], + ['!VNF0', 'execute_and_wait', 'tcpdump -qer dump.pcap vlan 4 2>/dev/null | wc -l','|^(\d+)$'], + ['tools', 'assert', '#STEP[-1][0] == 5'], + ], + }, +] +# Capture Example 2 - Setup with 2 NICs, where traffic is captured after it is +# processed by NIC under the test (2nd NIC). See documentation for further details. +# This TestCase will strip VLAN headers from traffic sent by the traffic generator. +# The removal of VLAN headers is verified by inspection of captured frames. +# +# NOTE: This setup expects a DUT with two NICs with two ports each. First NIC is +# connected to the traffic generator (standard VSPERF setup). Ports of a second NIC +# are interconnected by a patch cable. PCI addresses of all four ports have to be +# properly configured in the WHITELIST_NICS parameter. +_CAPTURE_P2P2P_OVS_ACTION = '' +_CAPTURE_P2P2P_SETUP = [ + # restore original NICS configuration, so we can refer to NICS elements + ['settings', 'resetValue', 'WHITELIST_NICS'], + ['settings', 'resetValue', 'NICS'], + # create and configure two bridges to forward traffic through NIC under + # the test and back to the traffic generator + # 1st bridge: + ['vswitch', 'add_switch', 'br0'], + ['tools', 'exec_shell', 'sudo ip addr flush dev $NICS[0]["device"]'], + ['tools', 'exec_shell', 'sudo ip link set dev $NICS[0]["device"] up'], + ['tools', 'exec_shell', '$TOOLS["ovs-vsctl"] add-port br0 $NICS[0]["device"]'], + ['tools', 'exec_shell', 'sudo $TOOLS["bind-tool"] --bind igb_uio $NICS[3]["pci"]'], + ['tools', 'exec_shell', '$TOOLS["ovs-vsctl"] add-port br0 dpdk0 -- ' + 'set Interface dpdk0 type=dpdk options:dpdk-devargs=$NICS[3]["pci"]'], + ['tools', 'exec_shell', '$TOOLS["ovs-ofctl"] add-flow br0 in_port=1,action=' + '$_CAPTURE_P2P2P_OVS_ACTION,output:2'], + # 2nd bridge: + ['vswitch', 'add_switch', 'br1'], + ['tools', 'exec_shell', 'sudo ip addr flush dev $NICS[2]["device"]'], + ['tools', 'exec_shell', 'sudo ip link set dev $NICS[2]["device"] up'], + ['tools', 'exec_shell', '$TOOLS["ovs-vsctl"] add-port br1 $NICS[2]["device"]'], + ['tools', 'exec_shell', 'sudo ip addr flush dev $NICS[1]["device"]'], + ['tools', 'exec_shell', 'sudo ip link set dev $NICS[1]["device"] up'], + ['tools', 'exec_shell', '$TOOLS["ovs-vsctl"] add-port br1 $NICS[1]["device"]'], + ['vswitch', 'add_flow', 'br1', {'in_port': '1', 'actions': ['output:2']}], + # log flow details + ['vswitch', 'dump_flows', 'br0'], + ['vswitch', 'dump_flows', 'br1'], +] +INTEGRATION_TESTS += [ + { + "Name": "capture_p2p2p_strip_vlan_ovs", + "Deployment": "clean", + "Description": "P2P Continuous Stream", + "Parameters" : { + "_CAPTURE_P2P2P_OVS_ACTION" : 'strip_vlan', + "TRAFFIC" : { + "bidir" : "False", + "traffic_type" : "rfc2544_continuous", + "frame_rate" : 100, + 'l2': { + 'srcmac': "ca:fe:00:00:00:00", + 'dstmac': "00:00:00:00:00:01" + }, + 'vlan': { + 'enabled': True, + 'id': 8, + 'priority': 1, + 'cfi': 0, + }, + }, + # suppress DPDK configuration, so physical interfaces are not bound to DPDK driver + 'WHITELIST_NICS' : [], + 'NICS' : [], + }, + "TestSteps": _CAPTURE_P2P2P_SETUP + [ + # capture traffic after processing by NIC under the test (after possible egress HW offloading) + ['tools', 'exec_shell_background', 'tcpdump -i $NICS[2]["device"] -c 5 -w capture.pcap ' + 'ether src $TRAFFIC["l2"]["srcmac"]'], + ['trafficgen', 'send_traffic', {}], + ['vswitch', 'dump_flows', 'br0'], + ['vswitch', 'dump_flows', 'br1'], + # there must be 5 captured frames... + ['tools', 'exec_shell', 'tcpdump -r capture.pcap | wc -l', '|^(\d+)$'], + ['tools', 'assert', '#STEP[-1][0] == 5'], + # ...but no vlan headers + ['tools', 'exec_shell', 'tcpdump -r capture.pcap vlan | wc -l', '|^(\d+)$'], + ['tools', 'assert', '#STEP[-1][0] == 0'], + ], + }, + # Capture Example 3 - Traffic capture by traffic generator. + # This TestCase uses OVS flow to add VLAN tag with given ID into every + # frame send by traffic generator. Correct frame modificaiton is verified by + # inspection of packet capture received by T-Rex. + { + "Name": "capture_p2p_add_vlan_ovs_trex", + "Deployment": "clean", + "Description": "OVS: Test VLAN tag modification and verify it by traffic capture", + "vSwitch" : "OvsDpdkVhost", # works also for Vanilla OVS + "Parameters" : { + "TRAFFICGEN" : "Trex", + "TRAFFICGEN_DURATION" : 5, + "TRAFFIC" : { + "traffic_type" : "rfc2544_continuous", + "frame_rate" : 100, + # enable capture of five RX frames + 'capture': { + 'enabled': True, + 'tx_ports' : [], + 'rx_ports' : [1], + 'count' : 5, + }, + }, + }, + "TestSteps" : STEP_VSWITCH_P2P_INIT + [ + # replace standard L2 flows by flows, which will add VLAN tag with ID 3 + ['!vswitch', 'add_flow', 'int_br0', {'in_port': '1', 'actions': ['mod_vlan_vid:3','output:2']}], + ['!vswitch', 'add_flow', 'int_br0', {'in_port': '2', 'actions': ['mod_vlan_vid:3','output:1']}], + ['vswitch', 'dump_flows', 'int_br0'], + ['trafficgen', 'send_traffic', {}], + ['trafficgen', 'get_results'], + # verify that captured frames have vlan tag with ID 3 + ['tools', 'exec_shell', 'tcpdump -qer $RESULTS_PATH/#STEP[-1][0]["capture_rx"] vlan 3 ' + '2>/dev/null | wc -l', '|^(\d+)$'], + # number of received frames with expected VLAN id must match the number of captured frames + ['tools', 'assert', '#STEP[-1][0] == 5'], + ] + STEP_VSWITCH_P2P_FINIT, + }, + # + # End of examples of functional testcases with traffic capture validation + # ] # Example of TC definition with exact vSwitch, VNF and TRAFFICGEN values. diff --git a/core/results/results_constants.py b/core/results/results_constants.py index ef2df847..967adbf9 100644 --- a/core/results/results_constants.py +++ b/core/results/results_constants.py @@ -69,6 +69,10 @@ class ResultsConstants(object): TEST_START_TIME = "start_time" TEST_STOP_TIME = "stop_time" + # files with traffic capture + CAPTURE_TX = "capture_tx" + CAPTURE_RX = "capture_rx" + @staticmethod def get_traffic_constants(): """Method returns all Constants used to store results. diff --git a/core/traffic_controller.py b/core/traffic_controller.py index d6e7629c..de82dddf 100644 --- a/core/traffic_controller.py +++ b/core/traffic_controller.py @@ -43,6 +43,7 @@ class TrafficController(object): self._duration = None self._lossrate = None self._packet_sizes = None + self._connected = False self._mode = str(settings.getValue('mode')).lower() self._results = [] @@ -51,6 +52,10 @@ class TrafficController(object): """Set configuration values just before test execution so they can be changed during runtime by test steps. """ + if not self._connected: + self._traffic_gen_class.connect() + self._connected = True + self._duration = int(settings.getValue('TRAFFICGEN_DURATION')) self._lossrate = float(settings.getValue('TRAFFICGEN_LOSSRATE')) self._packet_sizes = settings.getValue('TRAFFICGEN_PKT_SIZES') @@ -62,7 +67,7 @@ class TrafficController(object): def __enter__(self): """Call initialisation function. """ - self._traffic_gen_class.connect() + pass def __exit__(self, type_, value, traceback): """Stop traffic, clean up. diff --git a/docs/testing/developer/devguide/design/vswitchperf_design.rst b/docs/testing/developer/devguide/design/vswitchperf_design.rst index 33051493..96ffcf62 100644 --- a/docs/testing/developer/devguide/design/vswitchperf_design.rst +++ b/docs/testing/developer/devguide/design/vswitchperf_design.rst @@ -415,6 +415,30 @@ Detailed description of ``TRAFFIC`` dictionary items follows: congestion (DEI header field). Data type: int (NOTE: must fit to 1 bit) Default value: 0 + 'capture' - A dictionary with traffic capture configuration. + NOTE: It is supported only by T-Rex traffic generator. + 'enabled' - Specifies if traffic should be captured + Data type: bool + Default value: False + 'tx_ports' - A list of ports, where frames transmitted towards DUT will + be captured. Ports have numbers 0 and 1. TX packet capture + is disabled if list of ports is empty. + Data type: list + Default value: [0] + 'rx_ports' - A list of ports, where frames received from DUT will + be captured. Ports have numbers 0 and 1. RX packet capture + is disabled if list of ports is empty. + Data type: list + Default value: [1] + 'count' - A number of frames to be captured. The same count value + is applied to both TX and RX captures. + Data type: int + Default value: 1 + 'filter' - An expression used to filter TX and RX packets. It uses the same + syntax as pcap library. See pcap-filter man page for additional + details. + Data type: str + Default value: '' .. _configuration-of-guest-options: diff --git a/docs/testing/user/configguide/installation.rst b/docs/testing/user/configguide/installation.rst index e98976bf..7f4d640b 100644 --- a/docs/testing/user/configguide/installation.rst +++ b/docs/testing/user/configguide/installation.rst @@ -136,13 +136,19 @@ The test suite requires Python 3.3 or newer and relies on a number of other system and python packages. These need to be installed for the test suite to function. +Updated kernel and certain development packages are required by DPDK, +OVS (especially Vanilla OVS) and QEMU. It is necessary to check if the +versions of these packages are not being **held-back** and if the +DNF/APT/YUM configuration does not prevent their modification, by +enforcing settings such as **"exclude-kernel"**. + Installation of required packages, preparation of Python 3 virtual environment and compilation of OVS, DPDK and QEMU is performed by -script **systems/build_base_machine.sh**. It should be executed under +script **systems/build_base_machine.sh**. It should be executed under the user account, which will be used for vsperf execution. **NOTE:** Password-less sudo access must be configured for given -user account before script is executed. +user account before the script is executed. .. code:: bash @@ -156,13 +162,14 @@ automatically. Script **build_base_machine.sh** will install all the vsperf dependencies in terms of system packages, Python 3.x and required Python modules. In case of CentOS 7 or RHEL it will install Python 3.3 from an additional -repository provided by Software Collections (`a link`_). Installation script +repository provided by Software Collections (`a link`_). The installation script will also use `virtualenv`_ to create a vsperf virtual environment, which is -isolated from the default Python environment. This environment will reside in a -directory called **vsperfenv** in $HOME. It will ensure, that system wide Python -installation is not modified or broken by VSPERF installation. The complete list -of Python packages installed inside virtualenv can be found at file -``requirements.txt``, which is located at vswitchperf repository. +isolated from the default Python environment, using the Python3 package located +in **/usr/bin/python3**. This environment will reside in a directory called +**vsperfenv** in $HOME. It will ensure, that system wide Python installation + is not modified or broken by VSPERF installation. The complete list of Python +packages installed inside virtualenv can be found in the file +``requirements.txt``, which is located at the vswitchperf repository. **NOTE:** For RHEL 7.3 Enterprise and CentOS 7.3 OVS Vanilla is not built from upstream source due to kernel incompatibilities. Please see the diff --git a/docs/testing/user/configguide/trafficgen.rst b/docs/testing/user/configguide/trafficgen.rst index 91c4084e..33824486 100644 --- a/docs/testing/user/configguide/trafficgen.rst +++ b/docs/testing/user/configguide/trafficgen.rst @@ -44,7 +44,8 @@ and is configured as follows: 'stream_type' : 'L4', 'pre_installed_flows' : 'No', # used by vswitch implementation 'flow_type' : 'port', # used by vswitch implementation - + 'flow_control' : False, # supported only by IxNet + 'learning_frames' : True, # supported only by IxNet 'l2': { 'framesize': 64, 'srcmac': '00:00:00:00:00:00', @@ -67,6 +68,13 @@ and is configured as follows: 'priority': 0, 'cfi': 0, }, + 'capture': { + 'enabled': False, + 'tx_ports' : [0], + 'rx_ports' : [1], + 'count': 1, + 'filter': '', + }, } The framesize parameter can be overridden from the configuration @@ -783,6 +791,10 @@ It is neccesary for proper connection between Trex server and VSPERF. cd trex-core/scripts/ ./t-rex-64 -i +**NOTE:** Please check your firewall settings at both DUT and T-Rex server. +Firewall must allow a connection from DUT (VSPERF) to the T-Rex server running +at TCP port 4501. + For additional information about Trex stateless mode see Trex stateless documentation: https://trex-tgn.cisco.com/trex/doc/trex_stateless.html diff --git a/docs/testing/user/userguide/teststeps.rst b/docs/testing/user/userguide/teststeps.rst index 40cc732c..08c95311 100644 --- a/docs/testing/user/userguide/teststeps.rst +++ b/docs/testing/user/userguide/teststeps.rst @@ -271,7 +271,9 @@ of supported objects and their most common functions follows: in case that condition is not ``True`` * ``Eval expression`` - evaluates given expression as a python code and returns its result - * ``Exec_Shell command`` - executes a shell command + * ``Exec_Shell command`` - executes a shell command and wait until it finishes + * ``Exec_Shell_Background command`` - executes a shell command at background; + Command will be automatically terminated at the end of testcase execution. * ``Exec_Python code`` - executes a python code diff --git a/testcases/testcase.py b/testcases/testcase.py index b3300b89..991c2890 100644 --- a/testcases/testcase.py +++ b/testcases/testcase.py @@ -169,7 +169,7 @@ class TestCase(object): self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3') self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4') self._traffic['l2']['dstmac'] = S.getValue('NICS')[1]['mac'] - elif len(S.getValue('NICS')) and \ + elif len(S.getValue('NICS')) >= 2 and \ (S.getValue('NICS')[0]['type'] == 'vf' or S.getValue('NICS')[1]['type'] == 'vf'): mac1 = S.getValue('NICS')[0]['mac'] @@ -265,6 +265,9 @@ class TestCase(object): # Stop all VNFs started by TestSteps in case that something went wrong self.step_stop_vnfs() + # Stop all processes executed by testcase + tasks.terminate_all_tasks(self._logger) + # umount hugepages if mounted self._umount_hugepages() diff --git a/tools/pkt_gen/ixia/ixia.py b/tools/pkt_gen/ixia/ixia.py index e768be06..d4ca56f2 100755 --- a/tools/pkt_gen/ixia/ixia.py +++ b/tools/pkt_gen/ixia/ixia.py @@ -111,6 +111,11 @@ def _build_set_cmds(values, prefix='dict set'): yield subkey continue + if isinstance(value, list): + value = '{{{}}}'.format(' '.join(str(x) for x in value)) + yield ' '.join([prefix, 'set', key, value]).strip() + continue + # tcl doesn't recognise the strings "True" or "False", only "1" # or "0". Special case to convert them if isinstance(value, bool): @@ -118,6 +123,9 @@ def _build_set_cmds(values, prefix='dict set'): else: value = str(value) + if isinstance(value, str) and not value: + value = '{}' + if prefix: yield ' '.join([prefix, key, value]).strip() else: diff --git a/tools/pkt_gen/ixnet/ixnet.py b/tools/pkt_gen/ixnet/ixnet.py index b8fb1879..d1ba9096 100755 --- a/tools/pkt_gen/ixnet/ixnet.py +++ b/tools/pkt_gen/ixnet/ixnet.py @@ -127,6 +127,11 @@ def _build_set_cmds(values, prefix='dict set'): yield subkey continue + if isinstance(value, list): + value = '{{{}}}'.format(' '.join(str(x) for x in value)) + yield ' '.join([prefix, 'set', key, value]).strip() + continue + # tcl doesn't recognise the strings "True" or "False", only "1" # or "0". Special case to convert them if isinstance(value, bool): @@ -134,6 +139,9 @@ def _build_set_cmds(values, prefix='dict set'): else: value = str(value) + if isinstance(value, str) and not value: + value = '{}' + if prefix: yield ' '.join([prefix, key, value]).strip() else: diff --git a/tools/pkt_gen/trex/trex.py b/tools/pkt_gen/trex/trex.py index 82118f6f..cfe54b78 100644 --- a/tools/pkt_gen/trex/trex.py +++ b/tools/pkt_gen/trex/trex.py @@ -20,6 +20,7 @@ import logging import subprocess import sys import time +import os from collections import OrderedDict # pylint: disable=unused-import import netaddr @@ -91,11 +92,11 @@ class Trex(ITrafficGenerator): the configuration file ''' self._stlclient = STLClient() - self._logger.info("TREX: In Trex connect method...") + self._logger.info("T-Rex: In Trex connect method...") if self._trex_host_ip_addr: cmd_ping = "ping -c1 " + self._trex_host_ip_addr else: - raise RuntimeError('TREX: Trex host not defined') + raise RuntimeError('T-Rex: Trex host not defined') ping = subprocess.Popen(cmd_ping, shell=True, stderr=subprocess.PIPE) output, error = ping.communicate() @@ -103,7 +104,7 @@ class Trex(ITrafficGenerator): if ping.returncode: self._logger.error(error) self._logger.error(output) - raise RuntimeError('TREX: Cannot ping Trex host at ' + \ + raise RuntimeError('T-Rex: Cannot ping Trex host at ' + \ self._trex_host_ip_addr) connect_trex = "ssh " + self._trex_user + \ @@ -122,13 +123,18 @@ class Trex(ITrafficGenerator): self._logger.error(error) self._logger.error(output) raise RuntimeError( - 'TREX: Cannot locate Trex program at %s within %s' \ + 'T-Rex: Cannot locate Trex program at %s within %s' \ % (self._trex_host_ip_addr, self._trex_base_dir)) - self._stlclient = STLClient(username=self._trex_user, server=self._trex_host_ip_addr, - verbose_level=0) - self._stlclient.connect() - self._logger.info("TREX: Trex host successfully found...") + try: + self._stlclient = STLClient(username=self._trex_user, server=self._trex_host_ip_addr, + verbose_level=0) + self._stlclient.connect() + except STLError: + raise RuntimeError('T-Rex: Cannot connect to T-Rex server. Please check if it is ' + 'running and that firewall allows connection to TCP port 4501.') + + self._logger.info("T-Rex: Trex host successfully found...") def disconnect(self): """Disconnect from the traffic generator. @@ -140,7 +146,7 @@ class Trex(ITrafficGenerator): :returns: None """ - self._logger.info("TREX: In trex disconnect method") + self._logger.info("T-Rex: In trex disconnect method") self._stlclient.disconnect(stop_traffic=True, release_ports=True) @staticmethod @@ -245,11 +251,16 @@ class Trex(ITrafficGenerator): return (stream_1, stream_2, stream_1_lat, stream_2_lat) - def generate_traffic(self, traffic, duration): + def generate_traffic(self, traffic, duration, disable_capture=False): """The method that generate a stream """ my_ports = [0, 1] + + # initialize ports self._stlclient.reset(my_ports) + self._stlclient.remove_all_captures() + self._stlclient.set_service_mode(ports=my_ports, enabled=False) + ports_info = self._stlclient.get_port_info(my_ports) # for SR-IOV if settings.getValue('TRAFFICGEN_TREX_PROMISCUOUS'): @@ -264,10 +275,35 @@ class Trex(ITrafficGenerator): self._stlclient.add_streams(stream_1_lat, ports=[0]) self._stlclient.add_streams(stream_2_lat, ports=[1]) + # enable traffic capture if requested + pcap_id = {} + if traffic['capture']['enabled'] and not disable_capture: + for ports in ['tx_ports', 'rx_ports']: + if traffic['capture'][ports]: + pcap_dir = ports[:2] + self._logger.info("T-Rex starting %s traffic capture", pcap_dir.upper()) + capture = {ports : traffic['capture'][ports], + 'limit' : traffic['capture']['count'], + 'bpf_filter' : traffic['capture']['filter']} + self._stlclient.set_service_mode(ports=traffic['capture'][ports], enabled=True) + pcap_id[pcap_dir] = self._stlclient.start_capture(**capture) + self._stlclient.clear_stats() - self._stlclient.start(ports=[0, 1], force=True, duration=duration) - self._stlclient.wait_on_traffic(ports=[0, 1]) + self._stlclient.start(ports=my_ports, force=True, duration=duration) + self._stlclient.wait_on_traffic(ports=my_ports) stats = self._stlclient.get_stats(sync_now=True) + + # export captured data into pcap file if possible + if pcap_id: + for pcap_dir in pcap_id: + pcap_file = 'capture_{}.pcap'.format(pcap_dir) + self._stlclient.stop_capture(pcap_id[pcap_dir]['id'], + os.path.join(settings.getValue('RESULTS_PATH'), pcap_file)) + stats['capture_{}'.format(pcap_dir)] = pcap_file + self._logger.info("T-Rex writing %s traffic capture into %s", pcap_dir.upper(), pcap_file) + # disable service mode for all ports used by Trex + self._stlclient.set_service_mode(ports=my_ports, enabled=False) + return stats @staticmethod @@ -325,6 +361,11 @@ class Trex(ITrafficGenerator): result[ResultsConstants.MIN_LATENCY_NS] = 'Unknown' result[ResultsConstants.MAX_LATENCY_NS] = 'Unknown' result[ResultsConstants.AVG_LATENCY_NS] = 'Unknown' + + if 'capture_tx' in stats: + result[ResultsConstants.CAPTURE_TX] = stats['capture_tx'] + if 'capture_rx' in stats: + result[ResultsConstants.CAPTURE_RX] = stats['capture_rx'] return result def learning_packets(self, traffic): @@ -336,7 +377,9 @@ class Trex(ITrafficGenerator): self._logger.info("T-Rex sending learning packets") learning_thresh_traffic = copy.deepcopy(traffic) learning_thresh_traffic["frame_rate"] = 1 - self.generate_traffic(learning_thresh_traffic, settings.getValue("TRAFFICGEN_TREX_LEARNING_DURATION")) + self.generate_traffic(learning_thresh_traffic, + settings.getValue("TRAFFICGEN_TREX_LEARNING_DURATION"), + disable_capture=True) self._logger.info("T-Rex finished learning packets") time.sleep(3) # allow packets to complete before starting test traffic @@ -403,6 +446,7 @@ class Trex(ITrafficGenerator): if settings.getValue('TRAFFICGEN_TREX_LEARNING_MODE'): self.learning_packets(traffic) + self._logger.info("T-Rex sending traffic") stats = self.generate_traffic(traffic, duration) return self.calculate_results(stats) diff --git a/tools/tasks.py b/tools/tasks.py index 4179291f..18f4d712 100644 --- a/tools/tasks.py +++ b/tools/tasks.py @@ -114,6 +114,16 @@ def run_task(cmd, logger, msg=None, check_error=False): return ('\n'.join(sout.decode(my_encoding).strip() for sout in stdout), ('\n'.join(sout.decode(my_encoding).strip() for sout in stderr))) +def update_pids(pid): + """update list of running pids, so they can be terminated at the end + """ + try: + pids = settings.getValue('_EXECUTED_PIDS') + pids.append(pid) + except AttributeError: + pids = [pid] + settings.setValue('_EXECUTED_PIDS', pids) + def run_background_task(cmd, logger, msg): """Run task in background and log when started. @@ -132,6 +142,8 @@ def run_background_task(cmd, logger, msg): proc = subprocess.Popen(map(os.path.expanduser, cmd), stdout=_get_stdout(), bufsize=0) + update_pids(proc.pid) + return proc.pid @@ -174,14 +186,13 @@ def terminate_task_subtree(pid, signal='-15', sleep=10, logger=None): :param logger: Logger to write details to """ try: - output = subprocess.check_output("pgrep -P " + str(pid), shell=True).decode().rstrip('\n') + children = subprocess.check_output("pgrep -P " + str(pid), shell=True).decode().rstrip('\n').split() except subprocess.CalledProcessError: - output = "" + children = [] terminate_task(pid, signal, sleep, logger) # just for case children were kept alive - children = output.split('\n') for child in children: terminate_task(child, signal, sleep, logger) @@ -208,6 +219,22 @@ def terminate_task(pid, signal='-15', sleep=10, logger=None): if signal.lstrip('-').upper() not in ('9', 'KILL', 'SIGKILL') and systeminfo.pid_isalive(pid): terminate_task(pid, '-9', sleep, logger) + pids = settings.getValue('_EXECUTED_PIDS') + if pid in pids: + pids.remove(pid) + settings.setValue('_EXECUTED_PIDS', pids) + +def terminate_all_tasks(logger): + """Terminate all processes executed by vsperf, just for case they were not + terminated by standard means. + """ + pids = settings.getValue('_EXECUTED_PIDS') + if pids: + logger.debug('Following processes will be terminated: %s', pids) + for pid in pids: + terminate_task_subtree(pid, logger=logger) + settings.setValue('_EXECUTED_PIDS', []) + class Process(object): """Control an instance of a long-running process. diff --git a/tools/teststepstools.py b/tools/teststepstools.py index 639e3437..33db8f79 100644 --- a/tools/teststepstools.py +++ b/tools/teststepstools.py @@ -19,6 +19,7 @@ import logging import subprocess import locale from tools.functions import filter_output +from tools.tasks import run_background_task _LOGGER = logging.getLogger(__name__) @@ -102,3 +103,19 @@ class TestStepsTools(object): """ validate result of shell `command' execution """ return result is not None + + @staticmethod + def Exec_Shell_Background(command): + """ Execute a shell `command' at the background and return its PID id + """ + try: + pid = run_background_task(command.split(), _LOGGER, "Background task: {}".format(command)) + return pid + except OSError: + return None + + @staticmethod + def validate_Exec_Shell_Background(result, dummy_command, dummy_regex=None): + """ validate result of shell `command' execution on the background + """ + return result is not None diff --git a/vswitches/ovs_dpdk_vhost.py b/vswitches/ovs_dpdk_vhost.py index 11b32c88..6deb0c25 100644 --- a/vswitches/ovs_dpdk_vhost.py +++ b/vswitches/ovs_dpdk_vhost.py @@ -114,12 +114,15 @@ class OvsDpdkVhost(IVSwitchOvs): Creates a port of type dpdk. The new port is named dpdk<n> where n is an integer starting from 0. """ + _nics = S.getValue('NICS') bridge = self._bridges[switch_name] dpdk_count = self._get_port_count('type=dpdk') + if dpdk_count == len(_nics): + raise RuntimeError("Can't add phy port! There are only {} ports defined " + "by WHITELIST_NICS parameter!".format(len(_nics))) port_name = 'dpdk' + str(dpdk_count) # PCI info. Please note there must be no blank space, eg must be # like 'options:dpdk-devargs=0000:06:00.0' - _nics = S.getValue('NICS') nic_pci = 'options:dpdk-devargs=' + _nics[dpdk_count]['pci'] params = ['--', 'set', 'Interface', port_name, 'type=dpdk', nic_pci] # multi-queue enable diff --git a/vswitches/ovs_vanilla.py b/vswitches/ovs_vanilla.py index 942ddd41..83c52050 100644 --- a/vswitches/ovs_vanilla.py +++ b/vswitches/ovs_vanilla.py @@ -75,10 +75,8 @@ class OvsVanilla(IVSwitchOvs): See IVswitch for general description """ if self._current_id == len(self._ports): - self._logger.error("Can't add port! There are only " + - len(self._ports) + " ports " + - "defined in config!") - raise RuntimeError('Failed to add phy port') + raise RuntimeError("Can't add phy port! There are only {} ports defined " + "by WHITELIST_NICS parameter!".format(len(self._ports))) if not self._ports[self._current_id]: self._logger.error("Can't detect device name for NIC %s", self._current_id) raise ValueError("Invalid device name for %s" % self._current_id) diff --git a/vswitches/vpp_dpdk_vhost.py b/vswitches/vpp_dpdk_vhost.py index c62e28d4..58d6bf51 100644 --- a/vswitches/vpp_dpdk_vhost.py +++ b/vswitches/vpp_dpdk_vhost.py @@ -225,7 +225,8 @@ class VppDpdkVhost(IVSwitch, tasks.Process): vpp_nics = self._get_nic_info(key='Pci') # check if there are any NICs left if len(self._phy_ports) >= len(S.getValue('NICS')): - raise RuntimeError('All available NICs are already configured!') + raise RuntimeError("Can't add phy port! There are only {} ports defined " + "by WHITELIST_NICS parameter!".format(len(S.getValue('NICS')))) nic = S.getValue('NICS')[len(self._phy_ports)] if not nic['pci'] in vpp_nics: |