Add gtpu sw performance test cases
[csit.git] / resources / libraries / python / InterfaceUtil.py
index 04fdff7..94c78a1 100644 (file)
@@ -20,6 +20,7 @@ from ipaddress import ip_address
 from robot.api import logger
 
 from resources.libraries.python.Constants import Constants
+from resources.libraries.python.CpuUtils import CpuUtils
 from resources.libraries.python.DUTSetup import DUTSetup
 from resources.libraries.python.IPAddress import IPAddress
 from resources.libraries.python.L2Util import L2Util
@@ -110,6 +111,13 @@ class RdmaMode(IntEnum):
     RDMA_API_MODE_DV = 2
 
 
+class AfXdpMode(IntEnum):
+    """AF_XDP interface mode."""
+    AF_XDP_API_MODE_AUTO = 0
+    AF_XDP_API_MODE_COPY = 1
+    AF_XDP_API_MODE_ZERO_COPY = 2
+
+
 class InterfaceUtil:
     """General utilities for managing interfaces"""
 
@@ -227,6 +235,26 @@ class InterfaceUtil:
                 f"Node {node[u'host']} has unknown NodeType: {node[u'type']}"
             )
 
+    @staticmethod
+    def set_interface_state_pci(
+            node, pf_pcis, namespace=None, state=u"up"):
+        """Set operational state for interface specified by PCI address.
+
+        :param node: Topology node.
+        :param pf_pcis: List of node's interfaces PCI addresses.
+        :param namespace: Exec command in namespace. (Optional, Default: none)
+        :param state: Up/Down. (Optional, default: up)
+        :type nodes: dict
+        :type pf_pcis: list
+        :type namespace: str
+        :type state: str
+        """
+        for pf_pci in pf_pcis:
+            pf_eth = InterfaceUtil.pci_to_eth(node, pf_pci)
+            InterfaceUtil.set_linux_interface_state(
+                node, pf_eth, namespace=namespace, state=state
+            )
+
     @staticmethod
     def set_interface_mtu(node, pf_pcis, mtu=9200):
         """Set Ethernet MTU for specified interfaces.
@@ -245,25 +273,43 @@ class InterfaceUtil:
             exec_cmd_no_error(node, cmd, sudo=True)
 
     @staticmethod
-    def set_interface_flow_control(node, pf_pcis, rx=u"off", tx=u"off"):
+    def set_interface_channels(
+            node, pf_pcis, num_queues=1, channel=u"combined"):
+        """Set interface channels for specified interfaces.
+
+        :param node: Topology node.
+        :param pf_pcis: List of node's interfaces PCI addresses.
+        :param num_queues: Number of channels. (Optional, Default: 1)
+        :param channel: Channel type. (Optional, Default: combined)
+        :type nodes: dict
+        :type pf_pcis: list
+        :type num_queues: int
+        :type channel: str
+        """
+        for pf_pci in pf_pcis:
+            pf_eth = InterfaceUtil.pci_to_eth(node, pf_pci)
+            cmd = f"ethtool --set-channels {pf_eth} {channel} {num_queues}"
+            exec_cmd_no_error(node, cmd, sudo=True)
+
+    @staticmethod
+    def set_interface_flow_control(node, pf_pcis, rxf=u"off", txf=u"off"):
         """Set Ethernet flow control for specified interfaces.
 
         :param node: Topology node.
         :param pf_pcis: List of node's interfaces PCI addresses.
-        :param rx: RX flow. Default: off.
-        :param tx: TX flow. Default: off.
+        :param rxf: RX flow. (Optional, Default: off).
+        :param txf: TX flow. (Optional, Default: off).
         :type nodes: dict
         :type pf_pcis: list
