diff --git a/tests/pfcwd/conftest.py b/tests/pfcwd/conftest.py new file mode 100644 index 0000000000..3521913f29 --- /dev/null +++ b/tests/pfcwd/conftest.py @@ -0,0 +1,75 @@ +import logging +import pytest +from common.fixtures.conn_graph_facts import conn_graph_facts +from files.pfcwd_helper import TrafficPorts, set_pfc_timers, select_test_ports + +logger = logging.getLogger(__name__) + +def pytest_addoption(parser): + """ + Command line args specific for the pfcwd test + + Args: + parser: pytest parser object + + Returns: + None + + """ + parser.addoption('--warm-reboot', action='store', type=bool, default=False, + help='Warm reboot needs to be enabled or not') + +@pytest.fixture(scope="module") +def setup_pfc_test(duthost, ptfhost, conn_graph_facts): + """ + Sets up all the parameters needed for the PFC Watchdog tests + + Args: + duthost: AnsibleHost instance for DUT + ptfhost: AnsibleHost instance for PTF + conn_graph_facts: fixture that contains the parsed topology info + + Yields: + setup_info: dictionary containing pfc timers, generated test ports and selected test ports + """ + mg_facts = duthost.minigraph_facts(host=duthost.hostname)['ansible_facts'] + port_list = mg_facts['minigraph_ports'].keys() + ports = (' ').join(port_list) + neighbors = conn_graph_facts['device_conn'] + dut_facts = duthost.setup()['ansible_facts'] + dut_eth0_ip = dut_facts['ansible_eth0']['ipv4']['address'] + dut_eth0_mac = dut_facts['ansible_eth0']['macaddress'] + vlan_nw = None + + if mg_facts['minigraph_vlans']: + # gather all vlan specific info + vlan_addr = mg_facts['minigraph_vlan_interfaces'][0]['addr'] + vlan_prefix = mg_facts['minigraph_vlan_interfaces'][0]['prefixlen'] + vlan_dev = mg_facts['minigraph_vlan_interfaces'][0]['attachto'] + vlan_ips = duthost.get_ip_in_range(num=1, prefix="{}/{}".format(vlan_addr, vlan_prefix), exclude_ips=[vlan_addr])['ansible_facts']['generated_ips'] + vlan_nw = vlan_ips[0].split('/')[0] + + # set unique MACS to PTF interfaces + ptfhost.script("./scripts/change_mac.sh") + + duthost.shell("ip route flush {}/32".format(vlan_nw)) + duthost.shell("ip route add {}/32 dev {}".format(vlan_nw, vlan_dev)) + + # build the port list for the test + tp_handle = TrafficPorts(mg_facts, neighbors, vlan_nw) + test_ports = tp_handle.build_port_list() + # select a subset of ports from the generated port list + selected_ports = select_test_ports(test_ports) + + setup_info = { 'test_ports': test_ports, + 'selected_test_ports': selected_ports, + 'pfc_timers' : set_pfc_timers() + } + + # set poll interval + duthost.command("pfcwd interval {}".format(setup_info['pfc_timers']['pfc_wd_poll_time'])) + + yield setup_info + + logger.info("--- Starting Pfcwd ---") + duthost.command("pfcwd start_default") diff --git a/tests/pfcwd/files/__init__.py b/tests/pfcwd/files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pfcwd/files/pfcwd_helper.py b/tests/pfcwd/files/pfcwd_helper.py new file mode 100644 index 0000000000..c2fa58e928 --- /dev/null +++ b/tests/pfcwd/files/pfcwd_helper.py @@ -0,0 +1,237 @@ +import datetime +import ipaddress + +class TrafficPorts(object): + """ Generate a list of ports needed for the PFC Watchdog test""" + def __init__(self, mg_facts, neighbors, vlan_nw): + """ + Args: + mg_facts (dict): parsed minigraph info + neighbors (list): 'device_conn' info from connection graph facts + vlan_nw (string): ip in the vlan range specified in the DUT + + """ + self.mg_facts = mg_facts + self.bgp_info = self.mg_facts['minigraph_bgp'] + self.port_idx_info = self.mg_facts['minigraph_port_indices'] + self.pc_info = self.mg_facts['minigraph_portchannels'] + self.vlan_info = self.mg_facts['minigraph_vlans'] + self.neighbors = neighbors + self.vlan_nw = vlan_nw + self.test_ports = dict() + self.pfc_wd_rx_port = None + self.pfc_wd_rx_port_addr = None + self.pfc_wd_rx_neighbor_addr = None + self.pfc_wd_rx_port_id = None + + def build_port_list(self): + """ + Generate a list of ports to be used for the test + + For T0 topology, the port list is built parsing the portchannel and vlan info and for T1, + port list is constructed from the interface info + """ + if self.mg_facts['minigraph_interfaces']: + self.parse_intf_list() + elif self.mg_facts['minigraph_portchannels']: + self.parse_pc_list() + if self.mg_facts['minigraph_vlans']: + self.test_ports.update(self.parse_vlan_list()) + return self.test_ports + + def parse_intf_list(self): + """ + Built the port info from the ports in 'minigraph_interfaces' + + The constructed port info is a dict with a port as the key (transmit port) and value contains + all the info associated with this port (its fanout neighbor, receive port, receive ptf id, + transmit ptf id, neighbor addr etc). The first port in the list is assumed to be the Rx port. + The rest of the ports will use this port as the Rx port while populating their dict + info. The selected Rx port when used as a transmit port will use the next port in + the list as its associated Rx port + """ + pfc_wd_test_port = None + first_pair = False + for intf in self.mg_facts['minigraph_interfaces']: + if ipaddress.ip_address(unicode(intf['addr'])).version != 4: + continue + # first port + if not self.pfc_wd_rx_port: + self.pfc_wd_rx_port = intf['attachto'] + self.pfc_wd_rx_port_addr = intf['addr'] + self.pfc_wd_rx_port_id = self.port_idx_info[self.pfc_wd_rx_port] + elif not pfc_wd_test_port: + # second port + first_pair = True + + # populate info for all ports except the first one + if first_pair or pfc_wd_test_port: + pfc_wd_test_port = intf['attachto'] + pfc_wd_test_port_addr = intf['addr'] + pfc_wd_test_port_id = self.port_idx_info[pfc_wd_test_port] + pfc_wd_test_neighbor_addr = None + + for item in self.bgp_info: + if ipaddress.ip_address(unicode(item['addr'])).version != 4: + continue + if not self.pfc_wd_rx_neighbor_addr and item['peer_addr'] == self.pfc_wd_rx_port_addr: + self.pfc_wd_rx_neighbor_addr = item['addr'] + if item['peer_addr'] == pfc_wd_test_port_addr: + pfc_wd_test_neighbor_addr = item['addr'] + + self.test_ports[pfc_wd_test_port] = {'test_neighbor_addr': pfc_wd_test_neighbor_addr, + 'rx_port': [self.pfc_wd_rx_port], + 'rx_neighbor_addr': self.pfc_wd_rx_neighbor_addr, + 'peer_device': self.neighbors[pfc_wd_test_port]['peerdevice'], + 'test_port_id': pfc_wd_test_port_id, + 'rx_port_id': [self.pfc_wd_rx_port_id], + 'test_port_type': 'interface' + } + # populate info for the first port + if first_pair: + self.test_ports[self.pfc_wd_rx_port] = {'test_neighbor_addr': self.pfc_wd_rx_neighbor_addr, + 'rx_port': [pfc_wd_test_port], + 'rx_neighbor_addr': pfc_wd_test_neighbor_addr, + 'peer_device': self.neighbors[self.pfc_wd_rx_port]['peerdevice'], + 'test_port_id': self.pfc_wd_rx_port_id, + 'rx_port_id': [pfc_wd_test_port_id], + 'test_port_type': 'interface' + } + + first_pair = False + + def parse_pc_list(self): + """ + Built the port info from the ports in portchannel + + The constructed port info is a dict with a port as the key (transmit port) and value contains + all the info associated with this port (its fanout neighbor, receive ports, receive + ptf ids, transmit ptf ids, neighbor portchannel addr, its own portchannel addr etc). + The first port in the list is assumed to be the Rx port. The rest + of the ports will use this port as the Rx port while populating their dict + info. The selected Rx port when used as a transmit port will use the next port in + the list as its associated Rx port + """ + pfc_wd_test_port = None + first_pair = False + for item in self.mg_facts['minigraph_portchannel_interfaces']: + if ipaddress.ip_address(unicode(item['addr'])).version != 4: + continue + pc = item['attachto'] + # first port + if not self.pfc_wd_rx_port: + self.pfc_wd_rx_portchannel = pc + self.pfc_wd_rx_port = self.pc_info[pc]['members'] + self.pfc_wd_rx_port_addr = item['addr'] + self.pfc_wd_rx_port_id = [self.port_idx_info[port] for port in self.pfc_wd_rx_port] + elif not pfc_wd_test_port: + # second port + first_pair = True + + # populate info for all ports except the first one + if first_pair or pfc_wd_test_port: + pfc_wd_test_portchannel = pc + pfc_wd_test_port = self.pc_info[pc]['members'] + pfc_wd_test_port_addr = item['addr'] + pfc_wd_test_port_id = [self.port_idx_info[port] for port in pfc_wd_test_port] + pfc_wd_test_neighbor_addr = None + + for bgp_item in self.bgp_info: + if ipaddress.ip_address(unicode(bgp_item['addr'])).version != 4: + continue + if not self.pfc_wd_rx_neighbor_addr and bgp_item['peer_addr'] == self.pfc_wd_rx_port_addr: + self.pfc_wd_rx_neighbor_addr = bgp_item['addr'] + if bgp_item['peer_addr'] == pfc_wd_test_port_addr: + pfc_wd_test_neighbor_addr = bgp_item['addr'] + + for port in pfc_wd_test_port: + self.test_ports[port] = {'test_neighbor_addr': pfc_wd_test_neighbor_addr, + 'rx_port': self.pfc_wd_rx_port, + 'rx_neighbor_addr': self.pfc_wd_rx_neighbor_addr, + 'peer_device': self.neighbors[port]['peerdevice'], + 'test_port_id': self.port_idx_info[port], + 'rx_port_id': self.pfc_wd_rx_port_id, + 'test_portchannel_members': pfc_wd_test_port_id, + 'test_port_type': 'portchannel' + } + # populate info for the first port + if first_pair: + for port in self.pfc_wd_rx_port: + self.test_ports[port] = {'test_neighbor_addr': self.pfc_wd_rx_neighbor_addr, + 'rx_port': pfc_wd_test_port, + 'rx_neighbor_addr': pfc_wd_test_neighbor_addr, + 'peer_device': self.neighbors[port]['peerdevice'], + 'test_port_id': self.port_idx_info[port], + 'rx_port_id': pfc_wd_test_port_id, + 'test_portchannel_members': self.pfc_wd_rx_port_id, + 'test_port_type': 'portchannel' + } + + first_pair = False + + def parse_vlan_list(self): + """ + Add vlan specific port info to the already populated port info dict. + + Each vlan interface will be the key and value contains all the info associated with this port + (receive fanout neighbor, receive port receive ptf id, transmit ptf id, neighbor addr etc). + + Args: + None + + Returns: + temp_ports (dict): port info constructed from the vlan interfaces + """ + temp_ports = dict() + vlan_members = self.vlan_info[self.vlan_info.keys()[0]]['members'] + for item in vlan_members: + temp_ports[item] = {'test_neighbor_addr': self.vlan_nw, + 'rx_port': self.pfc_wd_rx_port, + 'rx_neighbor_addr': self.pfc_wd_rx_neighbor_addr, + 'peer_device': self.neighbors[item]['peerdevice'], + 'test_port_id': self.port_idx_info[item], + 'rx_port_id': self.pfc_wd_rx_port_id, + 'test_port_type': 'vlan' + } + + return temp_ports + +def set_pfc_timers(): + """ + Set PFC timers + + Args: + None + + Returns: + pfc_timers (dict) + """ + pfc_timers = {'pfc_wd_detect_time': 400, + 'pfc_wd_restore_time': 400, + 'pfc_wd_restore_time_large': 3000, + 'pfc_wd_poll_time': 400 + } + return pfc_timers + + +def select_test_ports(test_ports): + """ + Select a subset of ports from the generated port info + + Args: + test_ports (dict): Constructed port info + + Returns: + selected_ports (dict): random port info or set of ports matching seed + """ + selected_ports = dict() + seed = int(datetime.datetime.today().day) + for key, value in test_ports.items(): + if (int(value['test_port_id']) % 15) == (seed % 15): + selected_ports.update({key:value}) + + if not selected_ports: + random_port = test_ports.keys()[0] + selected_ports[random_port] = test_ports[random_port] + + return selected_ports diff --git a/tests/pfcwd/templates/config_test_ignore_messages b/tests/pfcwd/templates/config_test_ignore_messages new file mode 100644 index 0000000000..b93ffa1530 --- /dev/null +++ b/tests/pfcwd/templates/config_test_ignore_messages @@ -0,0 +1,8 @@ +r, ".* Port counter .* not implemented" +r, ".* Port counter .* not supported" +r, ".* Invalid port counter .*" +r, ".* Unknown.*" +r, ".* SAI_STATUS_ATTR_NOT_SUPPORT.*" +r, ".* snmp.*" +r, ".* Trying to remove nonexisting queue from flex counter .*" +r, ".* ERR ntpd.*routing socket reports: No buffer space available.*" diff --git a/tests/pfcwd/templates/pfc_config_params.json b/tests/pfcwd/templates/pfc_config_params.json new file mode 100644 index 0000000000..000d6029b8 --- /dev/null +++ b/tests/pfcwd/templates/pfc_config_params.json @@ -0,0 +1,42 @@ +{ + "pfc_wd_fwd_action": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 4000, + "pfc_wd_restoration_time": 5000 + }, + "pfc_wd_invalid_action": { + "pfc_wd_action": "invalid", + "pfc_wd_detection_time": 4000, + "pfc_wd_restoration_time": 5000 + }, + "pfc_wd_invalid_detect_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": "400a", + "pfc_wd_restoration_time": 5000 + }, + "pfc_wd_low_detect_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 40, + "pfc_wd_restoration_time": 5000 + }, + "pfc_wd_high_detect_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 4000000, + "pfc_wd_restoration_time": 5000 + }, + "pfc_wd_invalid_restore_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 4000, + "pfc_wd_restoration_time": "500c" + }, + "pfc_wd_low_restore_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 4000, + "pfc_wd_restoration_time": 50 + }, + "pfc_wd_high_restore_time": { + "pfc_wd_action": "forward", + "pfc_wd_detection_time": 4000, + "pfc_wd_restoration_time": 50000000 + } +} diff --git a/tests/pfcwd/test_pfc_config.py b/tests/pfcwd/test_pfc_config.py new file mode 100644 index 0000000000..7c7d765648 --- /dev/null +++ b/tests/pfcwd/test_pfc_config.py @@ -0,0 +1,257 @@ +import json +import os +import pytest +import logging + +from common.helpers.assertions import pytest_assert +from common.plugins.loganalyzer.loganalyzer import LogAnalyzer + +logger = logging.getLogger(__name__) + +DUT_RUN_DIR = "/home/admin/pfc_wd_tests" +TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates") +TMP_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "testrun") +CONFIG_TEST_EXPECT_INVALID_ACTION_RE = ".* Invalid PFC Watchdog action .*" +CONFIG_TEST_EXPECT_INVALID_DETECT_TIME_RE = ".* Failed to parse PFC Watchdog .* detection_time .*" +CONFIG_TEST_EXPECT_INVALID_RESTORE_TIME_RE = ".* Failed to parse PFC Watchdog .* restoration_time .*" + +pytestmark = [pytest.mark.disable_loganalyzer] # disable automatic fixture and invoke within each test + +def create_run_dir(): + """ + Creates a temp run dir 'testrun' within the pfcwd folder + """ + try: + os.mkdir(TMP_DIR) + except OSError as err: + pytest.fail("Failed to create a temp run dir: {}".format(str(err))) + +def generate_cfg_templates(test_port): + """ + Build all the config templates that will be used for the config validation test + + Args: + test_port (string): a random port selected from the test port list + + Returns: + cfg_params (dict): all config templates + """ + create_run_dir() + with open(os.path.join(TEMPLATES_DIR, "pfc_config_params.json"), "r") as read_file: + cfg_params = json.load(read_file) + + for key in cfg_params: + write_file = key + write_params = dict() + write_params["PFC_WD"] = { test_port: { "action": cfg_params[key]["pfc_wd_action"], + "detection_time": cfg_params[key]["pfc_wd_detection_time"], + "restoration_time": cfg_params[key]["pfc_wd_restoration_time"] + } + } + # create individual template files for each test + with open(os.path.join(TMP_DIR, "{}.json".format(write_file)), "w") as wfile: + json.dump(write_params, wfile) + + return cfg_params + +def copy_templates_to_dut(duthost, cfg_params): + """ + Copy all the templates created to the DUT + + Args: + duthost (AnsibleHost): instance + cfg_params (dict): all config templates + + Returns: + None + """ + duthost.shell("mkdir -p {}".format(DUT_RUN_DIR)) + for key in cfg_params: + src_file = os.path.join(TMP_DIR, "{}.json".format(key)) + duthost.copy(src=src_file, dest="{}/{}.json".format(DUT_RUN_DIR, key)) + +def cfg_teardown(duthost): + """ + Cleans up the DUT temp dir and temp dir on the host after the module run + + Args: + duthost (AnsibleHost): instance + + Returns: + None + """ + if os.path.exists(TMP_DIR): + os.system("rm -rf {}".format(TMP_DIR)) + duthost.shell("rm -rf {}".format(DUT_RUN_DIR)) + +@pytest.fixture(scope='class', autouse=True) +def cfg_setup(setup_pfc_test, duthost): + """ + Class level automatic fixture. Prior to the test run, create all the templates + needed for each individual test and copy them on the DUT. + After the all the test cases are done, clean up temp dir on DUT and host + + Args: + setup_pfc_test: module fixture defined in module conftest.py + duthost: instance of AnsibleHost class + """ + setup_info = setup_pfc_test + pfc_wd_test_port = setup_info['test_ports'].keys()[0] + logger.info("Creating json templates for all config tests") + cfg_params = generate_cfg_templates(pfc_wd_test_port) + logger.info("Copying templates over to the DUT") + copy_templates_to_dut(duthost, cfg_params) + + yield + logger.info("--- Start running config tests ---") + + logger.info("--- Clean up config dir from DUT ---") + cfg_teardown(duthost) + + +@pytest.fixture(scope='function', autouse=True) +def stop_pfcwd(duthost): + """ + Fixture that stops PFC Watchdog before each test run + + Args: + duthost: instance of AnsibleHost class + + Returns: + None + """ + logger.info("--- Stop Pfcwd --") + duthost.command("pfcwd stop") + + +@pytest.mark.usefixtures('cfg_setup') +class TestPfcConfig(object): + """ + Test case definition and helper function class + """ + def execute_test(self, duthost, syslog_marker, ignore_regex=None, expect_regex=None, expect_errors=False): + """ + Helper function that loads each template on the DUT and verifies the expected behavior + + Args: + duthost (AnsibleHost): instance + syslog_marker (string): marker prefix name to be inserted in the syslog + ignore_regex (string): file containing regexs to be ignored by loganalyzer + expect_regex (string): regex pattern that is expected to be present in the syslog + expect_erros (bool): if the test expects an error msg in the syslog or not. Default: False + + Returns: + None + """ + loganalyzer = LogAnalyzer(ansible_host=duthost, marker_prefix=syslog_marker) + + if ignore_regex: + ignore_file = os.path.join(TEMPLATES_DIR, ignore_regex) + reg_exp = loganalyzer.parse_regexp_file(src=ignore_file) + loganalyzer.ignore_regex.extend(reg_exp) + + if expect_regex: + loganalyzer.expect_regex = [] + loganalyzer.expect_regex.extend(expect_regex) + + loganalyzer.match_regex = [] + with loganalyzer(fail=not expect_errors): + cmd = "sonic-cfggen -j {}/{}.json --write-to-db".format(DUT_RUN_DIR, syslog_marker) + out = duthost.command(cmd) + pytest_assert(out["rc"] == 0, "Failed to execute cmd {}: Error: {}".format(cmd, out["stderr"])) + + def test_forward_action_cfg(self, duthost): + """ + Tests if the config gets loaded properly for a valid cfg template + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_fwd_action", "config_test_ignore_messages") + + def test_invalid_action_cfg(self, duthost): + """ + Tests for syslog error when invalid action is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_invalid_action", None, [CONFIG_TEST_EXPECT_INVALID_ACTION_RE], True) + + def test_invalid_detect_time_cfg(self, duthost): + """ + Tests for syslog error when invalid detect time is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_invalid_detect_time", None, [CONFIG_TEST_EXPECT_INVALID_DETECT_TIME_RE], True) + + def test_low_detect_time_cfg(self, duthost): + """ + Tests for syslog error when detect time < lower bound is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_low_detect_time", None, [CONFIG_TEST_EXPECT_INVALID_DETECT_TIME_RE], True) + + def test_high_detect_time_cfg(self, duthost): + """ + Tests for syslog error when detect time > higher bound is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_high_detect_time", None, [CONFIG_TEST_EXPECT_INVALID_DETECT_TIME_RE], True) + + def test_invalid_restore_time_cfg(self, duthost): + """ + Tests for syslog error when invalid restore time is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_invalid_restore_time", None, [CONFIG_TEST_EXPECT_INVALID_RESTORE_TIME_RE], True) + + def test_low_restore_time_cfg(self, duthost): + """ + Tests for syslog error when restore time < lower bound is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_low_restore_time", None, [CONFIG_TEST_EXPECT_INVALID_RESTORE_TIME_RE], True) + + def test_high_restore_time_cfg(self, duthost): + """ + Tests for syslog error when restore time > higher bound is configured + + Args: + duthost(AnsibleHost): instance + + Returns: + None + """ + self.execute_test(duthost, "pfc_wd_high_restore_time", None, [CONFIG_TEST_EXPECT_INVALID_RESTORE_TIME_RE], True)