<div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif"><br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Thu, Nov 23, 2023 at 10:14 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Format according to the Google format and PEP257, with slight<br>
deviations.<br>
<br>
Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech><br>
---<br>
 dts/framework/testbed_model/sut_node.py | 230 ++++++++++++++++--------<br>
 dts/framework/testbed_model/tg_node.py  |  42 +++--<br>
 2 files changed, 176 insertions(+), 96 deletions(-)<br>
<br>
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py<br>
index 5ce9446dba..c4acea38d1 100644<br>
--- a/dts/framework/testbed_model/sut_node.py<br>
+++ b/dts/framework/testbed_model/sut_node.py<br>
@@ -3,6 +3,14 @@<br>
 # Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
 # Copyright(c) 2023 University of New Hampshire<br>
<br>
+"""System under test (DPDK + hardware) node.<br>
+<br>
+A system under test (SUT) is the combination of DPDK<br>
+and the hardware we're testing with DPDK (NICs, crypto and other devices).<br>
+An SUT node is where this SUT runs.<br>
+"""<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think this should just be "A SUT node"<br></div></div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+<br>
+<br>
 import os<br>
 import tarfile<br>
 import time<br>
@@ -26,6 +34,11 @@<br>
<br>
<br>
 class EalParameters(object):<br>
+    """The environment abstraction layer parameters.<br>
+<br>
+    The string representation can be created by converting the instance to a string.<br>
+    """<br>
+<br>
     def __init__(<br>
         self,<br>
         lcore_list: LogicalCoreList,<br>
@@ -35,21 +48,23 @@ def __init__(<br>
         vdevs: list[VirtualDevice],<br>
         other_eal_param: str,<br>
     ):<br>
-        """<br>
-        Generate eal parameters character string;<br>
-        :param lcore_list: the list of logical cores to use.<br>
-        :param memory_channels: the number of memory channels to use.<br>
-        :param prefix: set file prefix string, eg:<br>
-                        prefix='vf'<br>
-        :param no_pci: switch of disable PCI bus eg:<br>
-                        no_pci=True<br>
-        :param vdevs: virtual device list, eg:<br>
-                        vdevs=[<br>
-                            VirtualDevice('net_ring0'),<br>
-                            VirtualDevice('net_ring1')<br>
-                        ]<br>
-        :param other_eal_param: user defined DPDK eal parameters, eg:<br>
-                        other_eal_param='--single-file-segments'<br>
+        """Initialize the parameters according to inputs.<br>
+<br>
+        Process the parameters into the format used on the command line.<br>
+<br>
+        Args:<br>
+            lcore_list: The list of logical cores to use.<br>
+            memory_channels: The number of memory channels to use.<br>
+            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.<br>
+            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.<br>
+            vdevs: Virtual devices, e.g.::<br>
+<br>
+                vdevs=[<br>
+                    VirtualDevice('net_ring0'),<br>
+                    VirtualDevice('net_ring1')<br>
+                ]<br>
+            other_eal_param: user defined DPDK EAL parameters, e.g.:<br>
+                ``other_eal_param='--single-file-segments'``<br>
         """<br>
         self._lcore_list = f"-l {lcore_list}"<br>
         self._memory_channels = f"-n {memory_channels}"<br>
@@ -61,6 +76,7 @@ def __init__(<br>
         self._other_eal_param = other_eal_param<br>
<br>
     def __str__(self) -> str:<br>
+        """Create the EAL string."""<br>
         return (<br>
             f"{self._lcore_list} "<br>
             f"{self._memory_channels} "<br>
@@ -72,11 +88,21 @@ def __str__(self) -> str:<br>
<br>
<br>
 class SutNode(Node):<br>
-    """<br>
-    A class for managing connections to the System under Test, providing<br>
-    methods that retrieve the necessary information about the node (such as<br>
-    CPU, memory and NIC details) and configuration capabilities.<br>
-    Another key capability is building DPDK according to given build target.<br>
+    """The system under test node.<br>
+<br>
+    The SUT node extends :class:`Node` with DPDK specific features:<br>
+<br>
+        * DPDK build,<br>
+        * Gathering of DPDK build info,<br>
+        * The running of DPDK apps, interactively or one-time execution,<br>
+        * DPDK apps cleanup.<br>
+<br>
+    The :option:`--tarball` command line argument and the :envvar:`DTS_DPDK_TARBALL`<br>
+    environment variable configure the path to the DPDK tarball<br>
+    or the git commit ID, tag ID or tree ID to test.<br>
+<br>
+    Attributes:<br>
+        config: The SUT node configuration<br>
     """<br>
<br>
     config: SutNodeConfiguration<br>
@@ -94,6 +120,11 @@ class SutNode(Node):<br>
     _path_to_devbind_script: PurePath | None<br>
<br>
     def __init__(self, node_config: SutNodeConfiguration):<br>
+        """Extend the constructor with SUT node specifics.<br>
+<br>
+        Args:<br>
+            node_config: The SUT node's test run configuration.<br>
+        """<br>
         super(SutNode, self).__init__(node_config)<br>
         self._dpdk_prefix_list = []<br>
         self._build_target_config = None<br>
@@ -113,6 +144,12 @@ def __init__(self, node_config: SutNodeConfiguration):<br>
<br>
     @property<br>
     def _remote_dpdk_dir(self) -> PurePath:<br>
+        """The remote DPDK dir.<br>
+<br>
+        This internal property should be set after extracting the DPDK tarball. If it's not set,<br>
+        that implies the DPDK setup step has been skipped, in which case we can guess where<br>
+        a previous build was located.<br>
+        """<br>
         if self.__remote_dpdk_dir is None:<br>
             self.__remote_dpdk_dir = self._guess_dpdk_remote_dir()<br>
         return self.__remote_dpdk_dir<br>
@@ -123,6 +160,11 @@ def _remote_dpdk_dir(self, value: PurePath) -> None:<br>
<br>
     @property<br>
     def remote_dpdk_build_dir(self) -> PurePath:<br>
+        """The remote DPDK build directory.<br>
+<br>
+        This is the directory where DPDK was built.<br>
+        We assume it was built in a subdirectory of the extracted tarball.<br>
+        """<br>
         if self._build_target_config:<br>
             return self.main_session.join_remote_path(<br>
                 self._remote_dpdk_dir, self._<a href="http://build_target_config.name" rel="noreferrer" target="_blank">build_target_config.name</a><br>
@@ -132,18 +174,21 @@ def remote_dpdk_build_dir(self) -> PurePath:<br>
<br>
     @property<br>
     def dpdk_version(self) -> str:<br>
+        """Last built DPDK version."""<br>
         if self._dpdk_version is None:<br>
             self._dpdk_version = self.main_session.get_dpdk_version(self._remote_dpdk_dir)<br>
         return self._dpdk_version<br>
<br>
     @property<br>
     def node_info(self) -> NodeInfo:<br>
+        """Additional node information."""<br>
         if self._node_info is None:<br>
             self._node_info = self.main_session.get_node_info()<br>
         return self._node_info<br>
<br>
     @property<br>
     def compiler_version(self) -> str:<br>
+        """The node's compiler version."""<br>
         if self._compiler_version is None:<br>
             if self._build_target_config is not None:<br>
                 self._compiler_version = self.main_session.get_compiler_version(<br>
@@ -158,6 +203,7 @@ def compiler_version(self) -> str:<br>
<br>
     @property<br>
     def path_to_devbind_script(self) -> PurePath:<br>
+        """The path to the dpdk-devbind.py script on the node."""<br>
         if self._path_to_devbind_script is None:<br>
             self._path_to_devbind_script = self.main_session.join_remote_path(<br>
                 self._remote_dpdk_dir, "usertools", "dpdk-devbind.py"<br>
@@ -165,6 +211,11 @@ def path_to_devbind_script(self) -> PurePath:<br>
         return self._path_to_devbind_script<br>
<br>
     def get_build_target_info(self) -> BuildTargetInfo:<br>
+        """Get additional build target information.<br>
+<br>
+        Returns:<br>
+            The build target information,<br>
+        """<br>
         return BuildTargetInfo(<br>
             dpdk_version=self.dpdk_version, compiler_version=self.compiler_version<br>
         )<br>
@@ -173,8 +224,9 @@ def _guess_dpdk_remote_dir(self) -> PurePath:<br>
         return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir)<br>
<br>
     def _set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None:<br>
-        """<br>
-        Setup DPDK on the SUT node.<br>
+        """Setup DPDK on the SUT node.<br>
+<br>
+        Additional build target setup steps on top of those in :class:`Node`.<br>
         """<br>
         # we want to ensure that dpdk_version and compiler_version is reset for new<br>
         # build targets<br>
@@ -186,16 +238,14 @@ def _set_up_build_target(self, build_target_config: BuildTargetConfiguration) -><br>
         self.bind_ports_to_driver()<br>
<br>
     def _tear_down_build_target(self) -> None:<br>
-        """<br>
-        This method exists to be optionally overwritten by derived classes and<br>
-        is not decorated so that the derived class doesn't have to use the decorator.<br>
+        """Bind ports to the operating system drivers.<br>
+<br>
+        Additional build target teardown steps on top of those in :class:`Node`.<br>
         """<br>
         self.bind_ports_to_driver(for_dpdk=False)<br>
<br>
     def _configure_build_target(self, build_target_config: BuildTargetConfiguration) -> None:<br>
-        """<br>
-        Populate common environment variables and set build target config.<br>
-        """<br>
+        """Populate common environment variables and set build target config."""<br>
         self._env_vars = {}<br>
         self._build_target_config = build_target_config<br>
         self._env_vars.update(self.main_session.get_dpdk_build_env_vars(build_target_config.arch))<br>
@@ -207,9 +257,7 @@ def _configure_build_target(self, build_target_config: BuildTargetConfiguration)<br>
<br>
     @Node.skip_setup<br>
     def _copy_dpdk_tarball(self) -> None:<br>
-        """<br>
-        Copy to and extract DPDK tarball on the SUT node.<br>
-        """<br>
+        """Copy to and extract DPDK tarball on the SUT node."""<br>
         self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>("Copying DPDK tarball to SUT.")<br>
         self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir)<br>
<br>
@@ -238,8 +286,9 @@ def _copy_dpdk_tarball(self) -> None:<br>
<br>
     @Node.skip_setup<br>
     def _build_dpdk(self) -> None:<br>
-        """<br>
-        Build DPDK. Uses the already configured target. Assumes that the tarball has<br>
+        """Build DPDK.<br>
+<br>
+        Uses the already configured target. Assumes that the tarball has<br>
         already been copied to and extracted on the SUT node.<br>
         """<br>
         self.main_session.build_dpdk(<br>
@@ -250,15 +299,19 @@ def _build_dpdk(self) -> None:<br>
         )<br>
<br>
     def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath:<br>
-        """<br>
-        Build one or all DPDK apps. Requires DPDK to be already built on the SUT node.<br>
-        When app_name is 'all', build all example apps.<br>
-        When app_name is any other string, tries to build that example app.<br>
-        Return the directory path of the built app. If building all apps, return<br>
-        the path to the examples directory (where all apps reside).<br>
-        The meson_dpdk_args are keyword arguments<br>
-        found in meson_option.txt in root DPDK directory. Do not use -D with them,<br>
-        for example: enable_kmods=True.<br>
+        """Build one or all DPDK apps.<br>
+<br>
+        Requires DPDK to be already built on the SUT node.<br>
+<br>
+        Args:<br>
+            app_name: The name of the DPDK app to build.<br>
+                When `app_name` is ``all``, build all example apps.<br>
+            meson_dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory.<br>
+                Do not use ``-D`` with them.<br>
+<br>
+        Returns:<br>
+            The directory path of the built app. If building all apps, return<br>
+            the path to the examples directory (where all apps reside).<br>
         """<br>
         self.main_session.build_dpdk(<br>
             self._env_vars,<br>
@@ -277,9 +330,7 @@ def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePa<br>
         )<br>
<br>
     def kill_cleanup_dpdk_apps(self) -> None:<br>
-        """<br>
-        Kill all dpdk applications on the SUT. Cleanup hugepages.<br>
-        """<br>
+        """Kill all dpdk applications on the SUT, then clean up hugepages."""<br>
         if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():<br>
             # we can use the session if it exists and responds<br>
             self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)<br>
@@ -298,33 +349,34 @@ def create_eal_parameters(<br>
         vdevs: list[VirtualDevice] | None = None,<br>
         other_eal_param: str = "",<br>
     ) -> "EalParameters":<br>
-        """<br>
-        Generate eal parameters character string;<br>
-        :param lcore_filter_specifier: a number of lcores/cores/sockets to use<br>
-                        or a list of lcore ids to use.<br>
-                        The default will select one lcore for each of two cores<br>
-                        on one socket, in ascending order of core ids.<br>
-        :param ascending_cores: True, use cores with the lowest numerical id first<br>
-                        and continue in ascending order. If False, start with the<br>
-                        highest id and continue in descending order. This ordering<br>
-                        affects which sockets to consider first as well.<br>
-        :param prefix: set file prefix string, eg:<br>
-                        prefix='vf'<br>
-        :param append_prefix_timestamp: if True, will append a timestamp to<br>
-                        DPDK file prefix.<br>
-        :param no_pci: switch of disable PCI bus eg:<br>
-                        no_pci=True<br>
-        :param vdevs: virtual device list, eg:<br>
-                        vdevs=[<br>
-                            VirtualDevice('net_ring0'),<br>
-                            VirtualDevice('net_ring1')<br>
-                        ]<br>
-        :param other_eal_param: user defined DPDK eal parameters, eg:<br>
-                        other_eal_param='--single-file-segments'<br>
-        :return: eal param string, eg:<br>
-                '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420';<br>
-        """<br>
+        """Compose the EAL parameters.<br>
+<br>
+        Process the list of cores and the DPDK prefix and pass that along with<br>
+        the rest of the arguments.<br>
<br>
+        Args:<br>
+            lcore_filter_specifier: A number of lcores/cores/sockets to use<br>
+                or a list of lcore ids to use.<br>
+                The default will select one lcore for each of two cores<br>
+                on one socket, in ascending order of core ids.<br>
+            ascending_cores: Sort cores in ascending order (lowest to highest IDs).<br>
+                If :data:`False`, sort in descending order.<br>
+            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.<br>
+            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.<br>
+            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.<br>
+            vdevs: Virtual devices, e.g.::<br>
+<br>
+                vdevs=[<br>
+                    VirtualDevice('net_ring0'),<br>
+                    VirtualDevice('net_ring1')<br>
+                ]<br>
+            other_eal_param: user defined DPDK EAL parameters, e.g.:<br>
+                ``other_eal_param='--single-file-segments'``.<br>
+<br>
+        Returns:<br>
+            An EAL param string, such as<br>
+            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.<br>
+        """<br>
         lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))<br>