-        :type rx: str
-        :type tx: str
+        :type rxf: str
+        :type txf: str
         """
         for pf_pci in pf_pcis:
             pf_eth = InterfaceUtil.pci_to_eth(node, pf_pci)
-            cmd = f"ethtool -A {pf_eth} rx off tx off"
+            cmd = f"ethtool -A {pf_eth} rx {rxf} tx {txf}"
             ret_code, _, _ = exec_cmd(node, cmd, sudo=True)
             if int(ret_code) not in (0, 78):
-                raise RuntimeError("Failed to set MTU on {pf_eth}!")
-
+                raise RuntimeError("Failed to set flow control on {pf_eth}!")
 
     @staticmethod
     def set_pci_parameter(node, pf_pcis, key, value):
@@ -308,8 +354,7 @@ class InterfaceUtil:
             with PapiSocketExecutor(node) as papi_exec:
                 papi_exec.add(cmd, **args).get_reply(err_msg)
         except AssertionError as err:
-            # TODO: Make failure tolerance optional.
-            logger.debug(f"Setting MTU failed. Expected?\n{err}")
+            logger.debug(f"Setting MTU failed.\n{err}")
 
     @staticmethod
     def vpp_set_interfaces_mtu_on_node(node, mtu=9200):
@@ -1016,6 +1061,49 @@ class InterfaceUtil:
 
         return ifc_name, sw_if_index
 
+    @staticmethod
+    def create_gtpu_tunnel_interface(node, teid, source_ip, destination_ip):
+        """Create GTPU interface and return sw if index of created interface.
+
+        :param node: Node where to create GTPU interface.
+        :param teid: GTPU Tunnel Endpoint Identifier.
+        :param source_ip: Source IP of a GTPU Tunnel End Point.
+        :param destination_ip: Destination IP of a GTPU Tunnel End Point.
+        :type node: dict
+        :type teid: int
+        :type source_ip: str
+        :type destination_ip: str
+        :returns: SW IF INDEX of created interface.
+        :rtype: int
+        :raises RuntimeError: if it is unable to create GTPU interface on the
+            node.
+        """
+        cmd = u"gtpu_add_del_tunnel"
+        args = dict(
+            is_add=True,
+            src_address=IPAddress.create_ip_address_object(
+                ip_address(source_ip)
+            ),
+            dst_address=IPAddress.create_ip_address_object(
+                ip_address(destination_ip)
+            ),
+            mcast_sw_if_index=Constants.BITWISE_NON_ZERO,
+            encap_vrf_id=0,
+            decap_next_index=2,
+            teid=teid
+        )
+        err_msg = f"Failed to create GTPU tunnel interface " \
+            f"on host {node[u'host']}"
+        with PapiSocketExecutor(node) as papi_exec:
+            sw_if_index = papi_exec.add(cmd, **args).get_sw_if_index(err_msg)
+
+        if_key = Topology.add_new_port(node, u"gtpu_tunnel")
+        Topology.update_interface_sw_if_index(node, if_key, sw_if_index)
+        ifc_name = InterfaceUtil.vpp_get_interface_name(node, sw_if_index)
+        Topology.update_interface_name(node, if_key, ifc_name)
+
+        return sw_if_index
+
     @staticmethod
     def vpp_create_loopback(node, mac=None):
         """Create loopback interface on VPP node.
@@ -1181,6 +1269,58 @@ class InterfaceUtil:
 
         return Topology.get_interface_by_sw_index(node, sw_if_index)
 
+    @staticmethod
+    def vpp_create_af_xdp_interface(
+            node, if_key, num_rx_queues=None, rxq_size=0, txq_size=0,
+            mode=u"auto"):
+        """Create AF_XDP interface on VPP node.
+
+        :param node: DUT node from topology.
+        :param if_key: Physical interface key from topology file of interface
+            to be bound to compatible driver.
+        :param num_rx_queues: Number of RX queues. (Optional, Default: none)
+        :param rxq_size: Size of RXQ (0 = Default API; 512 = Default VPP).
+        :param txq_size: Size of TXQ (0 = Default API; 512 = Default VPP).
+        :param mode: AF_XDP interface mode. (Optional, Default: auto).
+        :type node: dict
+        :type if_key: str
+        :type num_rx_queues: int
+        :type rxq_size: int
+        :type txq_size: int
+        :type mode: str
+        :returns: Interface key (name) in topology file.
+        :rtype: str
+        :raises RuntimeError: If it is not possible to create AF_XDP interface
+            on the node.
+        """
+        PapiSocketExecutor.run_cli_cmd(
+            node, u"set logging class af_xdp level debug"
+        )
+
+        cmd = u"af_xdp_create"
+        pci_addr = Topology.get_interface_pci_addr(node, if_key)
+        args = dict(
+            name=InterfaceUtil.pci_to_eth(node, pci_addr),
+            host_if=InterfaceUtil.pci_to_eth(node, pci_addr),
+            rxq_num=int(num_rx_queues) if num_rx_queues else 0,
+            rxq_size=rxq_size,
+            txq_size=txq_size,
+            mode=getattr(AfXdpMode, f"AF_XDP_API_MODE_{mode.upper()}").value
+        )
+        err_msg = f"Failed to create AF_XDP interface on host {node[u'host']}"
+        with PapiSocketExecutor(node) as papi_exec:
+            sw_if_index = papi_exec.add(cmd, **args).get_sw_if_index(err_msg)
+
+        InterfaceUtil.vpp_set_interface_mac(
+            node, sw_if_index, Topology.get_interface_mac(node, if_key)
+        )
+        InterfaceUtil.add_eth_interface(
+            node, sw_if_index=sw_if_index, ifc_pfx=u"eth_af_xdp",
+            host_if_key=if_key
+        )
+
+        return Topology.get_interface_by_sw_index(node, sw_if_index)
+
     @staticmethod
     def vpp_create_rdma_interface(
             node, if_key, num_rx_queues=None, rxq_size=0, txq_size=0,
@@ -1218,7 +1358,7 @@ class InterfaceUtil:
             rxq_size=rxq_size,
             txq_size=txq_size,
             mode=getattr(RdmaMode, f"RDMA_API_MODE_{mode.upper()}").value,
-            # TODO: Set True for non-jumbo packets.
+            # Note: Set True for non-jumbo packets.
             no_multi_seg=False,
             max_pktlen=0,
         )
