xref: /dpdk/dts/framework/remote_session/testpmd_shell.py (revision 1e472b5746aeb6189fa254ab82ce4cd27999f868)
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