1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2024 University of New Hampshire 3 4"""Dynamic configuration of port queues test suite. 5 6This test suite tests the support of being able to either stop or reconfigure port queues at 7runtime without stopping the entire device. Previously, to configure a DPDK ethdev, the application 8first specifies how many Tx and Rx queues to include in the ethdev and then application sets up 9each queue individually. Only once all the queues have been set up can the application then start 10the device, and at this point traffic can flow. If device stops, this halts the flow of traffic on 11all queues in the ethdev completely. Dynamic queue is a capability present on some NICs that 12specifies whether the NIC is able to delay the configuration of queues on its port. This capability 13allows for the support of stopping and reconfiguring queues on a port at runtime without stopping 14the entire device. 15 16Support of this capability is shown by starting the Poll Mode Driver with multiple Rx and Tx queues 17configured and stopping some prior to forwarding packets, then examining whether or not the stopped 18ports and the unmodified ports were able to handle traffic. In addition to just stopping the ports, 19the ports must also show that they support configuration changes on their queues at runtime without 20stopping the entire device. This is shown by changing the ring size of the queues. 21 22If the Poll Mode Driver is able to stop some queues on a port and modify them then handle traffic 23on the unmodified queues while the others are stopped, then it is the case that the device properly 24supports dynamic configuration of its queues. 25""" 26 27import random 28from typing import Callable, ClassVar, MutableSet 29 30from scapy.layers.inet import IP # type: ignore[import-untyped] 31from scapy.layers.l2 import Ether # type: ignore[import-untyped] 32from scapy.packet import Raw # type: ignore[import-untyped] 33 34from framework.exception import InteractiveCommandExecutionError 35from framework.params.testpmd import PortTopology, SimpleForwardingModes 36from framework.remote_session.testpmd_shell import TestPmdShell 37from framework.test_suite import TestSuite, func_test 38from framework.testbed_model.capability import NicCapability, requires 39 40 41def setup_and_teardown_test( 42 test_meth: Callable[ 43 ["TestDynamicQueueConf", int, MutableSet, MutableSet, TestPmdShell, bool], None 44 ], 45) -> Callable[["TestDynamicQueueConf", bool], None]: 46 """Decorator that provides a setup and teardown for testing methods. 47 48 This decorator provides a method that sets up the environment for testing, runs the test 49 method, and then does a clean-up verification step after the queues are started again. The 50 decorated method will be provided with all the variables it should need to run testing 51 including: The ID of the port where the queues for testing reside, disjoint sets of IDs for 52 queues that are/aren't modified, a testpmd session to run testing with, and a flag that 53 indicates whether or not testing should be done on Rx or Tx queues. 54 55 Args: 56 test_meth: The decorated method that tests configuration of port queues at runtime. 57 This method must have the following parameters in order: An int that represents a 58 port ID, a set of queues for testing, a set of unmodified queues, a testpmd 59 interactive shell, and a boolean that, when :data:`True`, does Rx testing, 60 otherwise does Tx testing. This method must also be a member of the 61 :class:`TestDynamicQueueConf` class. 62 63 Returns: 64 A method that sets up the environment, runs the decorated method, then re-enables all 65 queues and validates they can still handle traffic. 66 """ 67 68 def wrap(self: "TestDynamicQueueConf", is_rx_testing: bool) -> None: 69 """Setup environment, run test function, then cleanup. 70 71 Start a testpmd shell and stop ports for testing, then call the decorated function that 72 performs the testing. After the decorated function is finished running its testing, 73 start the stopped queues and send packets to validate that these ports can properly 74 handle traffic after being started again. 75 76 Args: 77 self: Instance of :class:`TestDynamicQueueConf` `test_meth` belongs to. 78 is_rx_testing: If :data:`True` then Rx queues will be the ones modified throughout 79 the test, otherwise Tx queues will be modified. 80 """ 81 port_id = self.rx_port_num if is_rx_testing else self.tx_port_num 82 queues_to_config: set[int] = set() 83 while len(queues_to_config) < self.num_ports_to_modify: 84 queues_to_config.add(random.randint(1, self.number_of_queues - 1)) 85 unchanged_queues = set(range(self.number_of_queues)) - queues_to_config 86 with TestPmdShell( 87 self.sut_node, 88 port_topology=PortTopology.chained, 89 rx_queues=self.number_of_queues, 90 tx_queues=self.number_of_queues, 91 ) as testpmd: 92 for q in queues_to_config: 93 testpmd.stop_port_queue(port_id, q, is_rx_testing) 94 testpmd.set_forward_mode(SimpleForwardingModes.mac) 95 96 test_meth(self, port_id, queues_to_config, unchanged_queues, testpmd, is_rx_testing) 97 98 for queue_id in queues_to_config: 99 testpmd.start_port_queue(port_id, queue_id, is_rx_testing) 100 101 testpmd.start() 102 self.send_packets_with_different_addresses(self.number_of_packets_to_send) 103 forwarding_stats = testpmd.stop() 104 for queue_id in queues_to_config: 105 self.verify( 106 self.port_queue_in_stats(port_id, is_rx_testing, queue_id, forwarding_stats), 107 f"Modified queue {queue_id} on port {port_id} failed to receive traffic after" 108 "being started again.", 109 ) 110 111 return wrap 112 113 114class TestDynamicQueueConf(TestSuite): 115 """DPDK dynamic queue configuration test suite. 116 117 Testing for the support of dynamic queue configuration is done by splitting testing by the type 118 of queue (either Rx or Tx) and the type of testing (testing for stopping a port at runtime vs 119 testing configuration changes at runtime). Testing is done by first stopping a finite number of 120 port queues (3 is sufficient) and either modifying the configuration or sending packets to 121 verify that the unmodified queues can handle traffic. Specifically, the following cases are 122 tested: 123 124 1. The application should be able to start the device with only some of the 125 queues set up. 126 2. The application should be able to reconfigure existing queues at runtime 127 without calling dev_stop(). 128 """ 129 130 #: 131 num_ports_to_modify: ClassVar[int] = 3 132 #: Source IP address to use when sending packets. 133 src_addr: ClassVar[str] = "192.168.0.1" 134 #: Subnet to use for all of the destination addresses of the packets being sent. 135 dst_address_subnet: ClassVar[str] = "192.168.1" 136 #: ID of the port to modify Rx queues on. 137 rx_port_num: ClassVar[int] = 0 138 #: ID of the port to modify Tx queues on. 139 tx_port_num: ClassVar[int] = 1 140 #: Number of queues to start testpmd with. There will be the same number of Rx and Tx queues. 141 #: 8 was chosen as a number that is low enough for most NICs to accommodate while also being 142 #: enough to validate the usage of the queues. 143 number_of_queues: ClassVar[int] = 8 144 #: The number of packets to send while testing. The test calls for well over the ring size - 1 145 #: packets in the modification test case and the only options for ring size are 256 or 512, 146 #: therefore 1024 will be more than enough. 147 number_of_packets_to_send: ClassVar[int] = 1024 148 149 def send_packets_with_different_addresses(self, number_of_packets: int) -> None: 150 """Send a set number of packets each with different dst addresses. 151 152 Different destination addresses are required to ensure that each queue is used. If every 153 packet had the same address, then they would all be processed by the same queue. Note that 154 this means the current implementation of this method is limited to only work for up to 254 155 queues. A smaller subnet would be required to handle an increased number of queues. 156 157 Args: 158 number_of_packets: The number of packets to generate and then send using the traffic 159 generator. 160 """ 161 packets_to_send = [ 162 Ether() 163 / IP(src=self.src_addr, dst=f"{self.dst_address_subnet}.{(i % 254) + 1}") 164 / Raw() 165 for i in range(number_of_packets) 166 ] 167 self.send_packets(packets_to_send) 168 169 def port_queue_in_stats( 170 self, port_id: int, is_rx_queue: bool, queue_id: int, stats: str 171 ) -> bool: 172 """Verify if stats for a queue are in the provided output. 173 174 Args: 175 port_id: ID of the port that the queue resides on. 176 is_rx_queue: Type of queue to scan for, if :data:`True` then search for an Rx queue, 177 otherwise search for a Tx queue. 178 queue_id: ID of the queue. 179 stats: Testpmd forwarding statistics to scan for the given queue. 180 181 Returns: 182 If the queue appeared in the forwarding statistics. 183 """ 184 type_of_queue = "RX" if is_rx_queue else "TX" 185 return f"{type_of_queue} Port= {port_id}/Queue={queue_id:2d}" in stats 186 187 @setup_and_teardown_test 188 def modify_ring_size( 189 self, 190 port_id: int, 191 queues_to_modify: MutableSet[int], 192 unchanged_queues: MutableSet[int], 193 testpmd: TestPmdShell, 194 is_rx_testing: bool, 195 ) -> None: 196 """Verify ring size of port queues can be configured at runtime. 197 198 Ring size of queues in `queues_to_modify` are set to 512 unless that is already their 199 configured size, in which case they are instead set to 256. Queues in `queues_to_modify` 200 are expected to already be stopped before calling this method. `testpmd` is also expected 201 to already be started. 202 203 Args: 204 port_id: Port where the queues reside. 205 queues_to_modify: IDs of stopped queues to configure in the test. 206 unchanged_queues: IDs of running, unmodified queues. 207 testpmd: Running interactive testpmd application. 208 is_rx_testing: If :data:`True` Rx queues will be modified in the test, otherwise Tx 209 queues will be modified. 210 """ 211 for queue_id in queues_to_modify: 212 curr_ring_size = testpmd.get_queue_ring_size(port_id, queue_id, is_rx_testing) 213 new_ring_size = 256 if curr_ring_size == 512 else 512 214 try: 215 testpmd.set_queue_ring_size( 216 port_id, queue_id, new_ring_size, is_rx_testing, verify=True 217 ) 218 # The testpmd method verifies that the modification worked, so we catch that error 219 # and just re-raise it as a test case failure 220 except InteractiveCommandExecutionError: 221 self.verify( 222 False, 223 f"Failed to update the ring size of queue {queue_id} on port " 224 f"{port_id} at runtime", 225 ) 226 227 @setup_and_teardown_test 228 def stop_queues( 229 self, 230 port_id: int, 231 queues_to_modify: MutableSet[int], 232 unchanged_queues: MutableSet[int], 233 testpmd: TestPmdShell, 234 is_rx_testing: bool, 235 ) -> None: 236 """Verify stopped queues do not handle traffic and do not block traffic on other queues. 237 238 Queues in `queues_to_modify` are expected to already be stopped before calling this method. 239 `testpmd` is also expected to already be started. 240 241 Args: 242 port_id: Port where the queues reside. 243 queues_to_modify: IDs of stopped queues to configure in the test. 244 unchanged_queues: IDs of running, unmodified queues. 245 testpmd: Running interactive testpmd application. 246 is_rx_testing: If :data:`True` Rx queues will be modified in the test, otherwise Tx 247 queues will be modified. 248 """ 249 testpmd.start() 250 self.send_packets_with_different_addresses(self.number_of_packets_to_send) 251 forwarding_stats = testpmd.stop() 252 253 # Checking that all unmodified queues handled some packets is important because this 254 # test case checks for the absence of stopped queues to validate that they cannot 255 # receive traffic. If there are some unchanged queues that also didn't receive traffic, 256 # it means there could be another reason for the packets not transmitting and, 257 # therefore, a false positive result. 258 for unchanged_q_id in unchanged_queues: 259 self.verify( 260 self.port_queue_in_stats(port_id, is_rx_testing, unchanged_q_id, forwarding_stats), 261 f"Queue {unchanged_q_id} failed to receive traffic.", 262 ) 263 for stopped_q_id in queues_to_modify: 264 self.verify( 265 not self.port_queue_in_stats( 266 port_id, is_rx_testing, stopped_q_id, forwarding_stats 267 ), 268 f"Queue {stopped_q_id} should be stopped but still received traffic.", 269 ) 270 271 @requires(NicCapability.RUNTIME_RX_QUEUE_SETUP) 272 @func_test 273 def test_rx_queue_stop(self): 274 """Run method for stopping queues with flag for Rx testing set to :data:`True`.""" 275 self.stop_queues(True) 276 277 @requires(NicCapability.RUNTIME_RX_QUEUE_SETUP) 278 @func_test 279 def test_rx_queue_configuration(self): 280 """Run method for configuring queues with flag for Rx testing set to :data:`True`.""" 281 self.modify_ring_size(True) 282 283 @requires(NicCapability.RUNTIME_TX_QUEUE_SETUP) 284 @func_test 285 def test_tx_queue_stop(self): 286 """Run method for stopping queues with flag for Rx testing set to :data:`False`.""" 287 self.stop_queues(False) 288 289 @requires(NicCapability.RUNTIME_TX_QUEUE_SETUP) 290 @func_test 291 def test_tx_queue_configuration(self): 292 """Run method for configuring queues with flag for Rx testing set to :data:`False`.""" 293 self.modify_ring_size(False) 294