@@ -1515,6 +1655,29 @@ class InterfaceUtil:
         cmd = f"{ns_str} ip link set {interface} {mac_str}"
         exec_cmd_no_error(node, cmd, sudo=True)
 
+    @staticmethod
+    def set_linux_interface_promisc(
+            node, interface, namespace=None, vf_id=None, state=u"on"):
+        """Set promisc state for interface in linux.
+
+        :param node: Node where to execute command.
+        :param interface: Interface in namespace.
+        :param namespace: Exec command in namespace. (Optional, Default: None)
+        :param vf_id: Virtual Function id. (Optional, Default: None)
+        :param state: State of feature. (Optional, Default: on)
+        :type node: dict
+        :type interface: str
+        :type namespace: str
+        :type vf_id: int
+        :type state: str
+        """
+        promisc_str = f"vf {vf_id} promisc {state}" if vf_id is not None \
+            else f"promisc {state}"
+        ns_str = f"ip netns exec {namespace}" if namespace else u""
+
+        cmd = f"{ns_str} ip link set dev {interface} {promisc_str}"
+        exec_cmd_no_error(node, cmd, sudo=True)
+
     @staticmethod
     def set_linux_interface_trust_on(
             node, interface, namespace=None, vf_id=None):
@@ -1576,9 +1739,52 @@ class InterfaceUtil:
         exec_cmd_no_error(node, cmd, sudo=True)
 
     @staticmethod
-    def init_avf_interface(node, ifc_key, numvfs=1, osi_layer=u"L2"):
-        """Init PCI device by creating VIFs and bind them to vfio-pci for AVF
-        driver testing on DUT.
+    def init_interface(node, ifc_key, driver, numvfs=0, osi_layer=u"L2"):
+        """Init PCI device. Check driver compatibility and bind to proper
+        drivers. Optionally create NIC VFs.
+
+        :param node: DUT node.
+        :param ifc_key: Interface key from topology file.
+        :param driver: Base driver to use.
+        :param numvfs: Number of VIFs to initialize, 0 - disable the VIFs.
+        :param osi_layer: OSI Layer type to initialize TG with.
+            Default value "L2" sets linux interface spoof off.
+        :type node: dict
+        :type ifc_key: str
+        :type driver: str
+        :type numvfs: int
+        :type osi_layer: str
+        :returns: Virtual Function topology interface keys.
+        :rtype: list
+        :raises RuntimeError: If a reason preventing initialization is found.
+        """
+        kernel_driver = Topology.get_interface_driver(node, ifc_key)
+        vf_keys = []
+        if driver == u"avf":
+            if kernel_driver not in (
+                    u"ice", u"iavf", u"i40e", u"i40evf"):
+                raise RuntimeError(
+                    f"AVF needs ice or i40e compatible driver, not "
+                    f"{kernel_driver} at node {node[u'host']} ifc {ifc_key}"
+                )
+            vf_keys = InterfaceUtil.init_generic_interface(
+                node, ifc_key, numvfs=numvfs, osi_layer=osi_layer
+            )
+        elif driver == u"af_xdp":
+            if kernel_driver not in (
+                    u"ice", u"iavf", u"i40e", u"i40evf", u"mlx5_core"):
+                raise RuntimeError(
+                    f"AF_XDP needs ice or i40e or rdma compatible driver, not "
+                    f"{kernel_driver} at node {node[u'host']} ifc {ifc_key}"
+                )
+            vf_keys = InterfaceUtil.init_generic_interface(
+                node, ifc_key, numvfs=numvfs, osi_layer=osi_layer
+            )
+        return vf_keys
+
+    @staticmethod
+    def init_generic_interface(node, ifc_key, numvfs=0, osi_layer=u"L2"):
+        """Init PCI device. Bind to proper drivers. Optionally create NIC VFs.
 
         :param node: DUT node.
         :param ifc_key: Interface key from topology file.
@@ -1598,13 +1804,9 @@ class InterfaceUtil:
         pf_mac_addr = Topology.get_interface_mac(node, ifc_key).split(":")
         uio_driver = Topology.get_uio_driver(node)
         kernel_driver = Topology.get_interface_driver(node, ifc_key)
