[v7,15/21] dts: os session docstring update

Message ID 20231115130959.39420-16-juraj.linkes@pantheon.tech (mailing list archive)
State Superseded, archived
Delegated to: Thomas Monjalon
Headers
Series dts: docstrings update |

Checks

Context Check Description
ci/checkpatch success coding style OK

Commit Message

Juraj Linkeš Nov. 15, 2023, 1:09 p.m. UTC
  Format according to the Google format and PEP257, with slight
deviations.

Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
---
 dts/framework/testbed_model/os_session.py | 275 ++++++++++++++++------
 1 file changed, 208 insertions(+), 67 deletions(-)
  

Comments

Yoan Picchi Nov. 22, 2023, 11:50 a.m. UTC | #1
On 11/15/23 13:09, Juraj Linkeš wrote:
> Format according to the Google format and PEP257, with slight
> deviations.
> 
> Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> ---
>   dts/framework/testbed_model/os_session.py | 275 ++++++++++++++++------
>   1 file changed, 208 insertions(+), 67 deletions(-)
> 
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index 76e595a518..72b9193a61 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -2,6 +2,29 @@
>   # Copyright(c) 2023 PANTHEON.tech s.r.o.
>   # Copyright(c) 2023 University of New Hampshire
>   
> +"""OS-aware remote session.
> +
> +DPDK supports multiple different operating systems, meaning it can run on these different operating
> +systems. This module defines the common API that OS-unaware layers use and translates the API into
> +OS-aware calls/utility usage.
> +
> +Note:
> +    Running commands with administrative privileges requires OS awareness. This is the only layer
> +    that's aware of OS differences, so this is where non-privileged command get converted
> +    to privileged commands.
> +
> +Example:
> +    A user wishes to remove a directory on
> +    a remote :class:`~framework.testbed_model.sut_node.SutNode`.
> +    The :class:`~framework.testbed_model.sut_node.SutNode` object isn't aware what OS the node
> +    is running - it delegates the OS translation logic
> +    to :attr:`~framework.testbed_model.node.Node.main_session`. The SUT node calls
> +    :meth:`~OSSession.remove_remote_dir` with a generic, OS-unaware path and
> +    the :attr:`~framework.testbed_model.node.Node.main_session` translates that
> +    to ``rm -rf`` if the node's OS is Linux and other commands for other OSs.
> +    It also translates the path to match the underlying OS.
> +"""
> +
>   from abc import ABC, abstractmethod
>   from collections.abc import Iterable
>   from ipaddress import IPv4Interface, IPv6Interface
> @@ -28,10 +51,16 @@
>   
>   
>   class OSSession(ABC):
> -    """
> -    The OS classes create a DTS node remote session and implement OS specific
> +    """OS-unaware to OS-aware translation API definition.
> +
> +    The OSSession classes create a remote session to a DTS node and implement OS specific
>       behavior. There a few control methods implemented by the base class, the rest need
> -    to be implemented by derived classes.
> +    to be implemented by subclasses.
> +
> +    Attributes:
> +        name: The name of the session.
> +        remote_session: The remote session maintaining the connection to the node.
> +        interactive_session: The interactive remote session maintaining the connection to the node.
>       """
>   
>       _config: NodeConfiguration
> @@ -46,6 +75,15 @@ def __init__(
>           name: str,
>           logger: DTSLOG,
>       ):
> +        """Initialize the OS-aware session.
> +
> +        Connect to the node right away and also create an interactive remote session.
> +
> +        Args:
> +            node_config: The test run configuration of the node to connect to.
> +            name: The name of the session.
> +            logger: The logger instance this session will use.
> +        """
>           self._config = node_config
>           self.name = name
>           self._logger = logger
> @@ -53,15 +91,15 @@ def __init__(
>           self.interactive_session = create_interactive_session(node_config, logger)
>   
>       def close(self, force: bool = False) -> None:
> -        """
> -        Close the remote session.
> +        """Close the underlying remote session.
> +
> +        Args:
> +            force: Force the closure of the connection.
>           """
>           self.remote_session.close(force)
>   
>       def is_alive(self) -> bool:
> -        """
> -        Check whether the remote session is still responding.
> -        """
> +        """Check whether the underlying remote session is still responding."""
>           return self.remote_session.is_alive()
>   
>       def send_command(
> @@ -72,10 +110,23 @@ def send_command(
>           verify: bool = False,
>           env: dict | None = None,
>       ) -> CommandResult:
> -        """
> -        An all-purpose API in case the command to be executed is already
> -        OS-agnostic, such as when the path to the executed command has been
> -        constructed beforehand.
> +        """An all-purpose API for OS-agnostic commands.
> +
> +        This can be used for an execution of a portable command that's executed the same way
> +        on all operating systems, such as Python.
> +
> +        The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT`
> +        environment variable configure the timeout of command execution.
> +
> +        Args:
> +            command: The command to execute.
> +            timeout: Wait at most this long in seconds to execute the command.

confusing start/end of execution

> +            privileged: Whether to run the command with administrative privileges.
> +            verify: If :data:`True`, will check the exit code of the command.
> +            env: A dictionary with environment variables to be used with the command execution.
> +
> +        Raises:
> +            RemoteCommandExecutionError: If verify is :data:`True` and the command failed.
>           """
>           if privileged:
>               command = self._get_privileged_command(command)
> @@ -89,8 +140,20 @@ def create_interactive_shell(
>           privileged: bool,
>           app_args: str,
>       ) -> InteractiveShellType:
> -        """
> -        See "create_interactive_shell" in SutNode
> +        """Factory for interactive session handlers.
> +
> +        Instantiate `shell_cls` according to the remote OS specifics.
> +
> +        Args:
> +            shell_cls: The class of the shell.
> +            timeout: Timeout for reading output from the SSH channel. If you are
> +                reading from the buffer and don't receive any data within the timeout
> +                it will throw an error.
> +            privileged: Whether to run the shell with administrative privileges.
> +            app_args: The arguments to be passed to the application.
> +
> +        Returns:
> +            An instance of the desired interactive application shell.
>           """
>           return shell_cls(
>               self.interactive_session.session,
> @@ -114,27 +177,42 @@ def _get_privileged_command(command: str) -> str:
>   
>       @abstractmethod
>       def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:
> -        """
> -        Try to find DPDK remote dir in remote_dir.
> +        """Try to find DPDK directory in `remote_dir`.
> +
> +        The directory is the one which is created after the extraction of the tarball. The files
> +        are usually extracted into a directory starting with ``dpdk-``.
> +
> +        Returns:
> +            The absolute path of the DPDK remote directory, empty path if not found.
>           """
>   
>       @abstractmethod
>       def get_remote_tmp_dir(self) -> PurePath:
> -        """
> -        Get the path of the temporary directory of the remote OS.
> +        """Get the path of the temporary directory of the remote OS.
> +
> +        Returns:
> +            The absolute path of the temporary directory.
>           """
>   
>       @abstractmethod
>       def get_dpdk_build_env_vars(self, arch: Architecture) -> dict:
> -        """
> -        Create extra environment variables needed for the target architecture. Get
> -        information from the node if needed.
> +        """Create extra environment variables needed for the target architecture.
> +
> +        Different architectures may require different configuration, such as setting 32-bit CFLAGS.
> +
> +        Returns:
> +            A dictionary with keys as environment variables.
>           """
>   
>       @abstractmethod
>       def join_remote_path(self, *args: str | PurePath) -> PurePath:
> -        """
> -        Join path parts using the path separator that fits the remote OS.
> +        """Join path parts using the path separator that fits the remote OS.
> +
> +        Args:
> +            args: Any number of paths to join.
> +
> +        Returns:
> +            The resulting joined path.
>           """
>   
>       @abstractmethod
> @@ -143,13 +221,13 @@ def copy_from(
>           source_file: str | PurePath,
>           destination_file: str | PurePath,
>       ) -> None:
> -        """Copy a file from the remote Node to the local filesystem.
> +        """Copy a file from the remote node to the local filesystem.
>   
> -        Copy source_file from the remote Node associated with this remote
> -        session to destination_file on the local filesystem.
> +        Copy `source_file` from the remote node associated with this remote
> +        session to `destination_file` on the local filesystem.
>   
>           Args:
> -            source_file: the file on the remote Node.
> +            source_file: the file on the remote node.
>               destination_file: a file or directory path on the local filesystem.
>           """
>   
> @@ -159,14 +237,14 @@ def copy_to(
>           source_file: str | PurePath,
>           destination_file: str | PurePath,
>       ) -> None:
> -        """Copy a file from local filesystem to the remote Node.
> +        """Copy a file from local filesystem to the remote node.
>   
> -        Copy source_file from local filesystem to destination_file
> -        on the remote Node associated with this remote session.
> +        Copy `source_file` from local filesystem to `destination_file`
> +        on the remote node associated with this remote session.
>   
>           Args:
>               source_file: the file on the local filesystem.
> -            destination_file: a file or directory path on the remote Node.
> +            destination_file: a file or directory path on the remote node.
>           """
>   
>       @abstractmethod
> @@ -176,8 +254,12 @@ def remove_remote_dir(
>           recursive: bool = True,
>           force: bool = True,
>       ) -> None:
> -        """
> -        Remove remote directory, by default remove recursively and forcefully.
> +        """Remove remote directory, by default remove recursively and forcefully.
> +
> +        Args:
> +            remote_dir_path: The path of the directory to remove.
> +            recursive: If :data:`True`, also remove all contents inside the directory.
> +            force: If :data:`True`, ignore all warnings and try to remove at all costs.
>           """
>   
>       @abstractmethod
> @@ -186,9 +268,12 @@ def extract_remote_tarball(
>           remote_tarball_path: str | PurePath,
>           expected_dir: str | PurePath | None = None,
>       ) -> None:
> -        """
> -        Extract remote tarball in place. If expected_dir is a non-empty string, check
> -        whether the dir exists after extracting the archive.
> +        """Extract remote tarball in its remote directory.
> +
> +        Args:
> +            remote_tarball_path: The path of the tarball on the remote node.
> +            expected_dir: If non-empty, check whether `expected_dir` exists after extracting
> +                the archive.
>           """
>   
>       @abstractmethod
> @@ -201,69 +286,119 @@ def build_dpdk(
>           rebuild: bool = False,
>           timeout: float = SETTINGS.compile_timeout,
>       ) -> None:
> -        """
> -        Build DPDK in the input dir with specified environment variables and meson
> -        arguments.
> +        """Build DPDK on the remote node.
> +
> +        An extracted DPDK tarball must be present on the node. The build consists of two steps::
> +
> +            meson setup <meson args> remote_dpdk_dir remote_dpdk_build_dir
> +            ninja -C remote_dpdk_build_dir
> +
> +        The :option:`--compile-timeout` command line argument and the :envvar:`DTS_COMPILE_TIMEOUT`
> +        environment variable configure the timeout of DPDK build.
> +
> +        Args:
> +            env_vars: Use these environment variables then building DPDK.
> +            meson_args: Use these meson arguments when building DPDK.
> +            remote_dpdk_dir: The directory on the remote node where DPDK will be built.
> +            remote_dpdk_build_dir: The target build directory on the remote node.
> +            rebuild: If :data:`True`, do a subsequent build with ``meson configure`` instead
> +                of ``meson setup``.
> +            timeout: Wait at most this long in seconds for the build to execute.

confusing start/end of execution

>           """
>   
>       @abstractmethod
>       def get_dpdk_version(self, version_path: str | PurePath) -> str:
> -        """
> -        Inspect DPDK version on the remote node from version_path.
> +        """Inspect the DPDK version on the remote node.
> +
> +        Args:
> +            version_path: The path to the VERSION file containing the DPDK version.
> +
> +        Returns:
> +            The DPDK version.
>           """
>   
>       @abstractmethod
>       def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:
> -        """
> -        Compose a list of LogicalCores present on the remote node.
> -        If use_first_core is False, the first physical core won't be used.
> +        r"""Get the list of :class:`~framework.testbed_model.cpu.LogicalCore`\s on the remote node.
> +
> +        Args:
> +            use_first_core: If :data:`False`, the first physical core won't be used.
> +
> +        Returns:
> +            The logical cores present on the node.
>           """
>   
>       @abstractmethod
>       def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:
> -        """
> -        Kill and cleanup all DPDK apps identified by dpdk_prefix_list. If
> -        dpdk_prefix_list is empty, attempt to find running DPDK apps to kill and clean.
> +        """Kill and cleanup all DPDK apps.
> +
> +        Args:
> +            dpdk_prefix_list: Kill all apps identified by `dpdk_prefix_list`.
> +                If `dpdk_prefix_list` is empty, attempt to find running DPDK apps to kill and clean.
>           """
>   
>       @abstractmethod
>       def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
> -        """
> -        Get the DPDK file prefix that will be used when running DPDK apps.
> +        """Make OS-specific modification to the DPDK file prefix.
> +
> +        Args:
> +           dpdk_prefix: The OS-unaware file prefix.
> +
> +        Returns:
> +            The OS-specific file prefix.
>           """
>   
>       @abstractmethod
> -    def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None:
> -        """
> -        Get the node's Hugepage Size, configure the specified amount of hugepages
> +    def setup_hugepages(self, hugepage_count: int, force_first_numa: bool) -> None:
> +        """Configure hugepages on the node.
> +
> +        Get the node's Hugepage Size, configure the specified count of hugepages
>           if needed and mount the hugepages if needed.
> -        If force_first_numa is True, configure hugepages just on the first socket.
> +
> +        Args:
> +            hugepage_count: Configure this many hugepages.
> +            force_first_numa:  If :data:`True`, configure hugepages just on the first socket.

force *numa* configures the first *socket* ?

>           """
>   
>       @abstractmethod
>       def get_compiler_version(self, compiler_name: str) -> str:
> -        """
> -        Get installed version of compiler used for DPDK
> +        """Get installed version of compiler used for DPDK.
> +
> +        Args:
> +            compiler_name: The name of the compiler executable.
> +
> +        Returns:
> +            The compiler's version.
>           """
>   
>       @abstractmethod
>       def get_node_info(self) -> NodeInfo:
> -        """
> -        Collect information about the node
> +        """Collect additional information about the node.
> +
> +        Returns:
> +            Node information.
>           """
>   
>       @abstractmethod
>       def update_ports(self, ports: list[Port]) -> None:
> -        """
> -        Get additional information about ports:
> -            Logical name (e.g. enp7s0) if applicable
> -            Mac address
> +        """Get additional information about ports from the operating system and update them.
> +
> +        The additional information is:
> +
> +            * Logical name (e.g. ``enp7s0``) if applicable,
> +            * Mac address.
> +
> +        Args:
> +            ports: The ports to update.
>           """
>   
>       @abstractmethod
>       def configure_port_state(self, port: Port, enable: bool) -> None:
> -        """
> -        Enable/disable port.
> +        """Enable/disable `port` in the operating system.
> +
> +        Args:
> +            port: The port to configure.
> +            enable: If :data:`True`, enable the port, otherwise shut it down.
>           """
>   
>       @abstractmethod
> @@ -273,12 +408,18 @@ def configure_port_ip_address(
>           port: Port,
>           delete: bool,
>       ) -> None:
> -        """
> -        Configure (add or delete) an IP address of the input port.
> +        """Configure an IP address on `port` in the operating system.
> +
> +        Args:
> +            address: The address to configure.
> +            port: The port to configure.
> +            delete: If :data:`True`, remove the IP address, otherwise configure it.
>           """
>   
>       @abstractmethod
>       def configure_ipv4_forwarding(self, enable: bool) -> None:
> -        """
> -        Enable IPv4 forwarding in the underlying OS.
> +        """Enable IPv4 forwarding in the operating system.
> +
> +        Args:
> +            enable: If :data:`True`, enable the forwarding, otherwise disable it.
>           """
  
Juraj Linkeš Nov. 22, 2023, 1:27 p.m. UTC | #2
On Wed, Nov 22, 2023 at 12:50 PM Yoan Picchi <yoan.picchi@foss.arm.com> wrote:
>
> On 11/15/23 13:09, Juraj Linkeš wrote:
> > Format according to the Google format and PEP257, with slight
> > deviations.
> >
> > Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> > ---
> >   dts/framework/testbed_model/os_session.py | 275 ++++++++++++++++------
> >   1 file changed, 208 insertions(+), 67 deletions(-)
> >
> > diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> > index 76e595a518..72b9193a61 100644
> > --- a/dts/framework/testbed_model/os_session.py
> > +++ b/dts/framework/testbed_model/os_session.py
> > @@ -2,6 +2,29 @@
> >   # Copyright(c) 2023 PANTHEON.tech s.r.o.
> >   # Copyright(c) 2023 University of New Hampshire
> >
> > +"""OS-aware remote session.
> > +
> > +DPDK supports multiple different operating systems, meaning it can run on these different operating
> > +systems. This module defines the common API that OS-unaware layers use and translates the API into
> > +OS-aware calls/utility usage.
> > +
> > +Note:
> > +    Running commands with administrative privileges requires OS awareness. This is the only layer
> > +    that's aware of OS differences, so this is where non-privileged command get converted
> > +    to privileged commands.
> > +
> > +Example:
> > +    A user wishes to remove a directory on
> > +    a remote :class:`~framework.testbed_model.sut_node.SutNode`.
> > +    The :class:`~framework.testbed_model.sut_node.SutNode` object isn't aware what OS the node
> > +    is running - it delegates the OS translation logic
> > +    to :attr:`~framework.testbed_model.node.Node.main_session`. The SUT node calls
> > +    :meth:`~OSSession.remove_remote_dir` with a generic, OS-unaware path and
> > +    the :attr:`~framework.testbed_model.node.Node.main_session` translates that
> > +    to ``rm -rf`` if the node's OS is Linux and other commands for other OSs.
> > +    It also translates the path to match the underlying OS.
> > +"""
> > +
> >   from abc import ABC, abstractmethod
> >   from collections.abc import Iterable
> >   from ipaddress import IPv4Interface, IPv6Interface
> > @@ -28,10 +51,16 @@
> >
> >
> >   class OSSession(ABC):
> > -    """
> > -    The OS classes create a DTS node remote session and implement OS specific
> > +    """OS-unaware to OS-aware translation API definition.
> > +
> > +    The OSSession classes create a remote session to a DTS node and implement OS specific
> >       behavior. There a few control methods implemented by the base class, the rest need
> > -    to be implemented by derived classes.
> > +    to be implemented by subclasses.
> > +
> > +    Attributes:
> > +        name: The name of the session.
> > +        remote_session: The remote session maintaining the connection to the node.
> > +        interactive_session: The interactive remote session maintaining the connection to the node.
> >       """
> >
> >       _config: NodeConfiguration
> > @@ -46,6 +75,15 @@ def __init__(
> >           name: str,
> >           logger: DTSLOG,
> >       ):
> > +        """Initialize the OS-aware session.
> > +
> > +        Connect to the node right away and also create an interactive remote session.
> > +
> > +        Args:
> > +            node_config: The test run configuration of the node to connect to.
> > +            name: The name of the session.
> > +            logger: The logger instance this session will use.
> > +        """
> >           self._config = node_config
> >           self.name = name
> >           self._logger = logger
> > @@ -53,15 +91,15 @@ def __init__(
> >           self.interactive_session = create_interactive_session(node_config, logger)
> >
> >       def close(self, force: bool = False) -> None:
> > -        """
> > -        Close the remote session.
> > +        """Close the underlying remote session.
> > +
> > +        Args:
> > +            force: Force the closure of the connection.
> >           """
> >           self.remote_session.close(force)
> >
> >       def is_alive(self) -> bool:
> > -        """
> > -        Check whether the remote session is still responding.
> > -        """
> > +        """Check whether the underlying remote session is still responding."""
> >           return self.remote_session.is_alive()
> >
> >       def send_command(
> > @@ -72,10 +110,23 @@ def send_command(
> >           verify: bool = False,
> >           env: dict | None = None,
> >       ) -> CommandResult:
> > -        """
> > -        An all-purpose API in case the command to be executed is already
> > -        OS-agnostic, such as when the path to the executed command has been
> > -        constructed beforehand.
> > +        """An all-purpose API for OS-agnostic commands.
> > +
> > +        This can be used for an execution of a portable command that's executed the same way
> > +        on all operating systems, such as Python.
> > +
> > +        The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT`
> > +        environment variable configure the timeout of command execution.
> > +
> > +        Args:
> > +            command: The command to execute.
> > +            timeout: Wait at most this long in seconds to execute the command.
>
> confusing start/end of execution
>

Ack.

> > +            privileged: Whether to run the command with administrative privileges.
> > +            verify: If :data:`True`, will check the exit code of the command.
> > +            env: A dictionary with environment variables to be used with the command execution.
> > +
> > +        Raises:
> > +            RemoteCommandExecutionError: If verify is :data:`True` and the command failed.
> >           """
> >           if privileged:
> >               command = self._get_privileged_command(command)
> > @@ -89,8 +140,20 @@ def create_interactive_shell(
> >           privileged: bool,
> >           app_args: str,
> >       ) -> InteractiveShellType:
> > -        """
> > -        See "create_interactive_shell" in SutNode
> > +        """Factory for interactive session handlers.
> > +
> > +        Instantiate `shell_cls` according to the remote OS specifics.
> > +
> > +        Args:
> > +            shell_cls: The class of the shell.
> > +            timeout: Timeout for reading output from the SSH channel. If you are
> > +                reading from the buffer and don't receive any data within the timeout
> > +                it will throw an error.
> > +            privileged: Whether to run the shell with administrative privileges.
> > +            app_args: The arguments to be passed to the application.
> > +
> > +        Returns:
> > +            An instance of the desired interactive application shell.
> >           """
> >           return shell_cls(
> >               self.interactive_session.session,
> > @@ -114,27 +177,42 @@ def _get_privileged_command(command: str) -> str:
> >
> >       @abstractmethod
> >       def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:
> > -        """
> > -        Try to find DPDK remote dir in remote_dir.
> > +        """Try to find DPDK directory in `remote_dir`.
> > +
> > +        The directory is the one which is created after the extraction of the tarball. The files
> > +        are usually extracted into a directory starting with ``dpdk-``.
> > +
> > +        Returns:
> > +            The absolute path of the DPDK remote directory, empty path if not found.
> >           """
> >
> >       @abstractmethod
> >       def get_remote_tmp_dir(self) -> PurePath:
> > -        """
> > -        Get the path of the temporary directory of the remote OS.
> > +        """Get the path of the temporary directory of the remote OS.
> > +
> > +        Returns:
> > +            The absolute path of the temporary directory.
> >           """
> >
> >       @abstractmethod
> >       def get_dpdk_build_env_vars(self, arch: Architecture) -> dict:
> > -        """
> > -        Create extra environment variables needed for the target architecture. Get
> > -        information from the node if needed.
> > +        """Create extra environment variables needed for the target architecture.
> > +
> > +        Different architectures may require different configuration, such as setting 32-bit CFLAGS.
> > +
> > +        Returns:
> > +            A dictionary with keys as environment variables.
> >           """
> >
> >       @abstractmethod
> >       def join_remote_path(self, *args: str | PurePath) -> PurePath:
> > -        """
> > -        Join path parts using the path separator that fits the remote OS.
> > +        """Join path parts using the path separator that fits the remote OS.
> > +
> > +        Args:
> > +            args: Any number of paths to join.
> > +
> > +        Returns:
> > +            The resulting joined path.
> >           """
> >
> >       @abstractmethod
> > @@ -143,13 +221,13 @@ def copy_from(
> >           source_file: str | PurePath,
> >           destination_file: str | PurePath,
> >       ) -> None:
> > -        """Copy a file from the remote Node to the local filesystem.
> > +        """Copy a file from the remote node to the local filesystem.
> >
> > -        Copy source_file from the remote Node associated with this remote
> > -        session to destination_file on the local filesystem.
> > +        Copy `source_file` from the remote node associated with this remote
> > +        session to `destination_file` on the local filesystem.
> >
> >           Args:
> > -            source_file: the file on the remote Node.
> > +            source_file: the file on the remote node.
> >               destination_file: a file or directory path on the local filesystem.
> >           """
> >
> > @@ -159,14 +237,14 @@ def copy_to(
> >           source_file: str | PurePath,
> >           destination_file: str | PurePath,
> >       ) -> None:
> > -        """Copy a file from local filesystem to the remote Node.
> > +        """Copy a file from local filesystem to the remote node.
> >
> > -        Copy source_file from local filesystem to destination_file
> > -        on the remote Node associated with this remote session.
> > +        Copy `source_file` from local filesystem to `destination_file`
> > +        on the remote node associated with this remote session.
> >
> >           Args:
> >               source_file: the file on the local filesystem.
> > -            destination_file: a file or directory path on the remote Node.
> > +            destination_file: a file or directory path on the remote node.
> >           """
> >
> >       @abstractmethod
> > @@ -176,8 +254,12 @@ def remove_remote_dir(
> >           recursive: bool = True,
> >           force: bool = True,
> >       ) -> None:
> > -        """
> > -        Remove remote directory, by default remove recursively and forcefully.
> > +        """Remove remote directory, by default remove recursively and forcefully.
> > +
> > +        Args:
> > +            remote_dir_path: The path of the directory to remove.
> > +            recursive: If :data:`True`, also remove all contents inside the directory.
> > +            force: If :data:`True`, ignore all warnings and try to remove at all costs.
> >           """
> >
> >       @abstractmethod
> > @@ -186,9 +268,12 @@ def extract_remote_tarball(
> >           remote_tarball_path: str | PurePath,
> >           expected_dir: str | PurePath | None = None,
> >       ) -> None:
> > -        """
> > -        Extract remote tarball in place. If expected_dir is a non-empty string, check
> > -        whether the dir exists after extracting the archive.
> > +        """Extract remote tarball in its remote directory.
> > +
> > +        Args:
> > +            remote_tarball_path: The path of the tarball on the remote node.
> > +            expected_dir: If non-empty, check whether `expected_dir` exists after extracting
> > +                the archive.
> >           """
> >
> >       @abstractmethod
> > @@ -201,69 +286,119 @@ def build_dpdk(
> >           rebuild: bool = False,
> >           timeout: float = SETTINGS.compile_timeout,
> >       ) -> None:
> > -        """
> > -        Build DPDK in the input dir with specified environment variables and meson
> > -        arguments.
> > +        """Build DPDK on the remote node.
> > +
> > +        An extracted DPDK tarball must be present on the node. The build consists of two steps::
> > +
> > +            meson setup <meson args> remote_dpdk_dir remote_dpdk_build_dir
> > +            ninja -C remote_dpdk_build_dir
> > +
> > +        The :option:`--compile-timeout` command line argument and the :envvar:`DTS_COMPILE_TIMEOUT`
> > +        environment variable configure the timeout of DPDK build.
> > +
> > +        Args:
> > +            env_vars: Use these environment variables then building DPDK.
> > +            meson_args: Use these meson arguments when building DPDK.
> > +            remote_dpdk_dir: The directory on the remote node where DPDK will be built.
> > +            remote_dpdk_build_dir: The target build directory on the remote node.
> > +            rebuild: If :data:`True`, do a subsequent build with ``meson configure`` instead
> > +                of ``meson setup``.
> > +            timeout: Wait at most this long in seconds for the build to execute.
>
> confusing start/end of execution
>

Ack.

> >           """
> >
> >       @abstractmethod
> >       def get_dpdk_version(self, version_path: str | PurePath) -> str:
> > -        """
> > -        Inspect DPDK version on the remote node from version_path.
> > +        """Inspect the DPDK version on the remote node.
> > +
> > +        Args:
> > +            version_path: The path to the VERSION file containing the DPDK version.
> > +
> > +        Returns:
> > +            The DPDK version.
> >           """
> >
> >       @abstractmethod
> >       def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:
> > -        """
> > -        Compose a list of LogicalCores present on the remote node.
> > -        If use_first_core is False, the first physical core won't be used.
> > +        r"""Get the list of :class:`~framework.testbed_model.cpu.LogicalCore`\s on the remote node.
> > +
> > +        Args:
> > +            use_first_core: If :data:`False`, the first physical core won't be used.
> > +
> > +        Returns:
> > +            The logical cores present on the node.
> >           """
> >
> >       @abstractmethod
> >       def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:
> > -        """
> > -        Kill and cleanup all DPDK apps identified by dpdk_prefix_list. If
> > -        dpdk_prefix_list is empty, attempt to find running DPDK apps to kill and clean.
> > +        """Kill and cleanup all DPDK apps.
> > +
> > +        Args:
> > +            dpdk_prefix_list: Kill all apps identified by `dpdk_prefix_list`.
> > +                If `dpdk_prefix_list` is empty, attempt to find running DPDK apps to kill and clean.
> >           """
> >
> >       @abstractmethod
> >       def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
> > -        """
> > -        Get the DPDK file prefix that will be used when running DPDK apps.
> > +        """Make OS-specific modification to the DPDK file prefix.
> > +
> > +        Args:
> > +           dpdk_prefix: The OS-unaware file prefix.
> > +
> > +        Returns:
> > +            The OS-specific file prefix.
> >           """
> >
> >       @abstractmethod
> > -    def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None:
> > -        """
> > -        Get the node's Hugepage Size, configure the specified amount of hugepages
> > +    def setup_hugepages(self, hugepage_count: int, force_first_numa: bool) -> None:
> > +        """Configure hugepages on the node.
> > +
> > +        Get the node's Hugepage Size, configure the specified count of hugepages
> >           if needed and mount the hugepages if needed.
> > -        If force_first_numa is True, configure hugepages just on the first socket.
> > +
> > +        Args:
> > +            hugepage_count: Configure this many hugepages.
> > +            force_first_numa:  If :data:`True`, configure hugepages just on the first socket.
>
> force *numa* configures the first *socket* ?
>

Good catch, should be numa node, not socket.

> >           """
> >
> >       @abstractmethod
> >       def get_compiler_version(self, compiler_name: str) -> str:
> > -        """
> > -        Get installed version of compiler used for DPDK
> > +        """Get installed version of compiler used for DPDK.
> > +
> > +        Args:
> > +            compiler_name: The name of the compiler executable.
> > +
> > +        Returns:
> > +            The compiler's version.
> >           """
> >
> >       @abstractmethod
> >       def get_node_info(self) -> NodeInfo:
> > -        """
> > -        Collect information about the node
> > +        """Collect additional information about the node.
> > +
> > +        Returns:
> > +            Node information.
> >           """
> >
> >       @abstractmethod
> >       def update_ports(self, ports: list[Port]) -> None:
> > -        """
> > -        Get additional information about ports:
> > -            Logical name (e.g. enp7s0) if applicable
> > -            Mac address
> > +        """Get additional information about ports from the operating system and update them.
> > +
> > +        The additional information is:
> > +
> > +            * Logical name (e.g. ``enp7s0``) if applicable,
> > +            * Mac address.
> > +
> > +        Args:
> > +            ports: The ports to update.
> >           """
> >
> >       @abstractmethod
> >       def configure_port_state(self, port: Port, enable: bool) -> None:
> > -        """
> > -        Enable/disable port.
> > +        """Enable/disable `port` in the operating system.
> > +
> > +        Args:
> > +            port: The port to configure.
> > +            enable: If :data:`True`, enable the port, otherwise shut it down.
> >           """
> >
> >       @abstractmethod
> > @@ -273,12 +408,18 @@ def configure_port_ip_address(
> >           port: Port,
> >           delete: bool,
> >       ) -> None:
> > -        """
> > -        Configure (add or delete) an IP address of the input port.
> > +        """Configure an IP address on `port` in the operating system.
> > +
> > +        Args:
> > +            address: The address to configure.
> > +            port: The port to configure.
> > +            delete: If :data:`True`, remove the IP address, otherwise configure it.
> >           """
> >
> >       @abstractmethod
> >       def configure_ipv4_forwarding(self, enable: bool) -> None:
> > -        """
> > -        Enable IPv4 forwarding in the underlying OS.
> > +        """Enable IPv4 forwarding in the operating system.
> > +
> > +        Args:
> > +            enable: If :data:`True`, enable the forwarding, otherwise disable it.
> >           """
>
  

Patch

diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 76e595a518..72b9193a61 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -2,6 +2,29 @@ 
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
 
+"""OS-aware remote session.
+
+DPDK supports multiple different operating systems, meaning it can run on these different operating
+systems. This module defines the common API that OS-unaware layers use and translates the API into
+OS-aware calls/utility usage.
+
+Note:
+    Running commands with administrative privileges requires OS awareness. This is the only layer
+    that's aware of OS differences, so this is where non-privileged command get converted
+    to privileged commands.
+
+Example:
+    A user wishes to remove a directory on
+    a remote :class:`~framework.testbed_model.sut_node.SutNode`.
+    The :class:`~framework.testbed_model.sut_node.SutNode` object isn't aware what OS the node
+    is running - it delegates the OS translation logic
+    to :attr:`~framework.testbed_model.node.Node.main_session`. The SUT node calls
+    :meth:`~OSSession.remove_remote_dir` with a generic, OS-unaware path and
+    the :attr:`~framework.testbed_model.node.Node.main_session` translates that
+    to ``rm -rf`` if the node's OS is Linux and other commands for other OSs.
+    It also translates the path to match the underlying OS.
+"""
+
 from abc import ABC, abstractmethod
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
@@ -28,10 +51,16 @@ 
 
 
 class OSSession(ABC):
-    """
-    The OS classes create a DTS node remote session and implement OS specific
+    """OS-unaware to OS-aware translation API definition.
+
+    The OSSession classes create a remote session to a DTS node and implement OS specific
     behavior. There a few control methods implemented by the base class, the rest need
-    to be implemented by derived classes.
+    to be implemented by subclasses.
+
+    Attributes:
+        name: The name of the session.
+        remote_session: The remote session maintaining the connection to the node.
+        interactive_session: The interactive remote session maintaining the connection to the node.
     """
 
     _config: NodeConfiguration
@@ -46,6 +75,15 @@  def __init__(
         name: str,
         logger: DTSLOG,
     ):
+        """Initialize the OS-aware session.
+
+        Connect to the node right away and also create an interactive remote session.
+
+        Args:
+            node_config: The test run configuration of the node to connect to.
+            name: The name of the session.
+            logger: The logger instance this session will use.
+        """
         self._config = node_config
         self.name = name
         self._logger = logger
@@ -53,15 +91,15 @@  def __init__(
         self.interactive_session = create_interactive_session(node_config, logger)
 
     def close(self, force: bool = False) -> None:
-        """
-        Close the remote session.
+        """Close the underlying remote session.
+
+        Args:
+            force: Force the closure of the connection.
         """
         self.remote_session.close(force)
 
     def is_alive(self) -> bool:
-        """
-        Check whether the remote session is still responding.
-        """
+        """Check whether the underlying remote session is still responding."""
         return self.remote_session.is_alive()
 
     def send_command(
@@ -72,10 +110,23 @@  def send_command(
         verify: bool = False,
         env: dict | None = None,
     ) -> CommandResult:
-        """
-        An all-purpose API in case the command to be executed is already
-        OS-agnostic, such as when the path to the executed command has been
-        constructed beforehand.
+        """An all-purpose API for OS-agnostic commands.
+
+        This can be used for an execution of a portable command that's executed the same way
+        on all operating systems, such as Python.
+
+        The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT`
+        environment variable configure the timeout of command execution.
+
+        Args:
+            command: The command to execute.
+            timeout: Wait at most this long in seconds to execute the command.
+            privileged: Whether to run the command with administrative privileges.
+            verify: If :data:`True`, will check the exit code of the command.
+            env: A dictionary with environment variables to be used with the command execution.
+
+        Raises:
+            RemoteCommandExecutionError: If verify is :data:`True` and the command failed.
         """
         if privileged:
             command = self._get_privileged_command(command)
@@ -89,8 +140,20 @@  def create_interactive_shell(
         privileged: bool,
         app_args: str,
     ) -> InteractiveShellType:
-        """
-        See "create_interactive_shell" in SutNode
+        """Factory for interactive session handlers.
+
+        Instantiate `shell_cls` according to the remote OS specifics.
+
+        Args:
+            shell_cls: The class of the shell.
+            timeout: Timeout for reading output from the SSH channel. If you are
+                reading from the buffer and don't receive any data within the timeout
+                it will throw an error.
+            privileged: Whether to run the shell with administrative privileges.
+            app_args: The arguments to be passed to the application.
+
+        Returns:
+            An instance of the desired interactive application shell.
         """
         return shell_cls(
             self.interactive_session.session,
@@ -114,27 +177,42 @@  def _get_privileged_command(command: str) -> str:
 
     @abstractmethod
     def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:
-        """
-        Try to find DPDK remote dir in remote_dir.
+        """Try to find DPDK directory in `remote_dir`.
+
+        The directory is the one which is created after the extraction of the tarball. The files
+        are usually extracted into a directory starting with ``dpdk-``.
+
+        Returns:
+            The absolute path of the DPDK remote directory, empty path if not found.
         """
 
     @abstractmethod
     def get_remote_tmp_dir(self) -> PurePath:
-        """
-        Get the path of the temporary directory of the remote OS.
+        """Get the path of the temporary directory of the remote OS.
+
+        Returns:
+            The absolute path of the temporary directory.
         """
 
     @abstractmethod
     def get_dpdk_build_env_vars(self, arch: Architecture) -> dict:
-        """
-        Create extra environment variables needed for the target architecture. Get
-        information from the node if needed.
+        """Create extra environment variables needed for the target architecture.
+
+        Different architectures may require different configuration, such as setting 32-bit CFLAGS.
+
+        Returns:
+            A dictionary with keys as environment variables.
         """
 
     @abstractmethod
     def join_remote_path(self, *args: str | PurePath) -> PurePath:
-        """
-        Join path parts using the path separator that fits the remote OS.
+        """Join path parts using the path separator that fits the remote OS.
+
+        Args:
+            args: Any number of paths to join.
+
+        Returns:
+            The resulting joined path.
         """
 
     @abstractmethod
@@ -143,13 +221,13 @@  def copy_from(
         source_file: str | PurePath,
         destination_file: str | PurePath,
     ) -> None:
-        """Copy a file from the remote Node to the local filesystem.
+        """Copy a file from the remote node to the local filesystem.
 
-        Copy source_file from the remote Node associated with this remote
-        session to destination_file on the local filesystem.
+        Copy `source_file` from the remote node associated with this remote
+        session to `destination_file` on the local filesystem.
 
         Args:
-            source_file: the file on the remote Node.
+            source_file: the file on the remote node.
             destination_file: a file or directory path on the local filesystem.
         """
 
@@ -159,14 +237,14 @@  def copy_to(
         source_file: str | PurePath,
         destination_file: str | PurePath,
     ) -> None:
-        """Copy a file from local filesystem to the remote Node.
+        """Copy a file from local filesystem to the remote node.
 
-        Copy source_file from local filesystem to destination_file
-        on the remote Node associated with this remote session.
+        Copy `source_file` from local filesystem to `destination_file`
+        on the remote node associated with this remote session.
 
         Args:
             source_file: the file on the local filesystem.
-            destination_file: a file or directory path on the remote Node.
+            destination_file: a file or directory path on the remote node.
         """
 
     @abstractmethod
@@ -176,8 +254,12 @@  def remove_remote_dir(
         recursive: bool = True,
         force: bool = True,
     ) -> None:
-        """
-        Remove remote directory, by default remove recursively and forcefully.
+        """Remove remote directory, by default remove recursively and forcefully.
+
+        Args:
+            remote_dir_path: The path of the directory to remove.
+            recursive: If :data:`True`, also remove all contents inside the directory.
+            force: If :data:`True`, ignore all warnings and try to remove at all costs.
         """
 
     @abstractmethod
@@ -186,9 +268,12 @@  def extract_remote_tarball(
         remote_tarball_path: str | PurePath,
         expected_dir: str | PurePath | None = None,
     ) -> None:
-        """
-        Extract remote tarball in place. If expected_dir is a non-empty string, check
-        whether the dir exists after extracting the archive.
+        """Extract remote tarball in its remote directory.
+
+        Args:
+            remote_tarball_path: The path of the tarball on the remote node.
+            expected_dir: If non-empty, check whether `expected_dir` exists after extracting
+                the archive.
         """
 
     @abstractmethod
@@ -201,69 +286,119 @@  def build_dpdk(
         rebuild: bool = False,
         timeout: float = SETTINGS.compile_timeout,
     ) -> None:
-        """
-        Build DPDK in the input dir with specified environment variables and meson
-        arguments.
+        """Build DPDK on the remote node.
+
+        An extracted DPDK tarball must be present on the node. The build consists of two steps::
+
+            meson setup <meson args> remote_dpdk_dir remote_dpdk_build_dir
+            ninja -C remote_dpdk_build_dir
+
+        The :option:`--compile-timeout` command line argument and the :envvar:`DTS_COMPILE_TIMEOUT`
+        environment variable configure the timeout of DPDK build.
+
+        Args:
+            env_vars: Use these environment variables then building DPDK.
+            meson_args: Use these meson arguments when building DPDK.
+            remote_dpdk_dir: The directory on the remote node where DPDK will be built.
+            remote_dpdk_build_dir: The target build directory on the remote node.
+            rebuild: If :data:`True`, do a subsequent build with ``meson configure`` instead
+                of ``meson setup``.
+            timeout: Wait at most this long in seconds for the build to execute.
         """
 
     @abstractmethod
     def get_dpdk_version(self, version_path: str | PurePath) -> str:
-        """
-        Inspect DPDK version on the remote node from version_path.
+        """Inspect the DPDK version on the remote node.
+
+        Args:
+            version_path: The path to the VERSION file containing the DPDK version.
+
+        Returns:
+            The DPDK version.
         """
 
     @abstractmethod
     def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:
-        """
-        Compose a list of LogicalCores present on the remote node.
-        If use_first_core is False, the first physical core won't be used.
+        r"""Get the list of :class:`~framework.testbed_model.cpu.LogicalCore`\s on the remote node.
+
+        Args:
+            use_first_core: If :data:`False`, the first physical core won't be used.
+
+        Returns:
+            The logical cores present on the node.
         """
 
     @abstractmethod
     def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:
-        """
-        Kill and cleanup all DPDK apps identified by dpdk_prefix_list. If
-        dpdk_prefix_list is empty, attempt to find running DPDK apps to kill and clean.
+        """Kill and cleanup all DPDK apps.
+
+        Args:
+            dpdk_prefix_list: Kill all apps identified by `dpdk_prefix_list`.
+                If `dpdk_prefix_list` is empty, attempt to find running DPDK apps to kill and clean.
         """
 
     @abstractmethod
     def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
-        """
-        Get the DPDK file prefix that will be used when running DPDK apps.
+        """Make OS-specific modification to the DPDK file prefix.
+
+        Args:
+           dpdk_prefix: The OS-unaware file prefix.
+
+        Returns:
+            The OS-specific file prefix.
         """
 
     @abstractmethod
-    def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None:
-        """
-        Get the node's Hugepage Size, configure the specified amount of hugepages
+    def setup_hugepages(self, hugepage_count: int, force_first_numa: bool) -> None:
+        """Configure hugepages on the node.
+
+        Get the node's Hugepage Size, configure the specified count of hugepages
         if needed and mount the hugepages if needed.
-        If force_first_numa is True, configure hugepages just on the first socket.
+
+        Args:
+            hugepage_count: Configure this many hugepages.
+            force_first_numa:  If :data:`True`, configure hugepages just on the first socket.
         """
 
     @abstractmethod
     def get_compiler_version(self, compiler_name: str) -> str:
-        """
-        Get installed version of compiler used for DPDK
+        """Get installed version of compiler used for DPDK.
+
+        Args:
+            compiler_name: The name of the compiler executable.
+
+        Returns:
+            The compiler's version.
         """
 
     @abstractmethod
     def get_node_info(self) -> NodeInfo:
-        """
-        Collect information about the node
+        """Collect additional information about the node.
+
+        Returns:
+            Node information.
         """
 
     @abstractmethod
     def update_ports(self, ports: list[Port]) -> None:
-        """
-        Get additional information about ports:
-            Logical name (e.g. enp7s0) if applicable
-            Mac address
+        """Get additional information about ports from the operating system and update them.
+
+        The additional information is:
+
+            * Logical name (e.g. ``enp7s0``) if applicable,
+            * Mac address.
+
+        Args:
+            ports: The ports to update.
         """
 
     @abstractmethod
     def configure_port_state(self, port: Port, enable: bool) -> None:
-        """
-        Enable/disable port.
+        """Enable/disable `port` in the operating system.
+
+        Args:
+            port: The port to configure.
+            enable: If :data:`True`, enable the port, otherwise shut it down.
         """
 
     @abstractmethod
@@ -273,12 +408,18 @@  def configure_port_ip_address(
         port: Port,
         delete: bool,
     ) -> None:
-        """
-        Configure (add or delete) an IP address of the input port.
+        """Configure an IP address on `port` in the operating system.
+
+        Args:
+            address: The address to configure.
+            port: The port to configure.
+            delete: If :data:`True`, remove the IP address, otherwise configure it.
         """
 
     @abstractmethod
     def configure_ipv4_forwarding(self, enable: bool) -> None:
-        """
-        Enable IPv4 forwarding in the underlying OS.
+        """Enable IPv4 forwarding in the operating system.
+
+        Args:
+            enable: If :data:`True`, enable the forwarding, otherwise disable it.
         """