diff --git a/tests/common/tgen/__init__.py b/tests/common/tgen/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/common/tgen/ixia.py b/tests/common/tgen/ixia.py new file mode 100644 index 00000000000..591bc6a30a1 --- /dev/null +++ b/tests/common/tgen/ixia.py @@ -0,0 +1,81 @@ +# Ixia file to update ixia related variables + + +class IXIA(object): + """ + IXIA Class to get ixia related variables + """ + def __init__(self,testbed,duthost): + """ + Args: + testbed (pytest fixture): The testbed fixture. + duthost (pytest fixture): The duthost fixture. + + """ + + self.testbed = testbed + self.duthost = duthost + + @property + def api_serv_ip(self): + """ + In an Ixia testbed, there is no PTF docker. + Hence, we use ptf_ip field to store Ixia API server. + This fixture returns the IP address of the Ixia API server. + + Args: + testbed (pytest fixture): The testbed fixture. + + Returns: + Ixia API server IP + """ + return self.testbed['ptf_ip'] + + + @property + def api_serv_user(self): + """ + Return the username of Ixia API server. + + Returns: + Ixia API server username. + """ + return self.duthost.host.options['variable_manager']. \ + _hostvars[self.duthost.hostname]['secret_group_vars']['ixia_api_server']['user'] + + + @property + def api_serv_passwd(self): + """ + Return the password of Ixia API server. + + Returns: + Ixia API server password. + """ + return self.duthost.host.options['variable_manager']. \ + _hostvars[self.duthost.hostname]['secret_group_vars']['ixia_api_server']['password'] + + + @property + def api_serv_port(self): + """ + This fixture returns the TCP port for REST API of the ixia API server. + + Returns: + Ixia API server REST port. + """ + return self.duthost.host.options['variable_manager']. \ + _hostvars[self.duthost.hostname]['secret_group_vars']['ixia_api_server']['rest_port'] + + + @property + def api_serv_session_id(self): + """ + Ixia API server can spawn multiple session on the same REST port. + Optional for LINUX, required for windows return the session ID. + + Returns: + Ixia API server session id. + """ + return self.duthost.host.options['variable_manager']. \ + _hostvars[self.duthost.hostname]['secret_group_vars']['ixia_api_server']['session_id'] diff --git a/tests/common/tgen/tgen_fixtures.py b/tests/common/tgen/tgen_fixtures.py new file mode 100644 index 00000000000..c4e8f0b53fe --- /dev/null +++ b/tests/common/tgen/tgen_fixtures.py @@ -0,0 +1,27 @@ +## tgn fixtures file where multiple vendors can define as per Open TGEN approach +import pytest + +@pytest.fixture(scope='module') +def api(fanout_graph_facts, + tbinfo, + duthost): + """ + Common api fixture for tgen of any platform. + + Support is available for IXIA currently, please update for other traffic generators + """ + # This is a dynamic approach for getting the tgen platform + + if 'ixia-sonic' in fanout_graph_facts.keys(): + from ixnetwork_open_traffic_generator.ixnetworkapi import IxNetworkApi + from ixia import IXIA + ixia = IXIA(tbinfo,duthost) + ixnetwork_api = IxNetworkApi(address=ixia.api_serv_ip, + port=ixia.api_serv_port, + username=ixia.api_serv_user, + password=ixia.api_serv_passwd) + yield ixnetwork_api + if ixnetwork_api.assistant is not None: + ixnetwork_api.assistant.Session.remove() + else: + pytest.fail("The test supports only for IXIA TGEN, please add for other vendors") \ No newline at end of file diff --git a/tests/common/tgen/tgen_helpers.py b/tests/common/tgen/tgen_helpers.py new file mode 100644 index 00000000000..a964ca6b6b2 --- /dev/null +++ b/tests/common/tgen/tgen_helpers.py @@ -0,0 +1,573 @@ +import ipaddr +from netaddr import IPNetwork +from abstract_open_traffic_generator.port import Port +from abstract_open_traffic_generator.config import Options +from abstract_open_traffic_generator.config import Config +from abstract_open_traffic_generator.layer1 import\ + Layer1, OneHundredGbe, FlowControl, Ieee8021qbb +from abstract_open_traffic_generator.layer1 import \ + Ethernet as EthernetPort +from abstract_open_traffic_generator.port import Options as PortOptions +from abstract_open_traffic_generator.flow import Header, PfcPause +from abstract_open_traffic_generator.flow import Pattern as FieldPattern + +class FanoutManager(): + """Class for managing multiple chassis and extracting the information + like chassis IP, card, port etc. from fanout_graph_fact.""" + + def __init__(self, fanout_data): + """ When multiple chassis are available inside fanout_graph_facts + this method makes a list of chassis connection-details out of it. + So each chassis and details associated with it can be accessed by + a integer index (starting from 0) + + Args: + fanout_data (dict): the dictionary returned by fanout_graph_fact. + Example format of the fanout_data is given below + + {u'ixia-sonic': { + u'device_conn': { + u'Card9/Port1': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet0', + u'speed': u'100000' + }, + u'Card9/Port2': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet4', + u'speed': u'100000' + }, + u'Card9/Port3': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet8', + u'speed': u'100000' + }, + 'Card9/Port4': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet12', + u'speed': u'100000' + }, + u'Card9/Port5': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet16', + u'speed': u'100000' + }, + u'Card9/Port6': { + u'peerdevice': u'sonic-s6100-dut', + u'peerport': u'Ethernet20', + u'speed': u'100000' + } + }, + u'device_info': { + u'HwSku': u'IXIA-tester', + u'ManagementGw': u'10.36.78.54', + u'ManagementIp': u'10.36.78.53/32', + u'Type': u'DevIxiaChassis', + u'mgmtip': u'10.36.78.53' + }, + u'device_port_vlans': { + u'Card9/Port1': { + u'mode': u'Access', + u'vlanids': u'300', + u'vlanlist': [300] + }, + u'Card9/Port2': { + u'mode': u'Access', + u'vlanids': u'301', + u'vlanlist': [301] + }, + u'Card9/Port3': { + u'mode': u'Access', + u'vlanids': u'302', + u'vlanlist': [302] + }, + u'Card9/Port4': { + u'mode': u'Access', + u'vlanids': u'300', + u'vlanlist': [300] + }, + u'Card9/Port5': { + u'mode': u'Access', + u'vlanids': u'301', + u'vlanlist': [301] + }, + u'Card9/Port6': { + u'mode': u'Access', + u'vlanids': u'302', + u'vlanlist': [302] + } + }, + u'device_vlan_list': [301, 302, 300, 302, 300, 301], + u'device_vlan_range': [u'300-302'] + } + } + """ + self.last_fanout_assessed = None + self.fanout_list = [] + self.last_device_connection_details = None + self.current_tgen_port_list = None + self.ip_address = '0.0.0.0' + for i in fanout_data.keys(): + self.fanout_list.append(fanout_data[i]) + + def __parse_fanout_connections__(self): + device_conn = self.last_device_connection_details + retval = [] + for key in device_conn.keys(): + pp = device_conn[key]['peerport'] + string = self.ip_address + '/' + key + '/' + pp + retval.append(string) + retval.sort() + return (retval) + + def get_fanout_device_details(self, device_number): + """With the help of this function you can select the chassis you want + to access. For example get_fanout_device_details(0) selects the + first chassis. It just select the chassis but does not return + anything. The rest of the function then used to extract chassis + information like "get_chassis_ip()" will the return the ip address + of chassis 0 - the first chassis in the list. + + Note: + Counting or indexing starts from 0. That is 0 = 1st cassis, + 1 = 2nd chassis ... + + Args: + device_number (int): the chassis index (0 is the first) + + Returns: + None + """ + + # Pointer to chassis info + self.last_fanout_assessed = device_number + + # Chassis connection details + self.last_device_connection_details = \ + self.fanout_list[self.last_fanout_assessed]['device_conn'] + + # Chassis ip details + self.ip_address = \ + self.fanout_list[self.last_fanout_assessed]['device_info']['mgmtip'] + + # List of chassis cards and ports + self.current_tgen_port_list = \ + self.__parse_fanout_connections__() + + # return self.fanout_list[self.last_fanout_assessed] + + def get_ports(self) : + """This function returns list of ports associated with a chassis + (selected earlier using get_fanout_device_details() function) + as a list of dictionary. + + Note: If you have not used get_fanout_device_details(), by default 0th + (first) chassis remains selected. + + Args: + This function takes no argument. + + Returns: + Dictionary of chassis card port information. + """ + retval = [] + for port in self.current_tgen_port_list: + info_list = port.split('/') + dict_element = { + 'ip': info_list[0], + 'card_id': info_list[1].replace('Card', ''), + 'port_id': info_list[2].replace('Port', ''), + 'peer_port': info_list[3], + } + retval.append(dict_element) + + return retval + + +class TgenPorts(object): + """ + TgenPorts instance to configure the Tgen related properties + """ + def __init__(self, + conn_graph_facts, + fanout_graph_facts): + + self.conn_graph_facts = conn_graph_facts + self.fanout_graph_facts = fanout_graph_facts + + def get_available_phy_ports(self): + """ + Adds interface speed and returns available physical ports + + Return: + [{'card_id': u'9', + 'ip': u'10.36.78.53', + 'peer_port': u'Ethernet0', + 'port_id': u'1', + 'speed': 100000}, + {'card_id': u'9', + 'ip': u'10.36.78.53', + 'peer_port': u'Ethernet4', + 'port_id': u'2', + 'speed': 100000}, + {'card_id': u'9', + 'ip': u'10.36.78.53', + 'peer_port': u'Ethernet8', + 'port_id': u'3', + 'speed': 100000}] + """ + # fanout_devices = FanoutManager(self.fanout_graph_facts) + # import pdb; pdb.set_trace() + fanout_devices = FanoutManager(self.fanout_graph_facts) + fanout_devices.get_fanout_device_details(device_number=0) + device_conn = self.conn_graph_facts['device_conn'] + available_phy_port = fanout_devices.get_ports() + + for intf in available_phy_port: + peer_port = intf['peer_port'] + intf['speed'] = int(device_conn[peer_port]['speed']) + return available_phy_port + + def verify_required_ports(self, no_of_ports_required): + """ + Verifies the number of physical ports required and fails if not satisfied + + :param no_of_ports_required: No of Ports mandatory for the test. + If Topo doesn't statisfy throws error. + """ + available_phy_ports = self.get_available_phy_ports() + if no_of_ports_required is None: + return + if len(available_phy_ports) < no_of_ports_required: + pytest_assert(False, + "Number of physical ports must be at least {}".format(no_of_ports_required)) + + def create_ports_list(self,no_of_ports, start_index): + """ + A Function creates ports_list as the testcase needs to be repeated for + all available ports in the testbed even if the test needs n number of ports + + :param no_of_ports: Number of minimum required ports for the test + :start_index: index of the available port list + + Ex: If a test needs 2 ports and testbed has 3 ports. A 3 ports testbed will + get 2 combinations of ports [1,2], [2,3] and [3, 1] then start index determines + which combinition to return. + Return: + [{'card_id': '9', + 'ip': '10.36.78.53', + 'peer_port': 'Ethernet0', + 'port_id': '1', + 'speed': 100000}, + {'card_id': '9', + 'ip': '10.36.78.53', + 'peer_port': 'Ethernet4', + 'port_id': '2', + 'speed': 100000}], + """ + avail_ports = self.get_available_phy_ports() + if start_index > len(avail_ports)-1: + pytest.fail("Port list index is out of range") + slice_range = start_index + no_of_ports + num = slice_range - len(avail_ports) + return avail_ports[start_index: None if slice_range > len(avail_ports)-1 else slice_range] \ + + avail_ports[0:num if num>=0 else 0] + + + def l1_config(self, phy_ports): + """ + Creates config for traffic generator with given physical ports + + :param phy_ports: Physical ports config creation on traffic generator + """ + + # [one_hundred_gbps, fifty_gbps, forty_gbps, twenty_five_gpbs, ten_gbps] + # [one_thousand_mbps, one_hundred_fd_mbps, one_hundred_hd_mbps, ten_fd_mbps, ten_hd_mbps] + port_speeds = { + 100 : ['OneHundredGbe','one_hundred_gbps'], + 50 : ['OneHundredGbe','fifty_gbps'], + 40 : ['OneHundredGbe','forty_gbps'], + 25 : ['OneHundredGbe','twenty_five_gpbs'], + 10 : ['OneHundredGbe','ten_gbps'], + 1 : ['Ethernet','one_thousand_mbps'], + } + + ports = [] + port_names = {port_speeds[speed][0]+'.'+port_speeds[speed][1] : [] for speed in port_speeds} + + # Finding the port speed from the DUT conn_facts + for index,phy_port in enumerate(phy_ports,1): + port_location = get_location(phy_port) + port = Port(name='Port'+str(index),location=port_location) + speed = phy_port['speed']/1000 + if port_speeds.get(speed) is None: + pytest.fail("Currently port speed of {} gbe is not supported".format(speed)) + else: + key = port_speeds.get(speed)[0]+'.'+port_speeds.get(speed)[1] + port_names[key].append(port.name) + ports.append(port) + ####################################################################### + # currently setting the flow control for all the oneHunderdGbe objects. + # Need to add the code to get the options from dut to set the auto neg + # and ieee standards + ####################################################################### + pfc = Ieee8021qbb(pfc_delay=1, + pfc_class_0=0, + pfc_class_1=1, + pfc_class_2=2, + pfc_class_3=3, + pfc_class_4=4, + pfc_class_5=5, + pfc_class_6=6, + pfc_class_7=7) + + flow_ctl = FlowControl(choice=pfc) + + l1_obj_list = [] + for port_combi in port_names: + if len(port_names[port_combi]) == 0: + continue + obj_class, speed = port_combi.split(".",1) + if obj_class == "OneHundredGbe": + l1_obj_list.append( + Layer1(name='{} settings'.format(speed), + port_names=port_names[port_combi], + choice=OneHundredGbe(link_training=True, + ieee_media_defaults=False, + auto_negotiate=False, + rs_fec=True, + flow_control=flow_ctl, + speed=speed)) + ) + elif obj_class == "Ethernet": + l1_obj_list.append( + Layer1(name='{} settings'.format(speed), + port_names=port_names[port_combi], + choice=EthernetPort(auto_negotiate=False, + flow_control=flow_ctl, + speed=speed)) + ) + + config = Config(ports=ports, + layer1=l1_obj_list, + options=Options(PortOptions(location_preemption=True))) + + return config + + +def ansible_stdout_to_str(ansible_stdout): + """ + The stdout of Ansible host is essentially a list of unicode characters. + This function converts it to a string. + + Args: + ansible_stdout: stdout of Ansible + + Returns: + Return a string + """ + result = "" + for x in ansible_stdout: + result += x.encode('UTF8') + return result + + +def get_vlan_subnet(host_ans): + """ + Get VLAN subnet of a T0 device + + Args: + host_ans: Ansible host instance of the device + + Returns: + VLAN subnet, e.g., "192.168.1.1/24" where 192.168.1.1 is gateway + and 24 is prefix length + """ + mg_facts = host_ans.minigraph_facts(host=host_ans.hostname)['ansible_facts'] + mg_vlans = mg_facts['minigraph_vlans'] + + if len(mg_vlans) != 1: + print 'There should be only one Vlan at the DUT' + return None + + mg_vlan_intfs = mg_facts['minigraph_vlan_interfaces'] + prefix_len = mg_vlan_intfs[0]['prefixlen'] + gw_addr = ansible_stdout_to_str(mg_vlan_intfs[0]['addr']) + return gw_addr + '/' + str(prefix_len) + + +def get_addrs_in_subnet(subnet, number_of_ip): + """ + Get N IP addresses in a subnet. + + Args: + subnet (str): IPv4 subnet, e.g., '192.168.1.1/24' + number_of_ip (int): Number of IP addresses to get + + Return: + Return n IPv4 addresses in this subnet in a list. + """ + ip_addr = subnet.split('/')[0] + ip_addrs = [str(x) for x in list(IPNetwork(subnet))] + ip_addrs.remove(ip_addr) + + """ Try to avoid network and broadcast addresses """ + if len(ip_addrs) >= number_of_ip + 2: + del ip_addrs[0] + del ip_addrs[-1] + + return ip_addrs[:number_of_ip] + + +def get_location(intf): + """ Extracting location from interface, since TgenApi accepts location + in terms of chassis ip, card, and port in different format. + + Note: Interface must have the keys 'ip', 'card_id' and 'port_id' + + Args: + intf (dict) : intf must containg the keys 'ip', 'card_id', 'port_id'. + Example format : + {'ip': u'10.36.78.53', + 'port_id': u'1', + 'card_id': u'9', + 'speed': 100000, + 'peer_port': u'Ethernet0'} + + Returns: location in string format. Example: '10.36.78.5;1;2' where + 1 is card_id and 2 is port_id. + """ + location = None + try: + location = str("%s;%s;%s" % (intf['ip'], intf['card_id'], intf['port_id'])) + except Exception: + pytest_assert(False, + "Interface must have the keys 'ip', 'card_id' and 'port_id'") + return location + + +def increment_ip_address (ip, incr=1) : + """ + Increment IP address by an integer number. + + Args: + ip (str): IP address in string format. + incr (int): Increment by the specified number. + + Return: + IP address in the argument incremented by the given integer. + """ + ipaddress = ipaddr.IPv4Address(ip) + ipaddress = ipaddress + incr + return_value = ipaddress._string_from_ip_int(ipaddress._ip) + return(return_value) + + +def create_pause_packet(pause_prio_list): + """ + Creates and return the pause packet object that can be + configured in the Tgen traffic item, based on the priority + list passed via param :pause_prio_list + """ + + val = 0 + for prio in pause_prio_list: + val += 2 ** prio + choice = ['ffff' if prio in pause_prio_list else '0' for prio in range(8)] + pause = Header(PfcPause( + dst=FieldPattern(choice='01:80:C2:00:00:01'), + src=FieldPattern(choice='00:00:fa:ce:fa:ce'), + class_enable_vector=FieldPattern(choice=hex(val)), + pause_class_0=FieldPattern(choice=choice[0]), + pause_class_1=FieldPattern(choice=choice[1]), + pause_class_2=FieldPattern(choice=choice[2]), + pause_class_3=FieldPattern(choice=choice[3]), + pause_class_4=FieldPattern(choice=choice[4]), + pause_class_5=FieldPattern(choice=choice[5]), + pause_class_6=FieldPattern(choice=choice[6]), + pause_class_7=FieldPattern(choice=choice[7]), + )) + return pause + + +################################################################## +# Currently supporting only ixia and will +# add the provision to other tgen once this approach is approved +################################################################## + +import xml.etree.ElementTree as ET + +def _get_xml_root(file_name): + """ return xml root object """ + return ET.parse(file_name) + +def _find_device_links_from_root(root, device_tuple_list): + """ return list of device links from connection_graph.xml """ + devices = root.findall('./PhysicalNetworkGraphDeclaration/Devices')[0] + if len(devices) == 0: + return None + + dev_attr_values = [(each_device.attrib['Hostname'], each_device.attrib['Type']) \ + for each_device in devices] + + result = True + for dev_tuple in device_tuple_list: + if dev_tuple not in dev_attr_values: + result = None + break + if result: + return (dev_attr_values, root.findall('./PhysicalNetworkGraphDeclaration/DeviceInterfaceLinks')[0]) + return None + + +def _get_links(device_interfaces): + """ return list of interfaces for between ixia """ + dev_tuple = device_interfaces[0] + intfs = device_interfaces[1] + end_devices = [dev[0] for dev in dev_tuple if dev[1] == "DevIxiaChassis"] + links = [link for link in intfs \ + if link.attrib['EndDevice'] in end_devices] + return links + + +def _find_xml_file(): + """ find connection_graph.xmls from ../ansible/files path """ + import os + file_path = "../ansible/files" + path = os.walk(file_path) + xml_list = [os.path.join(file_path,xml) for xml in path.next()[2] if "connection_graph.xml" in xml] + try: + xml_list.pop(xml_list.index(os.path.join(file_path,'example_ixia_connection_graph.xml'))) + except Exception: + pass + return xml_list + + +def _get_dev_from_testbed(tb_file, tb_name): + """ return the duts list from the Testbed file.csv""" + # Currently supporting csv file formatted testbed file + # shall add support for yaml formatted testbed file in future + from tests.conftest import TestbedInfo + tb = TestbedInfo(tb_file) + duts = tb.testbed_topo[tb_name]['duts'] + duts = [(dut, 'DevSonic') for dut in duts] + return duts + + +def get_tgen_links(request): + """ will return the tgen links connected to duts """ + tb_file = request.config.getoption('testbed_file') + tb_name = request.config.getoption('testbed') + devs = _get_dev_from_testbed(tb_file, tb_name) + all_links = None + for xml in _find_xml_file(): + root = _get_xml_root(xml) + all_links = _find_device_links_from_root(root, devs) + if all_links: + break + if all_links is None: + pytest.fail("Could not fetch the links from xml") + tgen_links = _get_links(all_links) + return list(range(len(tgen_links))) + + diff --git a/tests/pfcwd/conftest.py b/tests/pfcwd/conftest.py index 2d855620d99..1b9c61d3336 100644 --- a/tests/pfcwd/conftest.py +++ b/tests/pfcwd/conftest.py @@ -90,3 +90,18 @@ def setup_pfc_test(duthost, ptfhost, conn_graph_facts): logger.info("--- Starting Pfcwd ---") duthost.command("pfcwd start_default") + + + +from tests.common.tgen.tgen_helpers import get_tgen_links + + +def generate_params_port_id(request): + """ returns the port id list """ + return get_tgen_links(request) + + +def pytest_generate_tests(metafunc): + if "port_id" in metafunc.fixturenames: + port_ids = generate_params_port_id(metafunc) + metafunc.parametrize("port_id", port_ids) diff --git a/tests/pfcwd/files/pfcwd.py b/tests/pfcwd/files/pfcwd.py new file mode 100755 index 00000000000..342c1b3b929 --- /dev/null +++ b/tests/pfcwd/files/pfcwd.py @@ -0,0 +1,251 @@ +from tests.common.tgen.tgen_helpers import get_vlan_subnet, \ + get_addrs_in_subnet, TgenPorts +from abstract_open_traffic_generator.device import * +from abstract_open_traffic_generator.flow import \ + Flow, TxRx, DeviceTxRx, Header, Size, Rate, Duration, \ + Continuous +from abstract_open_traffic_generator.flow_ipv4 import\ + Priority, Dscp +from abstract_open_traffic_generator.flow import Pattern as FieldPattern +from abstract_open_traffic_generator.flow import Ipv4 as Ipv4Header +from abstract_open_traffic_generator.flow import Ethernet as EthernetHeader +from abstract_open_traffic_generator.result import FlowRequest +from abstract_open_traffic_generator.control import * +from tests.common.helpers.assertions import pytest_assert +from tests.common.fixtures.conn_graph_facts import conn_graph_facts,\ + fanout_graph_facts +from .utils import lossless_prio_dscp_map +import logging +import pytest + +logger = logging.getLogger(__name__) + +""" Delay to start test Traffic """ +START_DELAY = 1 + +""" Rate percentage of test traffic """ +TRAFFIC_LINE_RATE = 50 + +""" Data packet size in bytes """ +FRAME_SIZE = 1024 + +""" Traffic through put tolerance """ +TOLERANCE_PERCENT = 1 + +""" + Hardcoding storm detection and restoration for + now, will fetch from the DUT once the code structure + is approved. +""" +STORM_DETECTION_TIME = 400 +STORM_RESTORATION_TIME = 2000 + +def run_pfcwd_impact_test(api, duthost, port_id, conn_graph_facts, fanout_graph_facts): + + """ runs the pfcwd impact test """ + + __create_tgen_config_pfcwd_impact__(api=api, + duthost=duthost, + port_id=port_id, + conn_graph_facts=conn_graph_facts, + fanout_graph_facts=fanout_graph_facts, + start_delay=START_DELAY, + traffic_line_rate=TRAFFIC_LINE_RATE, + frame_size=FRAME_SIZE) + + __run_pfcwd_impact_test__(api=api, duthost=duthost) + + + +def __create_tgen_config_pfcwd_impact__(api, + duthost, + port_id, + conn_graph_facts, + fanout_graph_facts, + start_delay, + traffic_line_rate, + frame_size): + """ Creates the Tgen config """ + ###################################################################### + # TgenPorts object is used to retrive the port bandwidth information + # dynamically from the DUT conn_graph_facts and fanout_graph_facts + # this will help config the tgen port + ###################################################################### + + tgen_ports = TgenPorts(conn_graph_facts, fanout_graph_facts) + # returns the list of current and neighboring ports information + port_list = tgen_ports.create_ports_list(2, port_id) + ###################################################################### + # Fetching lossless priority from duthost + # testcase will configure all the lossless priorities in one traffic + # item + ###################################################################### + lossless_priority = lossless_prio_dscp_map(duthost, True) + + ###################################################################### + # Tgen Device Configuration + ###################################################################### + + vlan_subnet = get_vlan_subnet(duthost) + vlan_ip_addrs = get_addrs_in_subnet(vlan_subnet, len(port_list)) + gw_ip = vlan_subnet.split('/')[0] + network_prefix = vlan_subnet.split('/')[1] + + tgen_config = tgen_ports.l1_config(port_list) + + tx_port = tgen_config.ports[0] + rx_port = tgen_config.ports[1] + + tx_device = Device(name='Tx Device', + container_name=tx_port.name, + device_count=1, + choice=Ipv4(name='Tx Ipv4', + address=Pattern(vlan_ip_addrs[0]), + prefix=Pattern(network_prefix), + gateway=Pattern(gw_ip), + ethernet=Ethernet(name='Tx Ethernet') + ) + ) + tgen_config.devices.append(tx_device) + + rx_device = Device(name='Rx Device', + container_name=rx_port.name, + device_count=1, + choice=Ipv4(name='Rx Ipv4', + address=Pattern(vlan_ip_addrs[1]), + prefix=Pattern(network_prefix), + gateway=Pattern(gw_ip), + ethernet=Ethernet(name='Rx Ethernet') + ) + ) + tgen_config.devices.append(rx_device) + + ###################################################################### + # Fetching priorities for lossless and lossy traffic + ###################################################################### + interface_priorities = lossless_priority[1] + priority_lossless_list = lossless_priority[0] + + lossless_list = interface_priorities.get(port_list[0].get('peer_port')) + + if lossless_list is None: + pytest_assert(False, "DSCP priorities are not configured on the port {}".format(port_list[0].get('peer_port'))) + + final_lossless_list = [] + for dscp in lossless_list: + if priority_lossless_list.get(dscp) is None: + continue + list_values = [str(x) for x in priority_lossless_list.get(dscp)] + final_lossless_list = final_lossless_list + list_values + + dscp_prio_lossy = [str(x) for x in range(64) if str(x) not in final_lossless_list] + + ###################################################################### + # Traffic configuration Traffic 1->2 lossless + ###################################################################### + + dscp_prio = Priority(Dscp(phb=FieldPattern(choice=final_lossless_list))) + flow_config = Flow(name="Traffic 1->2 lossless", + tx_rx=TxRx(DeviceTxRx(tx_device_names=[tx_device.name],rx_device_names=[rx_device.name])), + packet=[ + Header(choice=EthernetHeader()), + Header(choice=Ipv4Header(priority=dscp_prio)), + ], + size=Size(FRAME_SIZE), + rate=Rate('line', TRAFFIC_LINE_RATE), + duration=Duration(Continuous(delay=START_DELAY, delay_unit='nanoseconds')) + ) + tgen_config.flows.append(flow_config) + + ###################################################################### + # Traffic configuration Traffic 1->2 lossy + ###################################################################### + + dscp_prio = Priority(Dscp(phb=FieldPattern(choice=dscp_prio_lossy))) + flow_config = Flow(name="Traffic 1->2 lossy", + tx_rx=TxRx(DeviceTxRx(tx_device_names=[tx_device.name],rx_device_names=[rx_device.name])), + packet=[ + Header(choice=EthernetHeader()), + Header(choice=Ipv4Header(priority=dscp_prio)), + ], + size=Size(FRAME_SIZE), + rate=Rate('line', TRAFFIC_LINE_RATE), + duration=Duration(Continuous(delay=START_DELAY, delay_unit='nanoseconds')) + ) + tgen_config.flows.append(flow_config) + ####################################################################### + # Applying TGEN Config Created above and Test on lossless and lossy + ####################################################################### + api.set_state(State(ConfigState(config=tgen_config, state='set'))) + + +def __run_pfcwd_impact_test__(api, duthost): + """ runs the impact test """ + ####################################################################### + # Saving the DUT Configuration in variables and disabling pfcwd + ####################################################################### + + dut_cmd_disable = 'sudo pfcwd stop' + dut_cmd_enable = 'sudo pfcwd start --action drop ports all detection-time {} \ + --restoration-time {}'.format(STORM_DETECTION_TIME, STORM_RESTORATION_TIME) + duthost.shell(dut_cmd_disable) + duthost.shell('pfcwd show config') + + ############################################################################################## + # Start all flows + # 1. check for no loss in the flows Traffic 1->2 lossless,Traffic 1->2 lossy + # 2. configure pfcwd on dut and wait for 5 seconds + # 3. check for no loss in the flows configured + ############################################################################################## + logger.info("Starting the traffic") + api.set_state(State(FlowTransmitState(state='start'))) + ############################################################################################## + # Checking for the traffic state if it is started. + ############################################################################################## + __wait_for_traffic_start__(api) + + logger.info("STEP1: Verify the traffic, No loss Expected on all Priorities whild PFCWD is disabled") + __verify_traffic_for_impact_test__(api, 0.0, 'disabled') + logger.info("Sleeping for 5 seconds") + time.sleep(5) + logger.info("STEP2: Enabling PFCWD") + duthost.shell(dut_cmd_enable) + logger.info("STEP3: Verify the traffic, No loss Expected on all Priorities whild PFCWD is enabled") + __verify_traffic_for_impact_test__(api, 0.0, 'enabled') + logger.info("STEP4: Verify the traffic, No loss Expected on all Priorities whild PFCWD is disabled") + duthost.shell(dut_cmd_disable) + __verify_traffic_for_impact_test__(api, 0.0, 'disabled') + + logger.info("Stopping the traffic") + api.set_state(State(FlowTransmitState(state='stop'))) + + +def __wait_for_traffic_start__(api): + """ Wait for the traffic to Start """ + for i in range(100): + test_stat = api.get_flow_results(FlowRequest()) + if "started" in test_stat[0]['transmit']: + break + time.sleep(3) + + +def __verify_traffic_for_impact_test__(api, expected_loss, pfc_state): + """ Verify the traffic loss and through put """ + test_stat = api.get_flow_results(FlowRequest()) + for flow in test_stat: + if flow['loss'] > expected_loss: + pytest.fail("Observing loss in the flow {} while pfcwd state {}" + .format(flow['name'],pfc_state)) + tx_frame_rate = int(flow['frames_tx_rate']) + rx_frame_rate = int(flow['frames_rx_rate']) + tolerance = (tx_frame_rate * TOLERANCE_PERCENT)/100 + logger.info("\nTx Frame Rate,Rx Frame Rate of {} is {},{}" + .format(flow['name'],tx_frame_rate,rx_frame_rate)) + if tx_frame_rate > (rx_frame_rate + tolerance): + pytest.fail("Observing traffic rate change in the flow {} while pfcwd state {}" + .format(flow['name'],pfc_state)) + + + + + diff --git a/tests/pfcwd/files/utils.py b/tests/pfcwd/files/utils.py new file mode 100755 index 00000000000..7e1e9105cbe --- /dev/null +++ b/tests/pfcwd/files/utils.py @@ -0,0 +1,49 @@ + +def lossless_prio_dscp_map(duthost, dut_intf_map=None): + """ + This fixture reads the QOS parameters from SONiC DUT, and creates + lossless priority Vs. DSCP priority port map (dictionary key = lossless + priority). + Args: + duthost (pytest fixture) : duthost + + Returns: + Lossless priority vs. DSCP map (dictionary, key = lossless priority). + Example: {3: [3], 4: [4]} + """ + config_facts = duthost.config_facts(host=duthost.hostname, + source="persistent")['ansible_facts'] + + if "PORT_QOS_MAP" not in config_facts.keys(): + return None + + # Read the port QOS map. If pfc_enable flag is false then return None. + port_qos_map = config_facts["PORT_QOS_MAP"] + intf = port_qos_map.keys()[0] + if 'pfc_enable' not in port_qos_map[intf]: + return None + + lossless_priorities = \ + list(set([int(x) for intf in port_qos_map for x in port_qos_map[intf]['pfc_enable'].split(',')])) + dscp_to_tc_map = config_facts["DSCP_TO_TC_MAP"] + + result = dict() + for prio in lossless_priorities: + result[prio] = list() + + profile = dscp_to_tc_map.keys()[0] + + for dscp in dscp_to_tc_map[profile]: + tc = dscp_to_tc_map[profile][dscp] + + if int(tc) in lossless_priorities: + result[int(tc)].append(int(dscp)) + + lossless_priorities_interface = \ + {intf : [int(x) for x in port_qos_map[intf]['pfc_enable'].split(',')] for intf in port_qos_map} + + if dut_intf_map: + return (result, lossless_priorities_interface) + + return result + diff --git a/tests/pfcwd/test_pfcwd_impact.py b/tests/pfcwd/test_pfcwd_impact.py new file mode 100755 index 00000000000..ab21c9a5339 --- /dev/null +++ b/tests/pfcwd/test_pfcwd_impact.py @@ -0,0 +1,38 @@ +import pytest +import logging +from .files.pfcwd import run_pfcwd_impact_test +from tests.common.tgen.tgen_fixtures import api +from tests.common.fixtures.conn_graph_facts import conn_graph_facts,\ + fanout_graph_facts + + +def test_pfcwd_impact(api, duthost, port_id, conn_graph_facts, fanout_graph_facts): + """ + +-----------------+ +--------------+ +-----------------+ + | Keysight Port 1 |------ et1 | SONiC DUT | et2 ------| Keysight Port 2 | + +-----------------+ +--------------+ +-----------------+ + + Configuration: + 1. Configure lossless priorities on the DUT interface. + 2. Disable PFC Watch dog. + 3. On Keysight Chassis, create one unidirectional traffic with lossless priorities and + one unidirectional traffic with lossy priorities with 50% line rate each. + + # Workflow + 1. Start both lossless and lossy traffic on Keysight ports. + 2. Verify the traffic when pfc disabled state. + 3. Wait for 5 seconds and Enable the PFC watch dog on DUT. + 4. Verify the traffic when pfc enabled state. + 5. Disable PFC on DUT. + 6. verify the traffic when pfc disabled state again. + + Traffic Verfication: + In all above traffic verification, No traffic loss and No change in line rate shall be observed. + """ + + run_pfcwd_impact_test(api=api, + duthost=duthost, + port_id=port_id, + conn_graph_facts=conn_graph_facts, + fanout_graph_facts=fanout_graph_facts) +