-        if kernel_driver not in (u"ice", u"iavf", u"i40e", u"i40evf"):
-            raise RuntimeError(
-                f"AVF needs ice or i40e compatible driver, not {kernel_driver}"
-                f"at node {node[u'host']} ifc {ifc_key}"
-            )
         current_driver = DUTSetup.get_pci_dev_driver(
             node, pf_pci_addr.replace(u":", r"\:"))
+        pf_dev = f"`basename /sys/bus/pci/devices/{pf_pci_addr}/net/*`"
 
         VPPUtil.stop_vpp_service(node)
         if current_driver != kernel_driver:
@@ -1619,6 +1821,10 @@ class InterfaceUtil:
         # Initialize PCI VFs.
         DUTSetup.set_sriov_numvfs(node, pf_pci_addr, numvfs)
 
+        if not numvfs:
+            if osi_layer == u"L2":
+                InterfaceUtil.set_linux_interface_promisc(node, pf_dev)
+
         vf_ifc_keys = []
         # Set MAC address and bind each virtual function to uio driver.
         for vf_id in range(numvfs):
@@ -1628,7 +1834,6 @@ class InterfaceUtil:
                  ]
             )
 
-            pf_dev = f"`basename /sys/bus/pci/devices/{pf_pci_addr}/net/*`"
             InterfaceUtil.set_linux_interface_trust_on(
                 node, pf_dev, vf_id=vf_id
             )
@@ -1682,6 +1887,19 @@ class InterfaceUtil:
             details = papi_exec.get_details(err_msg)
         return sorted(details, key=lambda k: k[u"sw_if_index"])
 
+    @staticmethod
+    def vpp_sw_interface_rx_placement_dump_on_all_duts(nodes):
+        """Dump VPP interface RX placement on all given nodes.
+
+        :param nodes: Nodes to run command on.
+        :type nodes: dict
+        :returns: Thread mapping information as a list of dictionaries.
+        :rtype: list
+        """
+        for node in nodes.values():
+            if node[u"type"] == NodeType.DUT:
+                InterfaceUtil.vpp_sw_interface_rx_placement_dump(node)
+
     @staticmethod
     def vpp_sw_interface_set_rx_placement(
             node, sw_if_index, queue_id, worker_id):
@@ -1711,17 +1929,28 @@ class InterfaceUtil:
             papi_exec.add(cmd, **args).get_reply(err_msg)
 
     @staticmethod
-    def vpp_round_robin_rx_placement(node, prefix):
+    def vpp_round_robin_rx_placement(
+            node, prefix, dp_worker_limit=None):
         """Set Round Robin interface RX placement on all worker threads
         on node.
 
+        If specified, dp_core_limit limits the number of physical cores used
+        for data plane I/O work. Other cores are presumed to do something else,
+        e.g. asynchronous crypto processing.
+        None means all workers are used for data plane work.
+        Note this keyword specifies workers, not cores.
+
         :param node: Topology nodes.
         :param prefix: Interface name prefix.
+        :param dp_worker_limit: How many cores for data plane work.
         :type node: dict
         :type prefix: str
+        :type dp_worker_limit: Optional[int]
         """
         worker_id = 0
         worker_cnt = len(VPPUtil.vpp_show_threads(node)) - 1
+        if dp_worker_limit is not None:
+            worker_cnt = min(worker_cnt, dp_worker_limit)
         if not worker_cnt:
             return
         for placement in InterfaceUtil.vpp_sw_interface_rx_placement_dump(node):
@@ -1735,15 +1964,30 @@ class InterfaceUtil:
                     worker_id += 1
 
     @staticmethod
-    def vpp_round_robin_rx_placement_on_all_duts(nodes, prefix):
+    def vpp_round_robin_rx_placement_on_all_duts(
+            nodes, prefix, dp_core_limit=None):
         """Set Round Robin interface RX placement on all worker threads
         on all DUTs.
 
+        If specified, dp_core_limit limits the number of physical cores used
+        for data plane I/O work. Other cores are presumed to do something else,
+        e.g. asynchronous crypto processing.
+        None means all cores are used for data plane work.
+        Note this keyword specifies cores, not workers.
+
         :param nodes: Topology nodes.
         :param prefix: Interface name prefix.
+        :param dp_worker_limit: How many cores for data plane work.
         :type nodes: dict
         :type prefix: str
+        :type dp_worker_limit: Optional[int]
         """
         for node in nodes.values():
             if node[u"type"] == NodeType.DUT:
-                InterfaceUtil.vpp_round_robin_rx_placement(node, prefix)
+                dp_worker_limit = CpuUtils.worker_count_from_cores_and_smt(
+                    phy_cores=dp_core_limit,
+                    smt_used=CpuUtils.is_smt_enabled(node[u"cpuinfo"]),
+                )
+                InterfaceUtil.vpp_round_robin_rx_placement(
+                    node, prefix, dp_worker_limit
+                )