<br>
         if append_prefix_timestamp:<br>
@@ -348,14 +400,29 @@ def create_eal_parameters(<br>
     def run_dpdk_app(<br>
         self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30<br>
     ) -> CommandResult:<br>
-        """<br>
-        Run DPDK application on the remote node.<br>
+        """Run DPDK application on the remote node.<br>
+<br>
+        The application is not run interactively - the command that starts the application<br>
+        is executed and then the call waits for it to finish execution.<br>
+<br>
+        Args:<br>
+            app_path: The remote path to the DPDK application.<br>
+            eal_args: EAL parameters to run the DPDK application with.<br>
+            timeout: Wait at most this long in seconds for `command` execution to complete.<br>
+<br>
+        Returns:<br>
+            The result of the DPDK app execution.<br>
         """<br>
         return self.main_session.send_command(<br>
             f"{app_path} {eal_args}", timeout, privileged=True, verify=True<br>
         )<br>
<br>
     def configure_ipv4_forwarding(self, enable: bool) -> None:<br>
+        """Enable/disable IPv4 forwarding on the node.<br>
+<br>
+        Args:<br>
+            enable: If :data:`True`, enable the forwarding, otherwise disable it.<br>
+        """<br>
         self.main_session.configure_ipv4_forwarding(enable)<br>
<br>
     def create_interactive_shell(<br>
@@ -365,9 +432,13 @@ def create_interactive_shell(<br>
         privileged: bool = False,<br>
         eal_parameters: EalParameters | str | None = None,<br>
     ) -> InteractiveShellType:<br>
-        """Factory method for creating a handler for an interactive session.<br>
+        """Extend the factory for interactive session handlers.<br>
+<br>
+        The extensions are SUT node specific:<br>
<br>
-        Instantiate shell_cls according to the remote OS specifics.<br>
+            * The default for `eal_parameters`,<br>
+            * The interactive shell path `shell_cls.path` is prepended with path to the remote<br>
+              DPDK build directory for DPDK apps.<br>
<br>
         Args:<br>
             shell_cls: The class of the shell.<br>
@@ -377,9 +448,10 @@ def create_interactive_shell(<br>
             privileged: Whether to run the shell with administrative privileges.<br>
             eal_parameters: List of EAL parameters to use to launch the app. If this<br>
                 isn't provided or an empty string is passed, it will default to calling<br>
-                create_eal_parameters().<br>
+                :meth:`create_eal_parameters`.<br>
+<br>
         Returns:<br>
-            Instance of the desired interactive application.<br>
+            An instance of the desired interactive application shell.<br>
         """<br>
         if not eal_parameters:<br>
             eal_parameters = self.create_eal_parameters()<br>
@@ -396,8 +468,8 @@ def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:<br>
         """Bind all ports on the SUT to a driver.<br>
<br>
         Args:<br>
-            for_dpdk: Boolean that, when True, binds ports to os_driver_for_dpdk<br>
-            or, when False, binds to os_driver. Defaults to True.<br>
+            for_dpdk: If :data:`True`, binds ports to os_driver_for_dpdk.<br>
+                If :data:`False`, binds to os_driver.<br>
         """<br>
         for port in self.ports:<br>
             driver = port.os_driver_for_dpdk if for_dpdk else port.os_driver<br>
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py<br>
index 8a8f0019f3..f269d4c585 100644<br>
--- a/dts/framework/testbed_model/tg_node.py<br>
+++ b/dts/framework/testbed_model/tg_node.py<br>
@@ -5,13 +5,8 @@<br>
<br>
 """Traffic generator node.<br>
<br>
-This is the node where the traffic generator resides.<br>
-The distinction between a node and a traffic generator is as follows:<br>
-A node is a host that DTS connects to. It could be a baremetal server,<br>
-a VM or a container.<br>
-A traffic generator is software running on the node.<br>
-A traffic generator node is a node running a traffic generator.<br>
-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.<br>
+A TG node is where the TG runs.<br>
 """<br>
<br>
 from scapy.packet import Packet  # type: ignore[import]<br>
@@ -24,13 +19,16 @@<br>
<br>
<br>
 class TGNode(Node):<br>
-    """Manage connections to a node with a traffic generator.<br>
+    """The traffic generator node.<br>
<br>
-    Apart from basic node management capabilities, the Traffic Generator node has<br>
-    specialized methods for handling the traffic generator running on it.<br>
+    The TG node extends :class:`Node` with TG specific features:<br>
<br>
-    Arguments:<br>
-        node_config: The user configuration of the traffic generator node.<br>
+        * Traffic generator initialization,<br>
+        * The sending of traffic and receiving packets,<br>
+        * The sending of traffic without receiving packets.<br>
+<br>
+    Not all traffic generators are capable of capturing traffic, which is why there<br>
+    must be a way to send traffic without that.<br>
<br>
     Attributes:<br>
         traffic_generator: The traffic generator running on the node.<br>
@@ -39,6 +37,13 @@ class TGNode(Node):<br>
     traffic_generator: CapturingTrafficGenerator<br>
<br>
     def __init__(self, node_config: TGNodeConfiguration):<br>
+        """Extend the constructor with TG node specifics.<br>
+<br>
+        Initialize the traffic generator on the TG node.<br>
+<br>
+        Args:<br>
+            node_config: The TG node's test run configuration.<br>
+        """<br>
         super(TGNode, self).__init__(node_config)<br>
         self.traffic_generator = create_traffic_generator(self, node_config.traffic_generator)<br>
         self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>(f"Created node: {<a href="http://self.name" rel="noreferrer" target="_blank">self.name</a>}")<br>
@@ -50,17 +55,17 @@ def send_packet_and_capture(<br>
         receive_port: Port,<br>
         duration: float = 1,<br>
     ) -> list[Packet]:<br>
-        """Send a packet, return received traffic.<br>
+        """Send `packet`, return received traffic.<br>
<br>
-        Send a packet on the send_port and then return all traffic captured<br>
-        on the receive_port for the given duration. Also record the captured traffic<br>
+        Send `packet` on `send_port` and then return all traffic captured<br>
+        on `receive_port` for the given duration. Also record the captured traffic<br>
         in a pcap file.<br>
<br>
         Args:<br>
             packet: The packet to send.<br>
             send_port: The egress port on the TG node.<br>
             receive_port: The ingress port in the TG node.<br>
-            duration: Capture traffic for this amount of time after sending the packet.<br>
+            duration: Capture traffic for this amount of time after sending `packet`.<br>
<br>
         Returns:<br>
              A list of received packets. May be empty if no packets are captured.<br>
@@ -70,6 +75,9 @@ def send_packet_and_capture(<br>
         )<br>
<br>
     def close(self) -> None:<br>
-        """Free all resources used by the node"""<br>
+        """Free all resources used by the node.<br>
+<br>
+        This extends the superclass method with TG cleanup.<br>
+        """<br>
         self.traffic_generator.close()<br>
         super(TGNode, self).close()<br>
-- <br>
2.34.1<br>
<br>
</blockquote></div></div>