|
| 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 | +
|
| 6 | +This test suite tests the support of being able to either stop or reconfigure port queues at |
| 7 | +runtime without stopping the entire device. Previously, to configure a DPDK ethdev, the application |
| 8 | +first specifies how many Tx and Rx queues to include in the ethdev and then application sets up |
| 9 | +each queue individually. Only once all the queues have been set up can the application then start |
| 10 | +the device, and at this point traffic can flow. If device stops, this halts the flow of traffic on |
| 11 | +all queues in the ethdev completely. Dynamic queue is a capability present on some NICs that |
| 12 | +specifies whether the NIC is able to delay the configuration of queues on its port. This capability |
| 13 | +allows for the support of stopping and reconfiguring queues on a port at runtime without stopping |
| 14 | +the entire device. |
| 15 | +
|
| 16 | +Support of this capability is shown by starting the Poll Mode Driver with multiple Rx and Tx queues |
| 17 | +configured and stopping some prior to forwarding packets, then examining whether or not the stopped |
| 18 | +ports and the unmodified ports were able to handle traffic. In addition to just stopping the ports, |
| 19 | +the ports must also show that they support configuration changes on their queues at runtime without |
| 20 | +stopping the entire device. This is shown by changing the ring size of the queues. |
| 21 | +
|
| 22 | +If the Poll Mode Driver is able to stop some queues on a port and modify them then handle traffic |
| 23 | +on the unmodified queues while the others are stopped, then it is the case that the device properly |
| 24 | +supports dynamic configuration of its queues. |
| 25 | +""" |
| 26 | + |
| 27 | +import random |
| 28 | +from typing import Callable, ClassVar, MutableSet |
| 29 | + |
| 30 | +from scapy.layers.inet import IP # type: ignore[import-untyped] |
| 31 | +from scapy.layers.l2 import Ether # type: ignore[import-untyped] |
| 32 | +from scapy.packet import Raw # type: ignore[import-untyped] |
| 33 | + |
| 34 | +from framework.exception import InteractiveCommandExecutionError |
| 35 | +from framework.params.testpmd import PortTopology, SimpleForwardingModes |
| 36 | +from framework.remote_session.testpmd_shell import TestPmdShell |
| 37 | +from framework.test_suite import TestSuite, func_test |
| 38 | +from framework.testbed_model.capability import NicCapability, requires |
| 39 | + |
| 40 | + |
| 41 | +def 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 | + |
| 114 | +class 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) |
0 commit comments