From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 2137943421; Fri, 1 Dec 2023 19:06:24 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 117C442F5D; Fri, 1 Dec 2023 19:06:24 +0100 (CET) Received: from mail-pl1-f174.google.com (mail-pl1-f174.google.com [209.85.214.174]) by mails.dpdk.org (Postfix) with ESMTP id 3579E4025F for ; Fri, 1 Dec 2023 19:06:22 +0100 (CET) Received: by mail-pl1-f174.google.com with SMTP id d9443c01a7336-1cfc2d03b3aso7112275ad.1 for ; Fri, 01 Dec 2023 10:06:22 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1701453981; x=1702058781; darn=dpdk.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=2UzENXczSYn9YzOwPpFqtmjQAIFJBTeKNaJ0ZU1/Dmg=; b=HARnb6ixYviiXjSMWVFicibK9RKyLm3D9FFTStN29McLJbhIqdFFo3FkvNX/hSwbbu aIk5ZXePMv7F11DjRwvwxuBhmN8Xdeydx+7RQprLkuXRD3b0cvdynX962WppsZb3QcJ8 3csPYzSXJHUKsqfNooK3ioEWgP+kU3OaSTFgU= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1701453981; x=1702058781; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=2UzENXczSYn9YzOwPpFqtmjQAIFJBTeKNaJ0ZU1/Dmg=; b=hudoPEvANiAsHPLZrLqIPhJ8DRmZgrB2Jst+mfQ/ARBPg9T8jmGZ4BH9dXt4FOPwUJ m+NLf1/Gw0xiwa909FgZb4wVrqA6w7X3La/kK23SBtj3sos90kBrR7E4s7gH2FTOKpRk QRRh7CRAGnZGgsN45qsrwiaJHkGDTIitLF7LkFmFwEMDXWCPDZHsBByp8QSQcL4gc34B QDnHlE9zJl+PjPr0s1+6X0y5+TK/cCdaV4wghxUnL+W9Z9w0K0CIUks843PAmhj3jan2 sj0DCXVcb9WSKbr3JnMkMUMw4N9o3xr19f9M1vftcH7hRPYlJbyZjao8HYMsx3ZW2FRt vP0Q== X-Gm-Message-State: AOJu0YzyNxQPkKIIaGbpey4hyj4W7iZ9lR1Ei+TSbxEpGkhjfmYib2kJ 8zCb8HuDJYyuOdbf+c0REdCEgptsmWc8d8DUL7LpJQ== X-Google-Smtp-Source: AGHT+IEkLED8XeFWa5LzuHNg0x+FypTBytWJHaJ3ZORfeVZsSeQJPPLalHGmZjYlLbeHc/5KUdbZNe+1inLfeZZL2gU= X-Received: by 2002:a17:90b:3ec9:b0:286:1e60:69ae with SMTP id rm9-20020a17090b3ec900b002861e6069aemr8930155pjb.18.1701453981191; Fri, 01 Dec 2023 10:06:21 -0800 (PST) MIME-Version: 1.0 References: <20231115130959.39420-1-juraj.linkes@pantheon.tech> <20231123151344.162812-1-juraj.linkes@pantheon.tech> <20231123151344.162812-19-juraj.linkes@pantheon.tech> In-Reply-To: <20231123151344.162812-19-juraj.linkes@pantheon.tech> From: Jeremy Spewock Date: Fri, 1 Dec 2023 13:06:10 -0500 Message-ID: Subject: Re: [PATCH v8 18/21] dts: sut and tg nodes docstring update To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, probb@iol.unh.edu, paul.szczepanek@arm.com, yoan.picchi@foss.arm.com, Luca.Vizzarro@arm.com, dev@dpdk.org Content-Type: multipart/alternative; boundary="00000000000026a4c4060b76a0ba" X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --00000000000026a4c4060b76a0ba Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Thu, Nov 23, 2023 at 10:14=E2=80=AFAM Juraj Linke=C5=A1 wrote: > Format according to the Google format and PEP257, with slight > deviations. > > Signed-off-by: Juraj Linke=C5=A1 > --- > dts/framework/testbed_model/sut_node.py | 230 ++++++++++++++++-------- > dts/framework/testbed_model/tg_node.py | 42 +++-- > 2 files changed, 176 insertions(+), 96 deletions(-) > > diff --git a/dts/framework/testbed_model/sut_node.py > b/dts/framework/testbed_model/sut_node.py > index 5ce9446dba..c4acea38d1 100644 > --- a/dts/framework/testbed_model/sut_node.py > +++ b/dts/framework/testbed_model/sut_node.py > @@ -3,6 +3,14 @@ > # Copyright(c) 2023 PANTHEON.tech s.r.o. > # Copyright(c) 2023 University of New Hampshire > > +"""System under test (DPDK + hardware) node. > + > +A system under test (SUT) is the combination of DPDK > +and the hardware we're testing with DPDK (NICs, crypto and other devices= ). > +An SUT node is where this SUT runs. > +""" > I think this should just be "A SUT node" > + > + > import os > import tarfile > import time > @@ -26,6 +34,11 @@ > > > class EalParameters(object): > + """The environment abstraction layer parameters. > + > + The string representation can be created by converting the instance > to a string. > + """ > + > def __init__( > self, > lcore_list: LogicalCoreList, > @@ -35,21 +48,23 @@ def __init__( > vdevs: list[VirtualDevice], > other_eal_param: str, > ): > - """ > - Generate eal parameters character string; > - :param lcore_list: the list of logical cores to use. > - :param memory_channels: the number of memory channels to use. > - :param prefix: set file prefix string, eg: > - prefix=3D'vf' > - :param no_pci: switch of disable PCI bus eg: > - no_pci=3DTrue > - :param vdevs: virtual device list, eg: > - vdevs=3D[ > - VirtualDevice('net_ring0'), > - VirtualDevice('net_ring1') > - ] > - :param other_eal_param: user defined DPDK eal parameters, eg: > - other_eal_param=3D'--single-file-segments' > + """Initialize the parameters according to inputs. > + > + Process the parameters into the format used on the command line. > + > + Args: > + lcore_list: The list of logical cores to use. > + memory_channels: The number of memory channels to use. > + prefix: Set the file prefix string with which to start DPDK, > e.g.: ``prefix=3D'vf'``. > + no_pci: Switch to disable PCI bus e.g.: ``no_pci=3DTrue``. > + vdevs: Virtual devices, e.g.:: > + > + vdevs=3D[ > + VirtualDevice('net_ring0'), > + VirtualDevice('net_ring1') > + ] > + other_eal_param: user defined DPDK EAL parameters, e.g.: > + ``other_eal_param=3D'--single-file-segments'`` > """ > self._lcore_list =3D f"-l {lcore_list}" > self._memory_channels =3D f"-n {memory_channels}" > @@ -61,6 +76,7 @@ def __init__( > self._other_eal_param =3D other_eal_param > > def __str__(self) -> str: > + """Create the EAL string.""" > return ( > f"{self._lcore_list} " > f"{self._memory_channels} " > @@ -72,11 +88,21 @@ def __str__(self) -> str: > > > class SutNode(Node): > - """ > - A class for managing connections to the System under Test, providing > - methods that retrieve the necessary information about the node (such > as > - CPU, memory and NIC details) and configuration capabilities. > - Another key capability is building DPDK according to given build > target. > + """The system under test node. > + > + The SUT node extends :class:`Node` with DPDK specific features: > + > + * DPDK build, > + * Gathering of DPDK build info, > + * The running of DPDK apps, interactively or one-time execution, > + * DPDK apps cleanup. > + > + The :option:`--tarball` command line argument and the > :envvar:`DTS_DPDK_TARBALL` > + environment variable configure the path to the DPDK tarball > + or the git commit ID, tag ID or tree ID to test. > + > + Attributes: > + config: The SUT node configuration > """ > > config: SutNodeConfiguration > @@ -94,6 +120,11 @@ class SutNode(Node): > _path_to_devbind_script: PurePath | None > > def __init__(self, node_config: SutNodeConfiguration): > + """Extend the constructor with SUT node specifics. > + > + Args: > + node_config: The SUT node's test run configuration. > + """ > super(SutNode, self).__init__(node_config) > self._dpdk_prefix_list =3D [] > self._build_target_config =3D None > @@ -113,6 +144,12 @@ def __init__(self, node_config: SutNodeConfiguration= ): > > @property > def _remote_dpdk_dir(self) -> PurePath: > + """The remote DPDK dir. > + > + This internal property should be set after extracting the DPDK > tarball. If it's not set, > + that implies the DPDK setup step has been skipped, in which case > we can guess where > + a previous build was located. > + """ > if self.__remote_dpdk_dir is None: > self.__remote_dpdk_dir =3D self._guess_dpdk_remote_dir() > return self.__remote_dpdk_dir > @@ -123,6 +160,11 @@ def _remote_dpdk_dir(self, value: PurePath) -> None: > > @property > def remote_dpdk_build_dir(self) -> PurePath: > + """The remote DPDK build directory. > + > + This is the directory where DPDK was built. > + We assume it was built in a subdirectory of the extracted tarbal= l. > + """ > if self._build_target_config: > return self.main_session.join_remote_path( > self._remote_dpdk_dir, self._build_target_config.name > @@ -132,18 +174,21 @@ def remote_dpdk_build_dir(self) -> PurePath: > > @property > def dpdk_version(self) -> str: > + """Last built DPDK version.""" > if self._dpdk_version is None: > self._dpdk_version =3D > self.main_session.get_dpdk_version(self._remote_dpdk_dir) > return self._dpdk_version > > @property > def node_info(self) -> NodeInfo: > + """Additional node information.""" > if self._node_info is None: > self._node_info =3D self.main_session.get_node_info() > return self._node_info > > @property > def compiler_version(self) -> str: > + """The node's compiler version.""" > if self._compiler_version is None: > if self._build_target_config is not None: > self._compiler_version =3D > self.main_session.get_compiler_version( > @@ -158,6 +203,7 @@ def compiler_version(self) -> str: > > @property > def path_to_devbind_script(self) -> PurePath: > + """The path to the dpdk-devbind.py script on the node.""" > if self._path_to_devbind_script is None: > self._path_to_devbind_script =3D > self.main_session.join_remote_path( > self._remote_dpdk_dir, "usertools", "dpdk-devbind.py" > @@ -165,6 +211,11 @@ def path_to_devbind_script(self) -> PurePath: > return self._path_to_devbind_script > > def get_build_target_info(self) -> BuildTargetInfo: > + """Get additional build target information. > + > + Returns: > + The build target information, > + """ > return BuildTargetInfo( > dpdk_version=3Dself.dpdk_version, > compiler_version=3Dself.compiler_version > ) > @@ -173,8 +224,9 @@ def _guess_dpdk_remote_dir(self) -> PurePath: > return > self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) > > def _set_up_build_target(self, build_target_config: > BuildTargetConfiguration) -> None: > - """ > - Setup DPDK on the SUT node. > + """Setup DPDK on the SUT node. > + > + Additional build target setup steps on top of those in > :class:`Node`. > """ > # we want to ensure that dpdk_version and compiler_version is > reset for new > # build targets > @@ -186,16 +238,14 @@ def _set_up_build_target(self, build_target_config: > BuildTargetConfiguration) -> > self.bind_ports_to_driver() > > def _tear_down_build_target(self) -> None: > - """ > - This method exists to be optionally overwritten by derived > classes and > - is not decorated so that the derived class doesn't have to use > the decorator. > + """Bind ports to the operating system drivers. > + > + Additional build target teardown steps on top of those in > :class:`Node`. > """ > self.bind_ports_to_driver(for_dpdk=3DFalse) > > def _configure_build_target(self, build_target_config: > BuildTargetConfiguration) -> None: > - """ > - Populate common environment variables and set build target confi= g. > - """ > + """Populate common environment variables and set build target > config.""" > self._env_vars =3D {} > self._build_target_config =3D build_target_config > > self._env_vars.update(self.main_session.get_dpdk_build_env_vars(build_ta= rget_config.arch)) > @@ -207,9 +257,7 @@ def _configure_build_target(self, build_target_config= : > BuildTargetConfiguration) > > @Node.skip_setup > def _copy_dpdk_tarball(self) -> None: > - """ > - Copy to and extract DPDK tarball on the SUT node. > - """ > + """Copy to and extract DPDK tarball on the SUT node.""" > self._logger.info("Copying DPDK tarball to SUT.") > self.main_session.copy_to(SETTINGS.dpdk_tarball_path, > self._remote_tmp_dir) > > @@ -238,8 +286,9 @@ def _copy_dpdk_tarball(self) -> None: > > @Node.skip_setup > def _build_dpdk(self) -> None: > - """ > - Build DPDK. Uses the already configured target. Assumes that the > tarball has > + """Build DPDK. > + > + Uses the already configured target. Assumes that the tarball has > already been copied to and extracted on the SUT node. > """ > self.main_session.build_dpdk( > @@ -250,15 +299,19 @@ def _build_dpdk(self) -> None: > ) > > def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | > bool) -> PurePath: > - """ > - Build one or all DPDK apps. Requires DPDK to be already built on > the SUT node. > - When app_name is 'all', build all example apps. > - When app_name is any other string, tries to build that example > app. > - Return the directory path of the built app. If building all apps= , > return > - the path to the examples directory (where all apps reside). > - The meson_dpdk_args are keyword arguments > - found in meson_option.txt in root DPDK directory. Do not use -D > with them, > - for example: enable_kmods=3DTrue. > + """Build one or all DPDK apps. > + > + Requires DPDK to be already built on the SUT node. > + > + Args: > + app_name: The name of the DPDK app to build. > + When `app_name` is ``all``, build all example apps. > + meson_dpdk_args: The arguments found in ``meson_options.txt`= ` > in root DPDK directory. > + Do not use ``-D`` with them. > + > + Returns: > + The directory path of the built app. If building all apps, > return > + the path to the examples directory (where all apps reside). > """ > self.main_session.build_dpdk( > self._env_vars, > @@ -277,9 +330,7 @@ def build_dpdk_app(self, app_name: str, > **meson_dpdk_args: str | bool) -> PurePa > ) > > def kill_cleanup_dpdk_apps(self) -> None: > - """ > - Kill all dpdk applications on the SUT. Cleanup hugepages. > - """ > + """Kill all dpdk applications on the SUT, then clean up > hugepages.""" > if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(= ): > # we can use the session if it exists and responds > > self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list) > @@ -298,33 +349,34 @@ def create_eal_parameters( > vdevs: list[VirtualDevice] | None =3D None, > other_eal_param: str =3D "", > ) -> "EalParameters": > - """ > - Generate eal parameters character string; > - :param lcore_filter_specifier: a number of lcores/cores/sockets > to use > - or a list of lcore ids to use. > - The default will select one lcore for each of tw= o > cores > - on one socket, in ascending order of core ids. > - :param ascending_cores: True, use cores with the lowest numerica= l > id first > - and continue in ascending order. If False, start > with the > - highest id and continue in descending order. Thi= s > ordering > - affects which sockets to consider first as well. > - :param prefix: set file prefix string, eg: > - prefix=3D'vf' > - :param append_prefix_timestamp: if True, will append a timestamp > to > - DPDK file prefix. > - :param no_pci: switch of disable PCI bus eg: > - no_pci=3DTrue > - :param vdevs: virtual device list, eg: > - vdevs=3D[ > - VirtualDevice('net_ring0'), > - VirtualDevice('net_ring1') > - ] > - :param other_eal_param: user defined DPDK eal parameters, eg: > - other_eal_param=3D'--single-file-segments' > - :return: eal param string, eg: > - '-c 0xf -a 0000:88:00.0 > --file-prefix=3Ddpdk_1112_20190809143420'; > - """ > + """Compose the EAL parameters. > + > + Process the list of cores and the DPDK prefix and pass that alon= g > with > + the rest of the arguments. > > + Args: > + lcore_filter_specifier: A number of lcores/cores/sockets to > use > + or a list of lcore ids to use. > + The default will select one lcore for each of two cores > + on one socket, in ascending order of core ids. > + ascending_cores: Sort cores in ascending order (lowest to > highest IDs). > + If :data:`False`, sort in descending order. > + prefix: Set the file prefix string with which to start DPDK, > e.g.: ``prefix=3D'vf'``. > + append_prefix_timestamp: If :data:`True`, will append a > timestamp to DPDK file prefix. > + no_pci: Switch to disable PCI bus e.g.: ``no_pci=3DTrue``. > + vdevs: Virtual devices, e.g.:: > + > + vdevs=3D[ > + VirtualDevice('net_ring0'), > + VirtualDevice('net_ring1') > + ] > + other_eal_param: user defined DPDK EAL parameters, e.g.: > + ``other_eal_param=3D'--single-file-segments'``. > + > + Returns: > + An EAL param string, such as > + ``-c 0xf -a 0000:88:00.0 > --file-prefix=3Ddpdk_1112_20190809143420``. > + """ > lcore_list =3D > LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_core= s)) > > if append_prefix_timestamp: > @@ -348,14 +400,29 @@ def create_eal_parameters( > def run_dpdk_app( > self, app_path: PurePath, eal_args: "EalParameters", timeout: > float =3D 30 > ) -> CommandResult: > - """ > - Run DPDK application on the remote node. > + """Run DPDK application on the remote node. > + > + The application is not run interactively - the command that > starts the application > + is executed and then the call waits for it to finish execution. > + > + Args: > + app_path: The remote path to the DPDK application. > + eal_args: EAL parameters to run the DPDK application with. > + timeout: Wait at most this long in seconds for `command` > execution to complete. > + > + Returns: > + The result of the DPDK app execution. > """ > return self.main_session.send_command( > f"{app_path} {eal_args}", timeout, privileged=3DTrue, > verify=3DTrue > ) > > def configure_ipv4_forwarding(self, enable: bool) -> None: > + """Enable/disable IPv4 forwarding on the node. > + > + Args: > + enable: If :data:`True`, enable the forwarding, otherwise > disable it. > + """ > self.main_session.configure_ipv4_forwarding(enable) > > def create_interactive_shell( > @@ -365,9 +432,13 @@ def create_interactive_shell( > privileged: bool =3D False, > eal_parameters: EalParameters | str | None =3D None, > ) -> InteractiveShellType: > - """Factory method for creating a handler for an interactive > session. > + """Extend the factory for interactive session handlers. > + > + The extensions are SUT node specific: > > - Instantiate shell_cls according to the remote OS specifics. > + * The default for `eal_parameters`, > + * The interactive shell path `shell_cls.path` is prepended > with path to the remote > + DPDK build directory for DPDK apps. > > Args: > shell_cls: The class of the shell. > @@ -377,9 +448,10 @@ def create_interactive_shell( > privileged: Whether to run the shell with administrative > privileges. > eal_parameters: List of EAL parameters to use to launch the > app. If this > isn't provided or an empty string is passed, it will > default to calling > - create_eal_parameters(). > + :meth:`create_eal_parameters`. > + > Returns: > - Instance of the desired interactive application. > + An instance of the desired interactive application shell. > """ > if not eal_parameters: > eal_parameters =3D self.create_eal_parameters() > @@ -396,8 +468,8 @@ def bind_ports_to_driver(self, for_dpdk: bool =3D Tru= e) > -> None: > """Bind all ports on the SUT to a driver. > > Args: > - for_dpdk: Boolean that, when True, binds ports to > os_driver_for_dpdk > - or, when False, binds to os_driver. Defaults to True. > + for_dpdk: If :data:`True`, binds ports to os_driver_for_dpdk= . > + If :data:`False`, binds to os_driver. > """ > for port in self.ports: > driver =3D port.os_driver_for_dpdk if for_dpdk else > port.os_driver > diff --git a/dts/framework/testbed_model/tg_node.py > b/dts/framework/testbed_model/tg_node.py > index 8a8f0019f3..f269d4c585 100644 > --- a/dts/framework/testbed_model/tg_node.py > +++ b/dts/framework/testbed_model/tg_node.py > @@ -5,13 +5,8 @@ > > """Traffic generator node. > > -This is the node where the traffic generator resides. > -The distinction between a node and a traffic generator is as follows: > -A node is a host that DTS connects to. It could be a baremetal server, > -a VM or a container. > -A traffic generator is software running on the node. > -A traffic generator node is a node running a traffic generator. > -A node can be a traffic generator node as well as system under test node= . > +A traffic generator (TG) generates traffic that's sent towards the SUT > node. > +A TG node is where the TG runs. > """ > > from scapy.packet import Packet # type: ignore[import] > @@ -24,13 +19,16 @@ > > > class TGNode(Node): > - """Manage connections to a node with a traffic generator. > + """The traffic generator node. > > - Apart from basic node management capabilities, the Traffic Generator > node has > - specialized methods for handling the traffic generator running on it= . > + The TG node extends :class:`Node` with TG specific features: > > - Arguments: > - node_config: The user configuration of the traffic generator nod= e. > + * Traffic generator initialization, > + * The sending of traffic and receiving packets, > + * The sending of traffic without receiving packets. > + > + Not all traffic generators are capable of capturing traffic, which i= s > why there > + must be a way to send traffic without that. > > Attributes: > traffic_generator: The traffic generator running on the node. > @@ -39,6 +37,13 @@ class TGNode(Node): > traffic_generator: CapturingTrafficGenerator > > def __init__(self, node_config: TGNodeConfiguration): > + """Extend the constructor with TG node specifics. > + > + Initialize the traffic generator on the TG node. > + > + Args: > + node_config: The TG node's test run configuration. > + """ > super(TGNode, self).__init__(node_config) > self.traffic_generator =3D create_traffic_generator(self, > node_config.traffic_generator) > self._logger.info(f"Created node: {self.name}") > @@ -50,17 +55,17 @@ def send_packet_and_capture( > receive_port: Port, > duration: float =3D 1, > ) -> list[Packet]: > - """Send a packet, return received traffic. > + """Send `packet`, return received traffic. > > - Send a packet on the send_port and then return all traffic > captured > - on the receive_port for the given duration. Also record the > captured traffic > + Send `packet` on `send_port` and then return all traffic capture= d > + on `receive_port` for the given duration. Also record the > captured traffic > in a pcap file. > > Args: > packet: The packet to send. > send_port: The egress port on the TG node. > receive_port: The ingress port in the TG node. > - duration: Capture traffic for this amount of time after > sending the packet. > + duration: Capture traffic for this amount of time after > sending `packet`. > > Returns: > A list of received packets. May be empty if no packets are > captured. > @@ -70,6 +75,9 @@ def send_packet_and_capture( > ) > > def close(self) -> None: > - """Free all resources used by the node""" > + """Free all resources used by the node. > + > + This extends the superclass method with TG cleanup. > + """ > self.traffic_generator.close() > super(TGNode, self).close() > -- > 2.34.1 > > --00000000000026a4c4060b76a0ba Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


