1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2023 University of New Hampshire 3# Copyright(c) 2023 PANTHEON.tech s.r.o. 4# Copyright(c) 2024 Arm Limited 5 6"""Testpmd interactive shell. 7 8Typical usage example in a TestSuite:: 9 10 testpmd_shell = TestPmdShell(self.sut_node) 11 devices = testpmd_shell.get_devices() 12 for device in devices: 13 print(device) 14 testpmd_shell.close() 15""" 16 17import functools 18import re 19import time 20from dataclasses import dataclass, field 21from enum import Flag, auto 22from pathlib import PurePath 23from typing import Any, Callable, ClassVar, Concatenate, ParamSpec 24 25from typing_extensions import Self, Unpack 26 27from framework.exception import InteractiveCommandExecutionError, InternalError 28from framework.params.testpmd import SimpleForwardingModes, TestPmdParams 29from framework.params.types import TestPmdParamsDict 30from framework.parser import ParserFn, TextParser 31from framework.remote_session.dpdk_shell import DPDKShell 32from framework.settings import SETTINGS 33from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList 34from framework.testbed_model.sut_node import SutNode 35from framework.utils import StrEnum 36 37P = ParamSpec("P") 38TestPmdShellMethod = Callable[Concatenate["TestPmdShell", P], Any] 39 40 41class TestPmdDevice: 42 """The data of a device that testpmd can recognize. 43 44 Attributes: 45 pci_address: The PCI address of the device. 46 """ 47 48 pci_address: str 49 50 def __init__(self, pci_address_line: str): 51 """Initialize the device from the testpmd output line string. 52 53 Args: 54 pci_address_line: A line of testpmd output that contains a device. 55 """ 56 self.pci_address = pci_address_line.strip().split(": ")[1].strip() 57 58 def __str__(self) -> str: 59 """The PCI address captures what the device is.""" 60 return self.pci_address 61 62 63class VLANOffloadFlag(Flag): 64 """Flag representing the VLAN offload settings of a NIC port.""" 65 66 #: 67 STRIP = auto() 68 #: 69 FILTER = auto() 70 #: 71 EXTEND = auto() 72 #: 73 QINQ_STRIP = auto() 74 75 @classmethod 76 def from_str_dict(cls, d): 77 """Makes an instance from a dict containing the flag member names with an "on" value. 78 79 Args: 80 d: A dictionary containing the flag members as keys and any string value. 81 82 Returns: 83 A new instance of the flag. 84 """ 85 flag = cls(0) 86 for name in cls.__members__: 87 if d.get(name) == "on": 88 flag |= cls[name] 89 return flag 90 91 @classmethod 92 def make_parser(cls) -> ParserFn: 93 """Makes a parser function. 94 95 Returns: 96 ParserFn: A dictionary for the `dataclasses.field` metadata argument containing a 97 parser function that makes an instance of this flag from text. 98 """ 99 return TextParser.wrap( 100 TextParser.find( 101 r"VLAN offload:\s+" 102 r"strip (?P<STRIP>on|off), " 103 r"filter (?P<FILTER>on|off), " 104 r"extend (?P<EXTEND>on|off), " 105 r"qinq strip (?P<QINQ_STRIP>on|off)$", 106 re.MULTILINE, 107 named=True, 108 ), 109 cls.from_str_dict, 110 ) 111 112 113class RSSOffloadTypesFlag(Flag): 114 """Flag representing the RSS offload flow types supported by the NIC port.""" 115 116 #: 117 ipv4 = auto() 118 #: 119 ipv4_frag = auto() 120 #: 121 ipv4_tcp = auto() 122 #: 123 ipv4_udp = auto() 124 #: 125 ipv4_sctp = auto() 126 #: 127 ipv4_other = auto() 128 #: 129 ipv6 = auto() 130 #: 131 ipv6_frag = auto() 132 #: 133 ipv6_tcp = auto() 134 #: 135 ipv6_udp = auto() 136 #: 137 ipv6_sctp = auto() 138 #: 139 ipv6_other = auto() 140 #: 141 l2_payload = auto() 142 #: 143 ipv6_ex = auto() 144 #: 145 ipv6_tcp_ex = auto() 146 #: 147 ipv6_udp_ex = auto() 148 #: 149 port = auto() 150 #: 151 vxlan = auto() 152 #: 153 geneve = auto() 154 #: 155 nvgre = auto() 156 #: 157 user_defined_22 = auto() 158 #: 159 gtpu = auto() 160 #: 161 eth = auto() 162 #: 163 s_vlan = auto() 164 #: 165 c_vlan = auto() 166 #: 167 esp = auto() 168 #: 169 ah = auto() 170 #: 171 l2tpv3 = auto() 172 #: 173 pfcp = auto() 174 #: 175 pppoe = auto() 176 #: 177 ecpri = auto() 178 #: 179 mpls = auto() 180 #: 181 ipv4_chksum = auto() 182 #: 183 l4_chksum = auto() 184 #: 185 l2tpv2 = auto() 186 #: 187 ipv6_flow_label = auto() 188 #: 189 user_defined_38 = auto() 190 #: 191 user_defined_39 = auto() 192 #: 193 user_defined_40 = auto() 194 #: 195 user_defined_41 = auto() 196 #: 197 user_defined_42 = auto() 198 #: 199 user_defined_43 = auto() 200 #: 201 user_defined_44 = auto() 202 #: 203 user_defined_45 = auto() 204 #: 205 user_defined_46 = auto() 206 #: 207 user_defined_47 = auto() 208 #: 209 user_defined_48 = auto() 210 #: 211 user_defined_49 = auto() 212 #: 213 user_defined_50 = auto() 214 #: 215 user_defined_51 = auto() 216 #: 217 l3_pre96 = auto() 218 #: 219 l3_pre64 = auto() 220 #: 221 l3_pre56 = auto() 222 #: 223 l3_pre48 = auto() 224 #: 225 l3_pre40 = auto() 226 #: 227 l3_pre32 = auto() 228 #: 229 l2_dst_only = auto() 230 #: 231 l2_src_only = auto() 232 #: 233 l4_dst_only = auto() 234 #: 235 l4_src_only = auto() 236 #: 237 l3_dst_only = auto() 238 #: 239 l3_src_only = auto() 240 241 #: 242 ip = ipv4 | ipv4_frag | ipv4_other | ipv6 | ipv6_frag | ipv6_other | ipv6_ex 243 #: 244 udp = ipv4_udp | ipv6_udp | ipv6_udp_ex 245 #: 246 tcp = ipv4_tcp | ipv6_tcp | ipv6_tcp_ex 247 #: 248 sctp = ipv4_sctp | ipv6_sctp 249 #: 250 tunnel = vxlan | geneve | nvgre 251 #: 252 vlan = s_vlan | c_vlan 253 #: 254 all = ( 255 eth 256 | vlan 257 | ip 258 | tcp 259 | udp 260 | sctp 261 | l2_payload 262 | l2tpv3 263 | esp 264 | ah 265 | pfcp 266 | gtpu 267 | ecpri 268 | mpls 269 | l2tpv2 270 ) 271 272 @classmethod 273 def from_list_string(cls, names: str) -> Self: 274 """Makes a flag from a whitespace-separated list of names. 275 276 Args: 277 names: a whitespace-separated list containing the members of this flag. 278 279 Returns: 280 An instance of this flag. 281 """ 282 flag = cls(0) 283 for name in names.split(): 284 flag |= cls.from_str(name) 285 return flag 286 287 @classmethod 288 def from_str(cls, name: str) -> Self: 289 """Makes a flag matching the supplied name. 290 291 Args: 292 name: a valid member of this flag in text 293 Returns: 294 An instance of this flag. 295 """ 296 member_name = name.strip().replace("-", "_") 297 return cls[member_name] 298 299 @classmethod 300 def make_parser(cls) -> ParserFn: 301 """Makes a parser function. 302 303 Returns: 304 ParserFn: A dictionary for the `dataclasses.field` metadata argument containing a 305 parser function that makes an instance of this flag from text. 306 """ 307 return TextParser.wrap( 308 TextParser.find(r"Supported RSS offload flow types:((?:\r?\n? \S+)+)", re.MULTILINE), 309 RSSOffloadTypesFlag.from_list_string, 310 ) 311 312 313class DeviceCapabilitiesFlag(Flag): 314 """Flag representing the device capabilities.""" 315 316 #: Device supports Rx queue setup after device started. 317 RUNTIME_RX_QUEUE_SETUP = auto() 318 #: Device supports Tx queue setup after device started. 319 RUNTIME_TX_QUEUE_SETUP = auto() 320 #: Device supports shared Rx queue among ports within Rx domain and switch domain. 321 RXQ_SHARE = auto() 322 #: Device supports keeping flow rules across restart. 323 FLOW_RULE_KEEP = auto() 324 #: Device supports keeping shared flow objects across restart. 325 FLOW_SHARED_OBJECT_KEEP = auto() 326 327 @classmethod 328 def make_parser(cls) -> ParserFn: 329 """Makes a parser function. 330 331 Returns: 332 ParserFn: A dictionary for the `dataclasses.field` metadata argument containing a 333 parser function that makes an instance of this flag from text. 334 """ 335 return TextParser.wrap( 336 TextParser.find_int(r"Device capabilities: (0x[A-Fa-f\d]+)"), 337 cls, 338 ) 339 340 341class DeviceErrorHandlingMode(StrEnum): 342 """Enum representing the device error handling mode.""" 343 344 #: 345 none = auto() 346 #: 347 passive = auto() 348 #: 349 proactive = auto() 350 #: 351 unknown = auto() 352 353 @classmethod 354 def make_parser(cls) -> ParserFn: 355 """Makes a parser function. 356 357 Returns: 358 ParserFn: A dictionary for the `dataclasses.field` metadata argument containing a 359 parser function that makes an instance of this enum from text. 360 """ 361 return TextParser.wrap(TextParser.find(r"Device error handling mode: (\w+)"), cls) 362 363 364def make_device_private_info_parser() -> ParserFn: 365 """Device private information parser. 366 367 Ensures that we are not parsing invalid device private info output. 368 369 Returns: 370 ParserFn: A dictionary for the `dataclasses.field` metadata argument containing a parser 371 function that parses the device private info from the TestPmd port info output. 372 """ 373 374 def _validate(info: str): 375 info = info.strip() 376 if info == "none" or info.startswith("Invalid file") or info.startswith("Failed to dump"): 377 return None 378 return info 379 380 return TextParser.wrap(TextParser.find(r"Device private info:\s+([\s\S]+)"), _validate) 381 382 383@dataclass 384class TestPmdPort(TextParser): 385 """Dataclass representing the result of testpmd's ``show port info`` command.""" 386 387 #: 388 id: int = field(metadata=TextParser.find_int(r"Infos for port (\d+)\b")) 389 #: 390 device_name: str = field(metadata=TextParser.find(r"Device name: ([^\r\n]+)")) 391 #: 392 driver_name: str = field(metadata=TextParser.find(r"Driver name: ([^\r\n]+)")) 393 #: 394 socket_id: int = field(metadata=TextParser.find_int(r"Connect to socket: (\d+)")) 395 #: 396 is_link_up: bool = field(metadata=TextParser.find("Link status: up")) 397 #: 398 link_speed: str = field(metadata=TextParser.find(r"Link speed: ([^\r\n]+)")) 399 #: 400 is_link_full_duplex: bool = field(metadata=TextParser.find("Link duplex: full-duplex")) 401 #: 402 is_link_autonegotiated: bool = field(metadata=TextParser.find("Autoneg status: On")) 403 #: 404 is_promiscuous_mode_enabled: bool = field(metadata=TextParser.find("Promiscuous mode: enabled")) 405 #: 406 is_allmulticast_mode_enabled: bool = field( 407 metadata=TextParser.find("Allmulticast mode: enabled") 408 ) 409 #: Maximum number of MAC addresses 410 max_mac_addresses_num: int = field( 411 metadata=TextParser.find_int(r"Maximum number of MAC addresses: (\d+)") 412 ) 413 #: Maximum configurable length of RX packet 414 max_hash_mac_addresses_num: int = field( 415 metadata=TextParser.find_int(r"Maximum number of MAC addresses of hash filtering: (\d+)") 416 ) 417 #: Minimum size of RX buffer 418 min_rx_bufsize: int = field(metadata=TextParser.find_int(r"Minimum size of RX buffer: (\d+)")) 419 #: Maximum configurable length of RX packet 420 max_rx_packet_length: int = field( 421 metadata=TextParser.find_int(r"Maximum configurable length of RX packet: (\d+)") 422 ) 423 #: Maximum configurable size of LRO aggregated packet 424 max_lro_packet_size: int = field( 425 metadata=TextParser.find_int(r"Maximum configurable size of LRO aggregated packet: (\d+)") 426 ) 427 428 #: Current number of RX queues 429 rx_queues_num: int = field(metadata=TextParser.find_int(r"Current number of RX queues: (\d+)")) 430 #: Max possible RX queues 431 max_rx_queues_num: int = field(metadata=TextParser.find_int(r"Max possible RX queues: (\d+)")) 432 #: Max possible number of RXDs per queue 433 max_queue_rxd_num: int = field( 434 metadata=TextParser.find_int(r"Max possible number of RXDs per queue: (\d+)") 435 ) 436 #: Min possible number of RXDs per queue 437 min_queue_rxd_num: int = field( 438 metadata=TextParser.find_int(r"Min possible number of RXDs per queue: (\d+)") 439 ) 440 #: RXDs number alignment 441 rxd_alignment_num: int = field(metadata=TextParser.find_int(r"RXDs number alignment: (\d+)")) 442 443 #: Current number of TX queues 444 tx_queues_num: int = field(metadata=TextParser.find_int(r"Current number of TX queues: (\d+)")) 445 #: Max possible TX queues 446 max_tx_queues_num: int = field(metadata=TextParser.find_int(r"Max possible TX queues: (\d+)")) 447 #: Max possible number of TXDs per queue 448 max_queue_txd_num: int = field( 449 metadata=TextParser.find_int(r"Max possible number of TXDs per queue: (\d+)") 450 ) 451 #: Min possible number of TXDs per queue 452 min_queue_txd_num: int = field( 453 metadata=TextParser.find_int(r"Min possible number of TXDs per queue: (\d+)") 454 ) 455 #: TXDs number alignment 456 txd_alignment_num: int = field(metadata=TextParser.find_int(r"TXDs number alignment: (\d+)")) 457 #: Max segment number per packet 458 max_packet_segment_num: int = field( 459 metadata=TextParser.find_int(r"Max segment number per packet: (\d+)") 460 ) 461 #: Max segment number per MTU/TSO 462 max_mtu_segment_num: int = field( 463 metadata=TextParser.find_int(r"Max segment number per MTU\/TSO: (\d+)") 464 ) 465 466 #: 467 device_capabilities: DeviceCapabilitiesFlag = field( 468 metadata=DeviceCapabilitiesFlag.make_parser(), 469 ) 470 #: 471 device_error_handling_mode: DeviceErrorHandlingMode | None = field( 472 default=None, metadata=DeviceErrorHandlingMode.make_parser() 473 ) 474 #: 475 device_private_info: str | None = field( 476 default=None, 477 metadata=make_device_private_info_parser(), 478 ) 479 480 #: 481 hash_key_size: int | None = field( 482 default=None, metadata=TextParser.find_int(r"Hash key size in bytes: (\d+)") 483 ) 484 #: 485 redirection_table_size: int | None = field( 486 default=None, metadata=TextParser.find_int(r"Redirection table size: (\d+)") 487 ) 488 #: 489 supported_rss_offload_flow_types: RSSOffloadTypesFlag = field( 490 default=RSSOffloadTypesFlag(0), metadata=RSSOffloadTypesFlag.make_parser() 491 ) 492 493 #: 494 mac_address: str | None = field( 495 default=None, metadata=TextParser.find(r"MAC address: ([A-Fa-f0-9:]+)") 496 ) 497 #: 498 fw_version: str | None = field( 499 default=None, metadata=TextParser.find(r"Firmware-version: ([^\r\n]+)") 500 ) 501 #: 502 dev_args: str | None = field(default=None, metadata=TextParser.find(r"Devargs: ([^\r\n]+)")) 503 #: Socket id of the memory allocation 504 mem_alloc_socket_id: int | None = field( 505 default=None, 506 metadata=TextParser.find_int(r"memory allocation on the socket: (\d+)"), 507 ) 508 #: 509 mtu: int | None = field(default=None, metadata=TextParser.find_int(r"MTU: (\d+)")) 510 511 #: 512 vlan_offload: VLANOffloadFlag | None = field( 513 default=None, 514 metadata=VLANOffloadFlag.make_parser(), 515 ) 516 517 #: Maximum size of RX buffer 518 max_rx_bufsize: int | None = field( 519 default=None, metadata=TextParser.find_int(r"Maximum size of RX buffer: (\d+)") 520 ) 521 #: Maximum number of VFs 522 max_vfs_num: int | None = field( 523 default=None, metadata=TextParser.find_int(r"Maximum number of VFs: (\d+)") 524 ) 525 #: Maximum number of VMDq pools 526 max_vmdq_pools_num: int | None = field( 527 default=None, metadata=TextParser.find_int(r"Maximum number of VMDq pools: (\d+)") 528 ) 529 530 #: 531 switch_name: str | None = field( 532 default=None, metadata=TextParser.find(r"Switch name: ([\r\n]+)") 533 ) 534 #: 535 switch_domain_id: int | None = field( 536 default=None, metadata=TextParser.find_int(r"Switch domain Id: (\d+)") 537 ) 538 #: 539 switch_port_id: int | None = field( 540 default=None, metadata=TextParser.find_int(r"Switch Port Id: (\d+)") 541 ) 542 #: 543 switch_rx_domain: int | None = field( 544 default=None, metadata=TextParser.find_int(r"Switch Rx domain: (\d+)") 545 ) 546 547 548@dataclass 549class TestPmdPortStats(TextParser): 550 """Port statistics.""" 551 552 #: 553 port_id: int = field(metadata=TextParser.find_int(r"NIC statistics for port (\d+)")) 554 555 #: 556 rx_packets: int = field(metadata=TextParser.find_int(r"RX-packets:\s+(\d+)")) 557 #: 558 rx_missed: int = field(metadata=TextParser.find_int(r"RX-missed:\s+(\d+)")) 559 #: 560 rx_bytes: int = field(metadata=TextParser.find_int(r"RX-bytes:\s+(\d+)")) 561 #: 562 rx_errors: int = field(metadata=TextParser.find_int(r"RX-errors:\s+(\d+)")) 563 #: 564 rx_nombuf: int = field(metadata=TextParser.find_int(r"RX-nombuf:\s+(\d+)")) 565 566 #: 567 tx_packets: int = field(metadata=TextParser.find_int(r"TX-packets:\s+(\d+)")) 568 #: 569 tx_errors: int = field(metadata=TextParser.find_int(r"TX-errors:\s+(\d+)")) 570 #: 571 tx_bytes: int = field(metadata=TextParser.find_int(r"TX-bytes:\s+(\d+)")) 572 573 #: 574 rx_pps: int = field(metadata=TextParser.find_int(r"Rx-pps:\s+(\d+)")) 575 #: 576 rx_bps: int = field(metadata=TextParser.find_int(r"Rx-bps:\s+(\d+)")) 577 578 #: 579 tx_pps: int = field(metadata=TextParser.find_int(r"Tx-pps:\s+(\d+)")) 580 #: 581 tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)")) 582 583 584def requires_stopped_ports(func: TestPmdShellMethod) -> TestPmdShellMethod: 585 """Decorator for :class:`TestPmdShell` commands methods that require stopped ports. 586 587 If the decorated method is called while the ports are started, then these are stopped before 588 continuing. 589 590 Args: 591 func: The :class:`TestPmdShell` method to decorate. 592 """ 593 594 @functools.wraps(func) 595 def _wrapper(self: "TestPmdShell", *args: P.args, **kwargs: P.kwargs): 596 if self.ports_started: 597 self._logger.debug("Ports need to be stopped to continue.") 598 self.stop_all_ports() 599 600 return func(self, *args, **kwargs) 601 602 return _wrapper 603 604 605def requires_started_ports(func: TestPmdShellMethod) -> TestPmdShellMethod: 606 """Decorator for :class:`TestPmdShell` commands methods that require started ports. 607 608 If the decorated method is called while the ports are stopped, then these are started before 609 continuing. 610 611 Args: 612 func: The :class:`TestPmdShell` method to decorate. 613 """ 614 615 @functools.wraps(func) 616 def _wrapper(self: "TestPmdShell", *args: P.args, **kwargs: P.kwargs): 617 if not self.ports_started: 618 self._logger.debug("Ports need to be started to continue.") 619 self.start_all_ports() 620 621 return func(self, *args, **kwargs) 622 623 return _wrapper 624 625 626class TestPmdShell(DPDKShell): 627 """Testpmd interactive shell. 628 629 The testpmd shell users should never use 630 the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather 631 call specialized methods. If there isn't one that satisfies a need, it should be added. 632 633 Attributes: 634 ports_started: Indicates whether the ports are started. 635 """ 636 637 _app_params: TestPmdParams 638 _ports: list[TestPmdPort] | None 639 640 #: The path to the testpmd executable. 641 path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd") 642 643 #: The testpmd's prompt. 644 _default_prompt: ClassVar[str] = "testpmd>" 645 646 #: This forces the prompt to appear after sending a command. 647 _command_extra_chars: ClassVar[str] = "\n" 648 649 ports_started: bool 650 651 def __init__( 652 self, 653 node: SutNode, 654 privileged: bool = True, 655 timeout: float = SETTINGS.timeout, 656 lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(), 657 ascending_cores: bool = True, 658 append_prefix_timestamp: bool = True, 659 name: str | None = None, 660 **app_params: Unpack[TestPmdParamsDict], 661 ) -> None: 662 """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs.""" 663 super().__init__( 664 node, 665 privileged, 666 timeout, 667 lcore_filter_specifier, 668 ascending_cores, 669 append_prefix_timestamp, 670 TestPmdParams(**app_params), 671 name, 672 ) 673 self.ports_started = not self._app_params.disable_device_start 674 self._ports = None 675 676 @property 677 def ports(self) -> list[TestPmdPort]: 678 """The ports of the instance. 679 680 This caches the ports returned by :meth:`show_port_info_all`. 681 To force an update of port information, execute :meth:`show_port_info_all` or 682 :meth:`show_port_info`. 683 684 Returns: The list of known testpmd ports. 685 """ 686 if self._ports is None: 687 return self.show_port_info_all() 688 return self._ports 689 690 @requires_started_ports 691 def start(self, verify: bool = True) -> None: 692 """Start packet forwarding with the current configuration. 693 694 Args: 695 verify: If :data:`True` , a second start command will be sent in an attempt to verify 696 packet forwarding started as expected. 697 698 Raises: 699 InteractiveCommandExecutionError: If `verify` is :data:`True` and forwarding fails to 700 start or ports fail to come up. 701 """ 702 self.send_command("start") 703 if verify: 704 # If forwarding was already started, sending "start" again should tell us 705 start_cmd_output = self.send_command("start") 706 if "Packet forwarding already started" not in start_cmd_output: 707 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}") 708 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.") 709 710 number_of_ports = len(self._app_params.ports or []) 711 for port_id in range(number_of_ports): 712 if not self.wait_link_status_up(port_id): 713 raise InteractiveCommandExecutionError( 714 "Not all ports came up after starting packet forwarding in testpmd." 715 ) 716 717 def stop(self, verify: bool = True) -> None: 718 """Stop packet forwarding. 719 720 Args: 721 verify: If :data:`True` , the output of the stop command is scanned to verify that 722 forwarding was stopped successfully or not started. If neither is found, it is 723 considered an error. 724 725 Raises: 726 InteractiveCommandExecutionError: If `verify` is :data:`True` and the command to stop 727 forwarding results in an error. 728 """ 729 stop_cmd_output = self.send_command("stop") 730 if verify: 731 if ( 732 "Done." not in stop_cmd_output 733 and "Packet forwarding not started" not in stop_cmd_output 734 ): 735 self._logger.debug(f"Failed to stop packet forwarding: \n{stop_cmd_output}") 736 raise InteractiveCommandExecutionError("Testpmd failed to stop packet forwarding.") 737 738 def get_devices(self) -> list[TestPmdDevice]: 739 """Get a list of device names that are known to testpmd. 740 741 Uses the device info listed in testpmd and then parses the output. 742 743 Returns: 744 A list of devices. 745 """ 746 dev_info: str = self.send_command("show device info all") 747 dev_list: list[TestPmdDevice] = [] 748 for line in dev_info.split("\n"): 749 if "device name:" in line.lower(): 750 dev_list.append(TestPmdDevice(line)) 751 return dev_list 752 753 def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool: 754 """Wait until the link status on the given port is "up". 755 756 Arguments: 757 port_id: Port to check the link status on. 758 timeout: Time to wait for the link to come up. The default value for this 759 argument may be modified using the :option:`--timeout` command-line argument 760 or the :envvar:`DTS_TIMEOUT` environment variable. 761 762 Returns: 763 Whether the link came up in time or not. 764 """ 765 time_to_stop = time.time() + timeout 766 port_info: str = "" 767 while time.time() < time_to_stop: 768 port_info = self.send_command(f"show port info {port_id}") 769 if "Link status: up" in port_info: 770 break 771 time.sleep(0.5) 772 else: 773 self._logger.error(f"The link for port {port_id} did not come up in the given timeout.") 774 return "Link status: up" in port_info 775 776 def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True): 777 """Set packet forwarding mode. 778 779 Args: 780 mode: The forwarding mode to use. 781 verify: If :data:`True` the output of the command will be scanned in an attempt to 782 verify that the forwarding mode was set to `mode` properly. 783 784 Raises: 785 InteractiveCommandExecutionError: If `verify` is :data:`True` and the forwarding mode 786 fails to update. 787 """ 788 set_fwd_output = self.send_command(f"set fwd {mode.value}") 789 if f"Set {mode.value} packet forwarding mode" not in set_fwd_output: 790 self._logger.debug(f"Failed to set fwd mode to {mode.value}:\n{set_fwd_output}") 791 raise InteractiveCommandExecutionError( 792 f"Test pmd failed to set fwd mode to {mode.value}" 793 ) 794 795 def stop_all_ports(self, verify: bool = True) -> None: 796 """Stops all the ports. 797 798 Args: 799 verify: If :data:`True`, the output of the command will be checked for a successful 800 execution. 801 802 Raises: 803 InteractiveCommandExecutionError: If `verify` is :data:`True` and the ports were not 804 stopped successfully. 805 """ 806 self._logger.debug("Stopping all the ports...") 807 output = self.send_command("port stop all") 808 if verify and not output.strip().endswith("Done"): 809 raise InteractiveCommandExecutionError("Ports were not stopped successfully.") 810 811 self.ports_started = False 812 813 def start_all_ports(self, verify: bool = True) -> None: 814 """Starts all the ports. 815 816 Args: 817 verify: If :data:`True`, the output of the command will be checked for a successful 818 execution. 819 820 Raises: 821 InteractiveCommandExecutionError: If `verify` is :data:`True` and the ports were not 822 started successfully. 823 """ 824 self._logger.debug("Starting all the ports...") 825 output = self.send_command("port start all") 826 if verify and not output.strip().endswith("Done"): 827 raise InteractiveCommandExecutionError("Ports were not started successfully.") 828 829 self.ports_started = True 830 831 @requires_stopped_ports 832 def set_ports_queues(self, number_of: int) -> None: 833 """Sets the number of queues per port. 834 835 Args: 836 number_of: The number of RX/TX queues to create per port. 837 838 Raises: 839 InternalError: If `number_of` is invalid. 840 """ 841 if number_of < 1: 842 raise InternalError("The number of queues must be positive and non-zero.") 843 844 self.send_command(f"port config all rxq {number_of}") 845 self.send_command(f"port config all txq {number_of}") 846 847 def show_port_info_all(self) -> list[TestPmdPort]: 848 """Returns the information of all the ports. 849 850 Returns: 851 list[TestPmdPort]: A list containing all the ports information as `TestPmdPort`. 852 """ 853 output = self.send_command("show port info all") 854 855 # Sample output of the "all" command looks like: 856 # 857 # <start> 858 # 859 # ********************* Infos for port 0 ********************* 860 # Key: value 861 # 862 # ********************* Infos for port 1 ********************* 863 # Key: value 864 # <end> 865 # 866 # Takes advantage of the double new line in between ports as end delimiter. But we need to 867 # artificially add a new line at the end to pick up the last port. Because commands are 868 # executed on a pseudo-terminal created by paramiko on the remote node, lines end with CRLF. 869 # Therefore we also need to take the carriage return into account. 870 iter = re.finditer(r"\*{21}.*?[\r\n]{4}", output + "\r\n", re.S) 871 self._ports = [TestPmdPort.parse(block.group(0)) for block in iter] 872 return self._ports 873 874 def show_port_info(self, port_id: int) -> TestPmdPort: 875 """Returns the given port information. 876 877 Args: 878 port_id: The port ID to gather information for. 879 880 Raises: 881 InteractiveCommandExecutionError: If `port_id` is invalid. 882 883 Returns: 884 TestPmdPort: An instance of `TestPmdPort` containing the given port's information. 885 """ 886 output = self.send_command(f"show port info {port_id}", skip_first_line=True) 887 if output.startswith("Invalid port"): 888 raise InteractiveCommandExecutionError("invalid port given") 889 890 port = TestPmdPort.parse(output) 891 self._update_port(port) 892 return port 893 894 def _update_port(self, port: TestPmdPort) -> None: 895 if self._ports: 896 self._ports = [ 897 existing_port if port.id != existing_port.id else port 898 for existing_port in self._ports 899 ] 900 901 def show_port_stats_all(self) -> list[TestPmdPortStats]: 902 """Returns the statistics of all the ports. 903 904 Returns: 905 list[TestPmdPortStats]: A list containing all the ports stats as `TestPmdPortStats`. 906 """ 907 output = self.send_command("show port stats all") 908 909 # Sample output of the "all" command looks like: 910 # 911 # ########### NIC statistics for port 0 ########### 912 # values... 913 # ################################################# 914 # 915 # ########### NIC statistics for port 1 ########### 916 # values... 917 # ################################################# 918 # 919 iter = re.finditer(r"(^ #*.+#*$[^#]+)^ #*\r$", output, re.MULTILINE) 920 return [TestPmdPortStats.parse(block.group(1)) for block in iter] 921 922 def show_port_stats(self, port_id: int) -> TestPmdPortStats: 923 """Returns the given port statistics. 924 925 Args: 926 port_id: The port ID to gather information for. 927 928 Raises: 929 InteractiveCommandExecutionError: If `port_id` is invalid. 930 931 Returns: 932 TestPmdPortStats: An instance of `TestPmdPortStats` containing the given port's stats. 933 """ 934 output = self.send_command(f"show port stats {port_id}", skip_first_line=True) 935 if output.startswith("Invalid port"): 936 raise InteractiveCommandExecutionError("invalid port given") 937 938 return TestPmdPortStats.parse(output) 939 940 @requires_stopped_ports 941 def set_port_mtu(self, port_id: int, mtu: int, verify: bool = True) -> None: 942 """Change the MTU of a port using testpmd. 943 944 Some PMDs require that the port be stopped before changing the MTU, and it does no harm to 945 stop the port before configuring in cases where it isn't required, so ports are stopped 946 prior to changing their MTU. 947 948 Args: 949 port_id: ID of the port to adjust the MTU on. 950 mtu: Desired value for the MTU to be set to. 951 verify: If `verify` is :data:`True` then the output will be scanned in an attempt to 952 verify that the mtu was properly set on the port. Defaults to :data:`True`. 953 954 Raises: 955 InteractiveCommandExecutionError: If `verify` is :data:`True` and the MTU was not 956 properly updated on the port matching `port_id`. 957 """ 958 set_mtu_output = self.send_command(f"port config mtu {port_id} {mtu}") 959 if verify and (f"MTU: {mtu}" not in self.send_command(f"show port info {port_id}")): 960 self._logger.debug( 961 f"Failed to set mtu to {mtu} on port {port_id}." f" Output was:\n{set_mtu_output}" 962 ) 963 raise InteractiveCommandExecutionError( 964 f"Test pmd failed to update mtu of port {port_id} to {mtu}" 965 ) 966 967 def set_port_mtu_all(self, mtu: int, verify: bool = True) -> None: 968 """Change the MTU of all ports using testpmd. 969 970 Runs :meth:`set_port_mtu` for every port that testpmd is aware of. 971 972 Args: 973 mtu: Desired value for the MTU to be set to. 974 verify: Whether to verify that setting the MTU on each port was successful or not. 975 Defaults to :data:`True`. 976 977 Raises: 978 InteractiveCommandExecutionError: If `verify` is :data:`True` and the MTU was not 979 properly updated on at least one port. 980 """ 981 for port in self.ports: 982 self.set_port_mtu(port.id, mtu, verify) 983 984 def _close(self) -> None: 985 """Overrides :meth:`~.interactive_shell.close`.""" 986 self.stop() 987 self.send_command("quit", "Bye...") 988 return super()._close() 989