<= div dir=3D"ltr" class=3D"gmail_attr">On Thu, Nov 23, 2023 at 10:14=E2=80=AF= AM Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech> wrote:
Format according to the Goog= le format and PEP257, with slight
deviations.

Signed-off-by: Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech>
---
=C2=A0dts/framework/testbed_model/sut_node.py | 230 ++++++++++++++++-------= -
=C2=A0dts/framework/testbed_model/tg_node.py=C2=A0 |=C2=A0 42 +++--
=C2=A02 files changed, 176 insertions(+), 96 deletions(-)

diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbe= d_model/sut_node.py
index 5ce9446dba..c4acea38d1 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -3,6 +3,14 @@
=C2=A0# Copyright(c) 2023 PANTHEON.tech s.r.o.
=C2=A0# Copyright(c) 2023 University of New Hampshire

+"""System under test (DPDK + hardware) node.
+
+A system under test (SUT) is the combination of DPDK
+and the hardware we're testing with DPDK (NICs, crypto and other devic= es).
+An SUT node is where this SUT runs.
+"""

I think this should just b= e "A SUT node"
=C2=A0
+
+
=C2=A0import os
=C2=A0import tarfile
=C2=A0import time
@@ -26,6 +34,11 @@


=C2=A0class EalParameters(object):
+=C2=A0 =C2=A0 """The environment abstraction layer paramete= rs.
+
+=C2=A0 =C2=A0 The string representation can be created by converting the i= nstance to a string.
+=C2=A0 =C2=A0 """
+
=C2=A0 =C2=A0 =C2=A0def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcore_list: LogicalCoreList,
@@ -35,21 +48,23 @@ def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0vdevs: list[VirtualDevice],
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0other_eal_param: str,
=C2=A0 =C2=A0 =C2=A0):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param lcore_list: the list of logical cores t= o use.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param memory_channels: the number of memory c= hannels to use.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 prefix=3D'vf'
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus eg: -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 no_pci=3DTrue
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 vdevs=3D[
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring0'),
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring1')
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 ]
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK eal = parameters, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 other_eal_param=3D'--single-file-segments'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Initialize the parameters ac= cording to inputs.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Process the parameters into the format used on= the command line.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcore_list: The list of logical = cores to use.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 memory_channels: The number of m= emory channels to use.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: Set the file prefix stri= ng with which to start DPDK, e.g.: ``prefix=3D'vf'``.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: Switch to disable PCI bu= s e.g.: ``no_pci=3DTrue``.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: Virtual devices, e.g.:: +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs=3D[
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Virt= ualDevice('net_ring0'),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Virt= ualDevice('net_ring1')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: user defined DP= DK EAL parameters, e.g.:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ``other_eal_param= =3D'--single-file-segments'``
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._lcore_list =3D f"-l {lcore_lis= t}"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._memory_channels =3D f"-n {memo= ry_channels}"
@@ -61,6 +76,7 @@ def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._other_eal_param =3D other_eal_param=

=C2=A0 =C2=A0 =C2=A0def __str__(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Create the EAL string."= ""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return (
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"{self._lcore_list} &= quot;
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"{self._memory_channe= ls} "
@@ -72,11 +88,21 @@ def __str__(self) -> str:


=C2=A0class SutNode(Node):
-=C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 A class for managing connections to the System under Test, p= roviding
-=C2=A0 =C2=A0 methods that retrieve the necessary information about the no= de (such as
-=C2=A0 =C2=A0 CPU, memory and NIC details) and configuration capabilities.=
-=C2=A0 =C2=A0 Another key capability is building DPDK according to given b= uild target.
+=C2=A0 =C2=A0 """The system under test node.
+
+=C2=A0 =C2=A0 The SUT node extends :class:`Node` with DPDK specific featur= es:
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * DPDK build,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Gathering of DPDK build info,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * The running of DPDK apps, interactively or o= ne-time execution,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * DPDK apps cleanup.
+
+=C2=A0 =C2=A0 The :option:`--tarball` command line argument and the :envva= r:`DTS_DPDK_TARBALL`
+=C2=A0 =C2=A0 environment variable configure the path to the DPDK tarball<= br> +=C2=A0 =C2=A0 or the git commit ID, tag ID or tree ID to test.
+
+=C2=A0 =C2=A0 Attributes:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 config: The SUT node configuration
=C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0config: SutNodeConfiguration
@@ -94,6 +120,11 @@ class SutNode(Node):
=C2=A0 =C2=A0 =C2=A0_path_to_devbind_script: PurePath | None

=C2=A0 =C2=A0 =C2=A0def __init__(self, node_config: SutNodeConfiguration):<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Extend the constructor with = SUT node specifics.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The SUT node's = test run configuration.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(SutNode, self).__init__(node_config= )
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._dpdk_prefix_list =3D []
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D None
@@ -113,6 +144,12 @@ def __init__(self, node_config: SutNodeConfiguration):=

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def _remote_dpdk_dir(self) -> PurePath:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """The remote DPDK dir.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This internal property should be set after ext= racting the DPDK tarball. If it's not set,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 that implies the DPDK setup step has been skip= ped, in which case we can guess where
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 a previous build was located.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self.__remote_dpdk_dir is None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.__remote_dpdk_dir =3D = self._guess_dpdk_remote_dir()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.__remote_dpdk_dir
@@ -123,6 +160,11 @@ def _remote_dpdk_dir(self, value: PurePath) -> None= :

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def remote_dpdk_build_dir(self) -> PurePath:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """The remote DPDK build direct= ory.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This is the directory where DPDK was built. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 We assume it was built in a subdirectory of th= e extracted tarball.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._build_target_config:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.jo= in_remote_path(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remote_= dpdk_dir, self._build_target_config.name
@@ -132,18 +174,21 @@ def remote_dpdk_build_dir(self) -> PurePath:

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def dpdk_version(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Last built DPDK version.&quo= t;""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._dpdk_version is None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._dpdk_version =3D self= .main_session.get_dpdk_version(self._remote_dpdk_dir)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self._dpdk_version

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def node_info(self) -> NodeInfo:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Additional node information.= """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._node_info is None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._node_info =3D self.ma= in_session.get_node_info()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self._node_info

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def compiler_version(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """The node's compiler vers= ion."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._compiler_version is None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._build_target_confi= g is not None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._compile= r_version =3D self.main_session.get_compiler_version(
@@ -158,6 +203,7 @@ def compiler_version(self) -> str:

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def path_to_devbind_script(self) -> PurePath:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """The path to the dpdk-devbind= .py script on the node."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._path_to_devbind_script is None:<= br> =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._path_to_devbind_scrip= t =3D self.main_session.join_remote_path(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remote_= dpdk_dir, "usertools", "dpdk-devbind.py"
@@ -165,6 +211,11 @@ def path_to_devbind_script(self) -> PurePath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self._path_to_devbind_script

=C2=A0 =C2=A0 =C2=A0def get_build_target_info(self) -> BuildTargetInfo:<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Get additional build target = information.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The build target information, +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return BuildTargetInfo(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0dpdk_version=3Dself.dpdk_ve= rsion, compiler_version=3Dself.compiler_version
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
@@ -173,8 +224,9 @@ def _guess_dpdk_remote_dir(self) -> PurePath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.guess_dpdk_remot= e_dir(self._remote_tmp_dir)

=C2=A0 =C2=A0 =C2=A0def _set_up_build_target(self, build_target_config: Bui= ldTargetConfiguration) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Setup DPDK on the SUT node.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Setup DPDK on the SUT node.<= br> +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Additional build target setup steps on top of = those in :class:`Node`.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# we want to ensure that dpdk_version and= compiler_version is reset for new
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# build targets
@@ -186,16 +238,14 @@ def _set_up_build_target(self, build_target_config: B= uildTargetConfiguration) ->
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.bind_ports_to_driver()

=C2=A0 =C2=A0 =C2=A0def _tear_down_build_target(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 This method exists to be optionally overwritte= n by derived classes and
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 is not decorated so that the derived class doe= sn't have to use the decorator.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Bind ports to the operating = system drivers.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Additional build target teardown steps on top = of those in :class:`Node`.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.bind_ports_to_driver(for_dpdk=3DFals= e)

=C2=A0 =C2=A0 =C2=A0def _configure_build_target(self, build_target_config: = BuildTargetConfiguration) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Populate common environment variables and set = build target config.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Populate common environment = variables and set build target config."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars =3D {}
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D build_targe= t_config
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars.update(self.main_session.g= et_dpdk_build_env_vars(build_target_config.arch))
@@ -207,9 +257,7 @@ def _configure_build_target(self, build_target_config: = BuildTargetConfiguration)

=C2=A0 =C2=A0 =C2=A0@Node.skip_setup
=C2=A0 =C2=A0 =C2=A0def _copy_dpdk_tarball(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy to and extract DPDK tarball on the SUT no= de.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy to and extract DPDK tar= ball on the SUT node."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info("Copying DPDK tarbal= l to SUT.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session.copy_to(SETTINGS.dpdk_t= arball_path, self._remote_tmp_dir)

@@ -238,8 +286,9 @@ def _copy_dpdk_tarball(self) -> None:

=C2=A0 =C2=A0 =C2=A0@Node.skip_setup
=C2=A0 =C2=A0 =C2=A0def _build_dpdk(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Build DPDK. Uses the already configured target= . Assumes that the tarball has
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Build DPDK.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Uses the already configured target. Assumes th= at the tarball has
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0already been copied to and extracted on t= he SUT node.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session.build_dpdk(
@@ -250,15 +299,19 @@ def _build_dpdk(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

=C2=A0 =C2=A0 =C2=A0def build_dpdk_app(self, app_name: str, **meson_dpdk_ar= gs: str | bool) -> PurePath:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Build one or all DPDK apps. Requires DPDK to b= e already built on the SUT node.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 When app_name is 'all', build all exam= ple apps.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 When app_name is any other string, tries to bu= ild that example app.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Return the directory path of the built app. If= building all apps, return
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 the path to the examples directory (where all = apps reside).
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 The meson_dpdk_args are keyword arguments
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 found in meson_option.txt in root DPDK directo= ry. Do not use -D with them,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 for example: enable_kmods=3DTrue.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Build one or all DPDK apps.<= br> +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Requires DPDK to be already built on the SUT n= ode.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 app_name: The name of the DPDK a= pp to build.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 When `app_name` is= ``all``, build all example apps.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 meson_dpdk_args: The arguments f= ound in ``meson_options.txt`` in root DPDK directory.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Do not use ``-D`` = with them.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The directory path of the built = app. If building all apps, return
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 the path to the examples directo= ry (where all apps reside).
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session.build_dpdk(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars,
@@ -277,9 +330,7 @@ def build_dpdk_app(self, app_name: str, **meson_dpdk_ar= gs: str | bool) -> PurePa
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

=C2=A0 =C2=A0 =C2=A0def kill_cleanup_dpdk_apps(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Kill all dpdk applications on the SUT. Cleanup= hugepages.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Kill all dpdk applications o= n the SUT, then clean up hugepages."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._dpdk_kill_session and self._dpdk= _kill_session.is_alive():
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# we can use the session if= it exists and responds
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._dpdk_kill_session.kil= l_cleanup_dpdk_apps(self._dpdk_prefix_list)
@@ -298,33 +349,34 @@ def create_eal_parameters(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0vdevs: list[VirtualDevice] | None =3D Non= e,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0other_eal_param: str =3D "", =C2=A0 =C2=A0 =C2=A0) -> "EalParameters":
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param lcore_filter_specifier: a number of lco= res/cores/sockets to use
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 or a list of lcore ids to use.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 The default will select one lcore for each of two cores
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 on one socket, in ascending order of core ids.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param ascending_cores: True, use cores with t= he lowest numerical id first
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 and continue in ascending order. If False, start with the
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 highest id and continue in descending order. This ordering
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 affects which sockets to consider first as well.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 prefix=3D'vf'
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param append_prefix_timestamp: if True, will = append a timestamp to
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 DPDK file prefix.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus eg: -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 no_pci=3DTrue
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 vdevs=3D[
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring0'),
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring1')
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 ]
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK eal = parameters, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 other_eal_param=3D'--single-file-segments'
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: eal param string, eg:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 '-c 0xf -a 000= 0:88:00.0 --file-prefix=3Ddpdk_1112_20190809143420';
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Compose the EAL parameters.<= br> +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Process the list of cores and the DPDK prefix = and pass that along with
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 the rest of the arguments.

+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcore_filter_specifier: A number= of lcores/cores/sockets to use
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 or a list of lcore= ids to use.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The default will s= elect one lcore for each of two cores
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 on one socket, in = ascending order of core ids.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending_cores: Sort cores in a= scending order (lowest to highest IDs).
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If :data:`False`, = sort in descending order.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: Set the file prefix stri= ng with which to start DPDK, e.g.: ``prefix=3D'vf'``.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 append_prefix_timestamp: If :dat= a:`True`, will append a timestamp to DPDK file prefix.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: Switch to disable PCI bu= s e.g.: ``no_pci=3DTrue``.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: Virtual devices, e.g.:: +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs=3D[
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Virt= ualDevice('net_ring0'),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Virt= ualDevice('net_ring1')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: user defined DP= DK EAL parameters, e.g.:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ``other_eal_param= =3D'--single-file-segments'``.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 An EAL param string, such as
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ``-c 0xf -a 0000:88:00.0 --file-= prefix=3Ddpdk_1112_20190809143420``.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcore_list =3D LogicalCoreList(self.filte= r_lcores(lcore_filter_specifier, ascending_cores))

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if append_prefix_timestamp:
@@ -348,14 +400,29 @@ def create_eal_parameters(
=C2=A0 =C2=A0 =C2=A0def run_dpdk_app(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self, app_path: PurePath, eal_args: "= ;EalParameters", timeout: float =3D 30
=C2=A0 =C2=A0 =C2=A0) -> CommandResult:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Run DPDK application on the remote node.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Run DPDK application on the = remote node.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The application is not run interactively - the= command that starts the application
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 is executed and then the call waits for it to = finish execution.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 app_path: The remote path to the= DPDK application.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_args: EAL parameters to run = the DPDK application with.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Wait at most this long = in seconds for `command` execution to complete.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The result of the DPDK app execu= tion.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.send_command( =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"{app_path} {eal_args= }", timeout, privileged=3DTrue, verify=3DTrue
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

=C2=A0 =C2=A0 =C2=A0def configure_ipv4_forwarding(self, enable: bool) ->= None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Enable/disable IPv4 forwardi= ng on the node.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 enable: If :data:`True`, enable = the forwarding, otherwise disable it.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session.configure_ipv4_forwardi= ng(enable)

=C2=A0 =C2=A0 =C2=A0def create_interactive_shell(
@@ -365,9 +432,13 @@ def create_interactive_shell(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0privileged: bool =3D False,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0eal_parameters: EalParameters | str | Non= e =3D None,
=C2=A0 =C2=A0 =C2=A0) -> InteractiveShellType:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Factory method for creating = a handler for an interactive session.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Extend the factory for inter= active session handlers.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The extensions are SUT node specific:

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Instantiate shell_cls according to the remote = OS specifics.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 * The default for `eal_parameter= s`,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 * The interactive shell path `sh= ell_cls.path` is prepended with path to the remote
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 DPDK build directory for = DPDK apps.

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Args:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0shell_cls: The class of the= shell.
@@ -377,9 +448,10 @@ def create_interactive_shell(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0privileged: Whether to run = the shell with administrative privileges.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0eal_parameters: List of EAL= parameters to use to launch the app. If this
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0isn't pro= vided or an empty string is passed, it will default to calling
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 create_eal_paramet= ers().
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 :meth:`create_eal_= parameters`.
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Returns:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Instance of the desired interact= ive application.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 An instance of the desired inter= active application shell.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if not eal_parameters:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0eal_parameters =3D self.cre= ate_eal_parameters()
@@ -396,8 +468,8 @@ def bind_ports_to_driver(self, for_dpdk: bool =3D True)= -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""Bind all ports on the S= UT to a driver.

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Args:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for_dpdk: Boolean that, when Tru= e, binds ports to os_driver_for_dpdk
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 or, when False, binds to os_driv= er. Defaults to True.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for_dpdk: If :data:`True`, binds= ports to os_driver_for_dpdk.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If :data:`False`, = binds to os_driver.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for port in self.ports:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0driver =3D port.os_driver_f= or_dpdk if for_dpdk else port.os_driver
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed= _model/tg_node.py
index 8a8f0019f3..f269d4c585 100644
--- a/dts/framework/testbed_model/tg_node.py
+++ b/dts/framework/testbed_model/tg_node.py
@@ -5,13 +5,8 @@

=C2=A0"""Traffic generator node.

-This is the node where the traffic generator resides.
-The distinction between a node and a traffic generator is as follows:
-A node is a host that DTS connects to. It could be a baremetal server,
-a VM or a container.
-A traffic generator is software running on the node.
-A traffic generator node is a node running a traffic generator.
-A node can be a traffic generator node as well as system under test node.<= br> +A traffic generator (TG) generates traffic that's sent towards the SUT= node.
+A TG node is where the TG runs.
=C2=A0"""

=C2=A0from scapy.packet import Packet=C2=A0 # type: ignore[import]
@@ -24,13 +19,16 @@


=C2=A0class TGNode(Node):
-=C2=A0 =C2=A0 """Manage connections to a node with a traffi= c generator.
+=C2=A0 =C2=A0 """The traffic generator node.

-=C2=A0 =C2=A0 Apart from basic node management capabilities, the Traffic G= enerator node has
-=C2=A0 =C2=A0 specialized methods for handling the traffic generator runni= ng on it.
+=C2=A0 =C2=A0 The TG node extends :class:`Node` with TG specific features:=

-=C2=A0 =C2=A0 Arguments:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The user configuration of the tra= ffic generator node.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Traffic generator initialization,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * The sending of traffic and receiving packets= ,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * The sending of traffic without receiving pac= kets.
+
+=C2=A0 =C2=A0 Not all traffic generators are capable of capturing traffic,= which is why there
+=C2=A0 =C2=A0 must be a way to send traffic without that.

=C2=A0 =C2=A0 =C2=A0Attributes:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0traffic_generator: The traffic generator = running on the node.
@@ -39,6 +37,13 @@ class TGNode(Node):
=C2=A0 =C2=A0 =C2=A0traffic_generator: CapturingTrafficGenerator

=C2=A0 =C2=A0 =C2=A0def __init__(self, node_config: TGNodeConfiguration): +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Extend the constructor with = TG node specifics.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initialize the traffic generator on the TG nod= e.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The TG node's t= est run configuration.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(TGNode, self).__init__(node_config)=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.traffic_generator =3D create_traffic= _generator(self, node_config.traffic_generator)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info(f"Created node: {self.name}")
@@ -50,17 +55,17 @@ def send_packet_and_capture(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0receive_port: Port,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0duration: float =3D 1,
=C2=A0 =C2=A0 =C2=A0) -> list[Packet]:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send a packet, return receiv= ed traffic.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send `packet`, return receiv= ed traffic.

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a packet on the send_port and then return= all traffic captured
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the receive_port for the given duration. Al= so record the captured traffic
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send `packet` on `send_port` and then return a= ll traffic captured
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 on `receive_port` for the given duration. Also= record the captured traffic
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0in a pcap file.

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Args:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0packet: The packet to send.=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0send_port: The egress port = on the TG node.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0receive_port: The ingress p= ort in the TG node.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 duration: Capture traffic for th= is amount of time after sending the packet.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 duration: Capture traffic for th= is amount of time after sending `packet`.

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Returns:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 A list of received packets= . May be empty if no packets are captured.
@@ -70,6 +75,9 @@ def send_packet_and_capture(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

=C2=A0 =C2=A0 =C2=A0def close(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Free all resources used by t= he node"""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Free all resources used by t= he node.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This extends the superclass method with TG cle= anup.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.traffic_generator.close()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(TGNode, self).close()
--
2.34.1

--00000000000026a4c4060b76a0ba--