<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 Wed, Nov 15, 2023 at 8:11 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">The standard Python tool for generating API documentation, Sphinx,<br>
imports modules one-by-one when generating the documentation. This<br>
requires code changes:<br>
* properly guarding argument parsing in the if __name__ == '__main__'<br>
  block,<br>
* the logger used by DTS runner underwent the same treatment so that it<br>
  doesn't create log files outside of a DTS run,<br>
* however, DTS uses the arguments to construct an object holding global<br>
  variables. The defaults for the global variables needed to be moved<br>
  from argument parsing elsewhere,<br>
* importing the remote_session module from framework resulted in<br>
  circular imports because of one module trying to import another<br>
  module. This is fixed by reorganizing the code,<br>
* some code reorganization was done because the resulting structure<br>
  makes more sense, improving documentation clarity.<br>
<br>
The are some other changes which are documentation related:<br>
* added missing type annotation so they appear in the generated docs,<br>
* reordered arguments in some methods,<br>
* removed superfluous arguments and attributes,<br>
* change private functions/methods/attributes to private and vice-versa.<br>
<br>
The above all appear in the generated documentation and the with them,<br>
the documentation is improved.<br>
<br>
Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech><br>
---<br>
 dts/framework/config/__init__.py              | 10 ++-<br>
 dts/framework/dts.py                          | 33 +++++--<br>
 dts/framework/exception.py                    | 54 +++++-------<br>
 dts/framework/remote_session/__init__.py      | 41 ++++-----<br>
 .../interactive_remote_session.py             |  0<br>
 .../{remote => }/interactive_shell.py         |  0<br>
 .../{remote => }/python_shell.py              |  0<br>
 .../remote_session/remote/__init__.py         | 27 ------<br>
 .../{remote => }/remote_session.py            |  0<br>
 .../{remote => }/ssh_session.py               | 12 +--<br>
 .../{remote => }/testpmd_shell.py             |  0<br>
 dts/framework/settings.py                     | 87 +++++++++++--------<br>
 dts/framework/test_result.py                  |  4 +-<br>
 dts/framework/test_suite.py                   |  7 +-<br>
 dts/framework/testbed_model/__init__.py       | 12 +--<br>
 dts/framework/testbed_model/{hw => }/cpu.py   | 13 +++<br>
 dts/framework/testbed_model/hw/__init__.py    | 27 ------<br>
 .../linux_session.py                          |  6 +-<br>
 dts/framework/testbed_model/node.py           | 25 ++++--<br>
 .../os_session.py                             | 22 ++---<br>
 dts/framework/testbed_model/{hw => }/port.py  |  0<br>
 .../posix_session.py                          |  4 +-<br>
 dts/framework/testbed_model/sut_node.py       |  8 +-<br>
 dts/framework/testbed_model/tg_node.py        | 30 +------<br>
 .../traffic_generator/__init__.py             | 24 +++++<br>
 .../capturing_traffic_generator.py            |  6 +-<br>
 .../{ => traffic_generator}/scapy.py          | 23 ++---<br>
 .../traffic_generator.py                      | 16 +++-<br>
 .../testbed_model/{hw => }/virtual_device.py  |  0<br>
 dts/framework/utils.py                        | 46 +++-------<br>
 dts/main.py                                   |  9 +-<br>
 31 files changed, 258 insertions(+), 288 deletions(-)<br>
 rename dts/framework/remote_session/{remote => }/interactive_remote_session.py (100%)<br>
 rename dts/framework/remote_session/{remote => }/interactive_shell.py (100%)<br>
 rename dts/framework/remote_session/{remote => }/python_shell.py (100%)<br>
 delete mode 100644 dts/framework/remote_session/remote/__init__.py<br>
 rename dts/framework/remote_session/{remote => }/remote_session.py (100%)<br>
 rename dts/framework/remote_session/{remote => }/ssh_session.py (91%)<br>
 rename dts/framework/remote_session/{remote => }/testpmd_shell.py (100%)<br>
 rename dts/framework/testbed_model/{hw => }/cpu.py (95%)<br>
 delete mode 100644 dts/framework/testbed_model/hw/__init__.py<br>
 rename dts/framework/{remote_session => testbed_model}/linux_session.py (97%)<br>
 rename dts/framework/{remote_session => testbed_model}/os_session.py (95%)<br>
 rename dts/framework/testbed_model/{hw => }/port.py (100%)<br>
 rename dts/framework/{remote_session => testbed_model}/posix_session.py (98%)<br>
 create mode 100644 dts/framework/testbed_model/traffic_generator/__init__.py<br>
 rename dts/framework/testbed_model/{ => traffic_generator}/capturing_traffic_generator.py (96%)<br>
 rename dts/framework/testbed_model/{ => traffic_generator}/scapy.py (95%)<br>
 rename dts/framework/testbed_model/{ => traffic_generator}/traffic_generator.py (80%)<br>
 rename dts/framework/testbed_model/{hw => }/virtual_device.py (100%)<br>
<br>
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py<br>
index cb7e00ba34..2044c82611 100644<br>
--- a/dts/framework/config/__init__.py<br>
+++ b/dts/framework/config/__init__.py<br>
@@ -17,6 +17,7 @@<br>
 import warlock  # type: ignore[import]<br>
 import yaml<br>
<br>
+from framework.exception import ConfigurationError<br>
 from framework.settings import SETTINGS<br>
 from framework.utils import StrEnum<br>
<br>
@@ -89,7 +90,7 @@ class TrafficGeneratorConfig:<br>
     traffic_generator_type: TrafficGeneratorType<br>
<br>
     @staticmethod<br>
-    def from_dict(d: dict):<br>
+    def from_dict(d: dict) -> "ScapyTrafficGeneratorConfig":<br>
         # This looks useless now, but is designed to allow expansion to traffic<br>
         # generators that require more configuration later.<br>
         match TrafficGeneratorType(d["type"]):<br>
@@ -97,6 +98,10 @@ def from_dict(d: dict):<br>
                 return ScapyTrafficGeneratorConfig(<br>
                     traffic_generator_type=TrafficGeneratorType.SCAPY<br>
                 )<br>
+            case _:<br>
+                raise ConfigurationError(<br>
+                    f'Unknown traffic generator type "{d["type"]}".'<br>
+                )<br>
<br>
<br>
 @dataclass(slots=True, frozen=True)<br>
@@ -324,6 +329,3 @@ def load_config() -> Configuration:<br>
     config: dict[str, Any] = warlock.model_factory(schema, name="_Config")(config_data)<br>
     config_obj: Configuration = Configuration.from_dict(dict(config))<br>
     return config_obj<br>
-<br>
-<br>
-CONFIGURATION = load_config()<br>
diff --git a/dts/framework/dts.py b/dts/framework/dts.py<br>
index f773f0c38d..4c7fb0c40a 100644<br>
--- a/dts/framework/dts.py<br>
+++ b/dts/framework/dts.py<br>
@@ -6,19 +6,19 @@<br>
 import sys<br>
<br>
 from .config import (<br>
-    CONFIGURATION,<br>
     BuildTargetConfiguration,<br>
     ExecutionConfiguration,<br>
     TestSuiteConfig,<br>
+    load_config,<br>
 )<br>
 from .exception import BlockingTestSuiteError<br>
 from .logger import DTSLOG, getLogger<br>
 from .test_result import BuildTargetResult, DTSResult, ExecutionResult, Result<br>
 from .test_suite import get_test_suites<br>
 from .testbed_model import SutNode, TGNode<br>
-from .utils import check_dts_python_version<br>
<br>
-dts_logger: DTSLOG = getLogger("DTSRunner")<br>
+# dummy defaults to satisfy linters<br>
+dts_logger: DTSLOG = None  # type: ignore[assignment]<br>
 result: DTSResult = DTSResult(dts_logger)<br>
<br>
<br>
@@ -30,14 +30,18 @@ def run_all() -> None:<br>
     global dts_logger<br>
     global result<br>
<br>
+    # create a regular DTS logger and create a new result with it<br>
+    dts_logger = getLogger("DTSRunner")<br>
+    result = DTSResult(dts_logger)<br>
+<br>
     # check the python version of the server that run dts<br>
-    check_dts_python_version()<br>
+    _check_dts_python_version()<br>
<br>
     sut_nodes: dict[str, SutNode] = {}<br>
     tg_nodes: dict[str, TGNode] = {}<br>
     try:<br>
         # for all Execution sections<br>
-        for execution in CONFIGURATION.executions:<br>
+        for execution in load_config().executions:<br>
             sut_node = sut_nodes.get(<a href="http://execution.system_under_test_node.name" rel="noreferrer" target="_blank">execution.system_under_test_node.name</a>)<br>
             tg_node = tg_nodes.get(<a href="http://execution.traffic_generator_node.name" rel="noreferrer" target="_blank">execution.traffic_generator_node.name</a>)<br>
<br>
@@ -82,6 +86,25 @@ def run_all() -> None:<br>
     _exit_dts()<br>
<br>
<br>
+def _check_dts_python_version() -> None:<br>
+    def RED(text: str) -> str:<br>
+        return f"\u001B[31;1m{str(text)}\u001B[0m"<br>
+<br>
+    if sys.version_info.major < 3 or (<br>
+        sys.version_info.major == 3 and sys.version_info.minor < 10<br>
+    ):<br>
+        print(<br>
+            RED(<br>
+                (<br>
+                    "WARNING: DTS execution node's python version is lower than"<br>
+                    "python 3.10, is deprecated and will not work in future releases."<br>
+                )<br>
+            ),<br>
+            file=sys.stderr,<br>
+        )<br>
+        print(RED("Please use Python >= 3.10 instead"), file=sys.stderr)<br>
+<br>
+<br>
 def _run_execution(<br>
     sut_node: SutNode,<br>
     tg_node: TGNode,<br>
diff --git a/dts/framework/exception.py b/dts/framework/exception.py<br>
index 001a5a5496..7489c03570 100644<br>
--- a/dts/framework/exception.py<br>
+++ b/dts/framework/exception.py<br>
@@ -42,19 +42,14 @@ class SSHTimeoutError(DTSError):<br>
     Command execution timeout.<br>
     """<br>
<br>
-    command: str<br>
-    output: str<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR<br>
+    _command: str<br>
<br>
-    def __init__(self, command: str, output: str):<br>
-        self.command = command<br>
-        self.output = output<br>
+    def __init__(self, command: str):<br>
+        self._command = command<br>
<br>
     def __str__(self) -> str:<br>
-        return f"TIMEOUT on {self.command}"<br>
-<br>
-    def get_output(self) -> str:<br>
-        return self.output<br>
+        return f"TIMEOUT on {self._command}"<br>
<br>
<br>
 class SSHConnectionError(DTSError):<br>
@@ -62,18 +57,18 @@ class SSHConnectionError(DTSError):<br>
     SSH connection error.<br>
     """<br>
<br>
-    host: str<br>
-    errors: list[str]<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR<br>
+    _host: str<br>
+    _errors: list[str]<br>
<br>
     def __init__(self, host: str, errors: list[str] | None = None):<br>
-        self.host = host<br>
-        self.errors = [] if errors is None else errors<br>
+        self._host = host<br>
+        self._errors = [] if errors is None else errors<br>
<br>
     def __str__(self) -> str:<br>
-        message = f"Error trying to connect with {self.host}."<br>
-        if self.errors:<br>
-            message += f" Errors encountered while retrying: {', '.join(self.errors)}"<br>
+        message = f"Error trying to connect with {self._host}."<br>
+        if self._errors:<br>
+            message += f" Errors encountered while retrying: {', '.join(self._errors)}"<br>
<br>
         return message<br>
<br>
@@ -84,14 +79,14 @@ class SSHSessionDeadError(DTSError):<br>
     It can no longer be used.<br>
     """<br>
<br>
-    host: str<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR<br>
+    _host: str<br>
<br>
     def __init__(self, host: str):<br>
-        self.host = host<br>
+        self._host = host<br>
<br>
     def __str__(self) -> str:<br>
-        return f"SSH session with {self.host} has died"<br>
+        return f"SSH session with {self._host} has died"<br>
<br>
<br>
 class ConfigurationError(DTSError):<br>
@@ -107,18 +102,18 @@ class RemoteCommandExecutionError(DTSError):<br>
     Raised when a command executed on a Node returns a non-zero exit status.<br>
     """<br>
<br>
-    command: str<br>
-    command_return_code: int<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.REMOTE_CMD_EXEC_ERR<br>
+    command: str<br>
+    _command_return_code: int<br>
<br>
     def __init__(self, command: str, command_return_code: int):<br>
         self.command = command<br>
-        self.command_return_code = command_return_code<br>
+        self._command_return_code = command_return_code<br>
<br>
     def __str__(self) -> str:<br>
         return (<br>
             f"Command {self.command} returned a non-zero exit code: "<br>
-            f"{self.command_return_code}"<br>
+            f"{self._command_return_code}"<br>
         )<br>
<br>
<br>
@@ -143,22 +138,15 @@ class TestCaseVerifyError(DTSError):<br>
     Used in test cases to verify the expected behavior.<br>
     """<br>
<br>
-    value: str<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.TESTCASE_VERIFY_ERR<br>
<br>
-    def __init__(self, value: str):<br>
-        self.value = value<br>
-<br>
-    def __str__(self) -> str:<br>
-        return repr(self.value)<br>
-<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Does this change mean we are no longer providing descriptions for what failing the verification means? I guess there isn't really harm in removing that functionality, but I'm not sure I see the value in removing the extra information either.<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>
 class BlockingTestSuiteError(DTSError):<br>
-    suite_name: str<br>
     severity: ClassVar[ErrorSeverity] = ErrorSeverity.BLOCKING_TESTSUITE_ERR<br>
+    _suite_name: str<br>
<br>
     def __init__(self, suite_name: str) -> None:<br>
-        self.suite_name = suite_name<br>
+        self._suite_name = suite_name<br>
<br>
     def __str__(self) -> str:<br>
-        return f"Blocking suite {self.suite_name} failed."<br>
+        return f"Blocking suite {self._suite_name} failed."<br>
diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py<br>
index 00b6d1f03a..5e7ddb2b05 100644<br>
--- a/dts/framework/remote_session/__init__.py<br>
+++ b/dts/framework/remote_session/__init__.py<br>
@@ -12,29 +12,24 @@<br>
<br>
 # pylama:ignore=W0611<br>
<br>
-from framework.config import OS, NodeConfiguration<br>
-from framework.exception import ConfigurationError<br>
+from framework.config import NodeConfiguration<br>
 from framework.logger import DTSLOG<br>
<br>
-from .linux_session import LinuxSession<br>
-from .os_session import InteractiveShellType, OSSession<br>
-from .remote import (<br>
-    CommandResult,<br>
-    InteractiveRemoteSession,<br>
-    InteractiveShell,<br>
-    PythonShell,<br>
-    RemoteSession,<br>
-    SSHSession,<br>
-    TestPmdDevice,<br>
-    TestPmdShell,<br>
-)<br>
-<br>
-<br>
-def create_session(<br>
+from .interactive_remote_session import InteractiveRemoteSession<br>
+from .interactive_shell import InteractiveShell<br>
+from .python_shell import PythonShell<br>
+from .remote_session import CommandResult, RemoteSession<br>
+from .ssh_session import SSHSession<br>
+from .testpmd_shell import TestPmdShell<br>
+<br>
+<br>
+def create_remote_session(<br>
     node_config: NodeConfiguration, name: str, logger: DTSLOG<br>
-) -> OSSession:<br>
-    match node_config.os:<br>
-        case OS.linux:<br>
-            return LinuxSession(node_config, name, logger)<br>
-        case _:<br>
-            raise ConfigurationError(f"Unsupported OS {node_config.os}")<br>
+) -> RemoteSession:<br>
+    return SSHSession(node_config, name, logger)<br>
+<br>
+<br>
+def create_interactive_session(<br>
+    node_config: NodeConfiguration, logger: DTSLOG<br>
+) -> InteractiveRemoteSession:<br>
+    return InteractiveRemoteSession(node_config, logger)<br>
diff --git a/dts/framework/remote_session/remote/interactive_remote_session.py b/dts/framework/remote_session/interactive_remote_session.py<br>
similarity index 100%<br>
rename from dts/framework/remote_session/remote/interactive_remote_session.py<br>
rename to dts/framework/remote_session/interactive_remote_session.py<br>
diff --git a/dts/framework/remote_session/remote/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py<br>
similarity index 100%<br>
rename from dts/framework/remote_session/remote/interactive_shell.py<br>
rename to dts/framework/remote_session/interactive_shell.py<br>
diff --git a/dts/framework/remote_session/remote/python_shell.py b/dts/framework/remote_session/python_shell.py<br>
similarity index 100%<br>
rename from dts/framework/remote_session/remote/python_shell.py<br>
rename to dts/framework/remote_session/python_shell.py<br>
diff --git a/dts/framework/remote_session/remote/__init__.py b/dts/framework/remote_session/remote/__init__.py<br>
deleted file mode 100644<br>
index 06403691a5..0000000000<br>
--- a/dts/framework/remote_session/remote/__init__.py<br>
+++ /dev/null<br>
@@ -1,27 +0,0 @@<br>
-# SPDX-License-Identifier: BSD-3-Clause<br>
-# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
-# Copyright(c) 2023 University of New Hampshire<br>
-<br>
-# pylama:ignore=W0611<br>
-<br>
-from framework.config import NodeConfiguration<br>
-from framework.logger import DTSLOG<br>
-<br>
-from .interactive_remote_session import InteractiveRemoteSession<br>
-from .interactive_shell import InteractiveShell<br>
-from .python_shell import PythonShell<br>
-from .remote_session import CommandResult, RemoteSession<br>
-from .ssh_session import SSHSession<br>
-from .testpmd_shell import TestPmdDevice, TestPmdShell<br>
-<br>
-<br>
-def create_remote_session(<br>
-    node_config: NodeConfiguration, name: str, logger: DTSLOG<br>
-) -> RemoteSession:<br>
-    return SSHSession(node_config, name, logger)<br>
-<br>
-<br>
-def create_interactive_session(<br>
-    node_config: NodeConfiguration, logger: DTSLOG<br>
-) -> InteractiveRemoteSession:<br>
-    return InteractiveRemoteSession(node_config, logger)<br>
diff --git a/dts/framework/remote_session/remote/remote_session.py b/dts/framework/remote_session/remote_session.py<br>
similarity index 100%<br>
rename from dts/framework/remote_session/remote/remote_session.py<br>
rename to dts/framework/remote_session/remote_session.py<br>
diff --git a/dts/framework/remote_session/remote/ssh_session.py b/dts/framework/remote_session/ssh_session.py<br>
similarity index 91%<br>
rename from dts/framework/remote_session/remote/ssh_session.py<br>
rename to dts/framework/remote_session/ssh_session.py<br>
index 8d127f1601..cee11d14d6 100644<br>
--- a/dts/framework/remote_session/remote/ssh_session.py<br>
+++ b/dts/framework/remote_session/ssh_session.py<br>
@@ -18,9 +18,7 @@<br>
     SSHException,<br>
 )<br>
<br>
-from framework.config import NodeConfiguration<br>
 from framework.exception import SSHConnectionError, SSHSessionDeadError, SSHTimeoutError<br>
-from framework.logger import DTSLOG<br>
<br>
 from .remote_session import CommandResult, RemoteSession<br>
<br>
@@ -45,14 +43,6 @@ class SSHSession(RemoteSession):<br>
<br>
     session: Connection<br>
<br>
-    def __init__(<br>
-        self,<br>
-        node_config: NodeConfiguration,<br>
-        session_name: str,<br>
-        logger: DTSLOG,<br>
-    ):<br>
-        super(SSHSession, self).__init__(node_config, session_name, logger)<br>
-<br>
     def _connect(self) -> None:<br>
         errors = []<br>
         retry_attempts = 10<br>
@@ -117,7 +107,7 @@ def _send_command(<br>
<br>
         except CommandTimedOut as e:<br>
             self._logger.exception(e)<br>
-            raise SSHTimeoutError(command, e.result.stderr) from e<br>
+            raise SSHTimeoutError(command) from e<br>
<br>
         return CommandResult(<br>
             <a href="http://self.name" rel="noreferrer" target="_blank">self.name</a>, command, output.stdout, output.stderr, output.return_code<br>
diff --git a/dts/framework/remote_session/remote/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py<br>
similarity index 100%<br>
rename from dts/framework/remote_session/remote/testpmd_shell.py<br>
rename to dts/framework/remote_session/testpmd_shell.py<br>
diff --git a/dts/framework/settings.py b/dts/framework/settings.py<br>
index cfa39d011b..7f5841d073 100644<br>
--- a/dts/framework/settings.py<br>
+++ b/dts/framework/settings.py<br>
@@ -6,7 +6,7 @@<br>
 import argparse<br>
 import os<br>
 from collections.abc import Callable, Iterable, Sequence<br>
-from dataclasses import dataclass<br>
+from dataclasses import dataclass, field<br>
 from pathlib import Path<br>
 from typing import Any, TypeVar<br>
<br>
@@ -22,8 +22,8 @@ def __init__(<br>
             option_strings: Sequence[str],<br>
             dest: str,<br>
             nargs: str | int | None = None,<br>
-            const: str | None = None,<br>
-            default: str = None,<br>
+            const: bool | None = None,<br>
+            default: Any = None,<br>
             type: Callable[[str], _T | argparse.FileType | None] = None,<br>
             choices: Iterable[_T] | None = None,<br>
             required: bool = False,<br>
@@ -32,6 +32,12 @@ def __init__(<br>
         ) -> None:<br>
             env_var_value = os.environ.get(env_var)<br>
             default = env_var_value or default<br>
+            if const is not None:<br>
+                nargs = 0<br>
+                default = const if env_var_value else default<br>
+                type = None<br>
+                choices = None<br>
+                metavar = None<br>
             super(_EnvironmentArgument, self).__init__(<br>
                 option_strings,<br>
                 dest,<br>
@@ -52,22 +58,28 @@ def __call__(<br>
             values: Any,<br>
             option_string: str = None,<br>
         ) -> None:<br>
-            setattr(namespace, self.dest, values)<br>
+            if self.const is not None:<br>
+                setattr(namespace, self.dest, self.const)<br>
+            else:<br>
+                setattr(namespace, self.dest, values)<br>
<br>
     return _EnvironmentArgument<br>
<br>
<br>
-@dataclass(slots=True, frozen=True)<br>
-class _Settings:<br>
-    config_file_path: str<br>
-    output_dir: str<br>
-    timeout: float<br>
-    verbose: bool<br>
-    skip_setup: bool<br>
-    dpdk_tarball_path: Path<br>
-    compile_timeout: float<br>
-    test_cases: list<br>
-    re_run: int<br>
+@dataclass(slots=True)<br>
+class Settings:<br>
+    config_file_path: Path = Path(__file__).parent.parent.joinpath("conf.yaml")<br>
+    output_dir: str = "output"<br>
+    timeout: float = 15<br>
+    verbose: bool = False<br>
+    skip_setup: bool = False<br>
+    dpdk_tarball_path: Path | str = "dpdk.tar.xz"<br>
+    compile_timeout: float = 1200<br>
+    test_cases: list[str] = field(default_factory=list)<br>
+    re_run: int = 0<br>
+<br>
+<br>
+SETTINGS: Settings = Settings()<br>
<br>
<br>
 def _get_parser() -> argparse.ArgumentParser:<br>
@@ -81,7 +93,8 @@ def _get_parser() -> argparse.ArgumentParser:<br>
     parser.add_argument(<br>
         "--config-file",<br>
         action=_env_arg("DTS_CFG_FILE"),<br>
-        default="conf.yaml",<br>
+        default=SETTINGS.config_file_path,<br>
+        type=Path,<br>
         help="[DTS_CFG_FILE] configuration file that describes the test cases, SUTs "<br>
         "and targets.",<br>
     )<br>
@@ -90,7 +103,7 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "--output-dir",<br>
         "--output",<br>
         action=_env_arg("DTS_OUTPUT_DIR"),<br>
-        default="output",<br>
+        default=SETTINGS.output_dir,<br>
         help="[DTS_OUTPUT_DIR] Output directory where dts logs and results are saved.",<br>
     )<br>
<br>
@@ -98,7 +111,7 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "-t",<br>
         "--timeout",<br>
         action=_env_arg("DTS_TIMEOUT"),<br>
-        default=15,<br>
+        default=SETTINGS.timeout,<br>
         type=float,<br>
         help="[DTS_TIMEOUT] The default timeout for all DTS operations except for "<br>
         "compiling DPDK.",<br>
@@ -108,8 +121,9 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "-v",<br>
         "--verbose",<br>
         action=_env_arg("DTS_VERBOSE"),<br>
-        default="N",<br>
-        help="[DTS_VERBOSE] Set to 'Y' to enable verbose output, logging all messages "<br>
+        default=SETTINGS.verbose,<br>
+        const=True,<br>
+        help="[DTS_VERBOSE] Specify to enable verbose output, logging all messages "<br>
         "to the console.",<br>
     )<br>
<br>
@@ -117,8 +131,8 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "-s",<br>
         "--skip-setup",<br>
         action=_env_arg("DTS_SKIP_SETUP"),<br>
-        default="N",<br>
-        help="[DTS_SKIP_SETUP] Set to 'Y' to skip all setup steps on SUT and TG nodes.",<br>
+        const=True,<br>
+        help="[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes.",<br>
     )<br>
<br>
     parser.add_argument(<br>
@@ -126,7 +140,7 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "--snapshot",<br>
         "--git-ref",<br>
         action=_env_arg("DTS_DPDK_TARBALL"),<br>
-        default="dpdk.tar.xz",<br>
+        default=SETTINGS.dpdk_tarball_path,<br>
         type=Path,<br>
         help="[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, "<br>
         "tag ID or tree ID to test. To test local changes, first commit them, "<br>
@@ -136,7 +150,7 @@ def _get_parser() -> argparse.ArgumentParser:<br>
     parser.add_argument(<br>
         "--compile-timeout",<br>
         action=_env_arg("DTS_COMPILE_TIMEOUT"),<br>
-        default=1200,<br>
+        default=SETTINGS.compile_timeout,<br>
         type=float,<br>
         help="[DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK.",<br>
     )<br>
@@ -153,7 +167,7 @@ def _get_parser() -> argparse.ArgumentParser:<br>
         "--re-run",<br>
         "--re_run",<br>
         action=_env_arg("DTS_RERUN"),<br>
-        default=0,<br>
+        default=SETTINGS.re_run,<br>
         type=int,<br>
         help="[DTS_RERUN] Re-run each test case the specified amount of times "<br>
         "if a test failure occurs",<br>
@@ -162,23 +176,22 @@ def _get_parser() -> argparse.ArgumentParser:<br>
     return parser<br>
<br>
<br>
-def _get_settings() -> _Settings:<br>
+def get_settings() -> Settings:<br>
     parsed_args = _get_parser().parse_args()<br>
-    return _Settings(<br>
+    return Settings(<br>
         config_file_path=parsed_args.config_file,<br>
         output_dir=parsed_args.output_dir,<br>
         timeout=parsed_args.timeout,<br>
-        verbose=(parsed_args.verbose == "Y"),<br>
-        skip_setup=(parsed_args.skip_setup == "Y"),<br>
+        verbose=parsed_args.verbose,<br>
+        skip_setup=parsed_args.skip_setup,<br>
         dpdk_tarball_path=Path(<br>
-            DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir)<br>
-        )<br>
-        if not os.path.exists(parsed_args.tarball)<br>
-        else Path(parsed_args.tarball),<br>
+            Path(DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir))<br>
+            if not os.path.exists(parsed_args.tarball)<br>
+            else Path(parsed_args.tarball)<br>
+        ),<br>
         compile_timeout=parsed_args.compile_timeout,<br>
-        test_cases=parsed_args.test_cases.split(",") if parsed_args.test_cases else [],<br>
+        test_cases=(<br>
+            parsed_args.test_cases.split(",") if parsed_args.test_cases else []<br>
+        ),<br>
         re_run=parsed_args.re_run,<br>
     )<br>
-<br>
-<br>
-SETTINGS: _Settings = _get_settings()<br>
diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py<br>
index f0fbe80f6f..603e18872c 100644<br>
--- a/dts/framework/test_result.py<br>
+++ b/dts/framework/test_result.py<br>
@@ -254,7 +254,7 @@ def add_build_target(<br>
         self._inner_results.append(build_target_result)<br>
         return build_target_result<br>
<br>
-    def add_sut_info(self, sut_info: NodeInfo):<br>
+    def add_sut_info(self, sut_info: NodeInfo) -> None:<br>
         self.sut_os_name = sut_info.os_name<br>
         self.sut_os_version = sut_info.os_version<br>
         self.sut_kernel_version = sut_info.kernel_version<br>
@@ -297,7 +297,7 @@ def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:<br>
         self._inner_results.append(execution_result)<br>
         return execution_result<br>
<br>
-    def add_error(self, error) -> None:<br>
+    def add_error(self, error: Exception) -> None:<br>
         self._errors.append(error)<br>
<br>
     def process(self) -> None:<br>
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py<br>
index 3b890c0451..d53553bf34 100644<br>
--- a/dts/framework/test_suite.py<br>
+++ b/dts/framework/test_suite.py<br>
@@ -11,7 +11,7 @@<br>
 import re<br>
 from ipaddress import IPv4Interface, IPv6Interface, ip_interface<br>
 from types import MethodType<br>
-from typing import Union<br>
+from typing import Any, Union<br>
<br>
 from scapy.layers.inet import IP  # type: ignore[import]<br>
 from scapy.layers.l2 import Ether  # type: ignore[import]<br>
@@ -26,8 +26,7 @@<br>
 from .logger import DTSLOG, getLogger<br>
 from .settings import SETTINGS<br>
 from .test_result import BuildTargetResult, Result, TestCaseResult, TestSuiteResult<br>
-from .testbed_model import SutNode, TGNode<br>
-from .testbed_model.hw.port import Port, PortLink<br>
+from .testbed_model import Port, PortLink, SutNode, TGNode<br>
 from .utils import get_packet_summaries<br>
<br>
<br>
@@ -453,7 +452,7 @@ def _execute_test_case(<br>
<br>
<br>
 def get_test_suites(testsuite_module_path: str) -> list[type[TestSuite]]:<br>
-    def is_test_suite(object) -> bool:<br>
+    def is_test_suite(object: Any) -> bool:<br>
         try:<br>
             if issubclass(object, TestSuite) and object is not TestSuite:<br>
                 return True<br>
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py<br>
index 5cbb859e47..8ced05653b 100644<br>
--- a/dts/framework/testbed_model/__init__.py<br>
+++ b/dts/framework/testbed_model/__init__.py<br>
@@ -9,15 +9,9 @@<br>
<br>
 # pylama:ignore=W0611<br>
<br>
-from .hw import (<br>
-    LogicalCore,<br>
-    LogicalCoreCount,<br>
-    LogicalCoreCountFilter,<br>
-    LogicalCoreList,<br>
-    LogicalCoreListFilter,<br>
-    VirtualDevice,<br>
-    lcore_filter,<br>
-)<br>
+from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList<br>
 from .node import Node<br>
+from .port import Port, PortLink<br>
 from .sut_node import SutNode<br>
 from .tg_node import TGNode<br>
+from .virtual_device import VirtualDevice<br>
diff --git a/dts/framework/testbed_model/hw/cpu.py b/dts/framework/testbed_model/cpu.py<br>
similarity index 95%<br>
rename from dts/framework/testbed_model/hw/cpu.py<br>
rename to dts/framework/testbed_model/cpu.py<br>
index d1918a12dc..8fe785dfe4 100644<br>
--- a/dts/framework/testbed_model/hw/cpu.py<br>
+++ b/dts/framework/testbed_model/cpu.py<br>
@@ -272,3 +272,16 @@ def filter(self) -> list[LogicalCore]:<br>
             )<br>
<br>
         return filtered_lcores<br>
+<br>
+<br>
+def lcore_filter(<br>
+    core_list: list[LogicalCore],<br>
+    filter_specifier: LogicalCoreCount | LogicalCoreList,<br>
+    ascending: bool,<br>
+) -> LogicalCoreFilter:<br>
+    if isinstance(filter_specifier, LogicalCoreList):<br>
+        return LogicalCoreListFilter(core_list, filter_specifier, ascending)<br>
+    elif isinstance(filter_specifier, LogicalCoreCount):<br>
+        return LogicalCoreCountFilter(core_list, filter_specifier, ascending)<br>
+    else:<br>
+        raise ValueError(f"Unsupported filter r{filter_specifier}")<br>
diff --git a/dts/framework/testbed_model/hw/__init__.py b/dts/framework/testbed_model/hw/__init__.py<br>
deleted file mode 100644<br>
index 88ccac0b0e..0000000000<br>
--- a/dts/framework/testbed_model/hw/__init__.py<br>
+++ /dev/null<br>
@@ -1,27 +0,0 @@<br>
-# SPDX-License-Identifier: BSD-3-Clause<br>
-# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
-<br>
-# pylama:ignore=W0611<br>
-<br>
-from .cpu import (<br>
-    LogicalCore,<br>
-    LogicalCoreCount,<br>
-    LogicalCoreCountFilter,<br>
-    LogicalCoreFilter,<br>
-    LogicalCoreList,<br>
-    LogicalCoreListFilter,<br>
-)<br>
-from .virtual_device import VirtualDevice<br>
-<br>
-<br>
-def lcore_filter(<br>
-    core_list: list[LogicalCore],<br>
-    filter_specifier: LogicalCoreCount | LogicalCoreList,<br>
-    ascending: bool,<br>
-) -> LogicalCoreFilter:<br>
-    if isinstance(filter_specifier, LogicalCoreList):<br>
-        return LogicalCoreListFilter(core_list, filter_specifier, ascending)<br>
-    elif isinstance(filter_specifier, LogicalCoreCount):<br>
-        return LogicalCoreCountFilter(core_list, filter_specifier, ascending)<br>
-    else:<br>
-        raise ValueError(f"Unsupported filter r{filter_specifier}")<br>
diff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/testbed_model/linux_session.py<br>
similarity index 97%<br>
rename from dts/framework/remote_session/linux_session.py<br>
rename to dts/framework/testbed_model/linux_session.py<br>
index a3f1a6bf3b..f472bb8f0f 100644<br>
--- a/dts/framework/remote_session/linux_session.py<br>
+++ b/dts/framework/testbed_model/linux_session.py<br>
@@ -9,10 +9,10 @@<br>
 from typing_extensions import NotRequired<br>
<br>
 from framework.exception import RemoteCommandExecutionError<br>
-from framework.testbed_model import LogicalCore<br>
-from framework.testbed_model.hw.port import Port<br>
 from framework.utils import expand_range<br>
<br>
+from .cpu import LogicalCore<br>
+from .port import Port<br>
 from .posix_session import PosixSession<br>
<br>
<br>
@@ -64,7 +64,7 @@ def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:<br>
             lcores.append(LogicalCore(lcore, core, socket, node))<br>
         return lcores<br>
<br>
-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:<br>
+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:<br>
         return dpdk_prefix<br>
<br>
     def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None:<br>
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py<br>
index fc01e0bf8e..fa5b143cdd 100644<br>
--- a/dts/framework/testbed_model/node.py<br>
+++ b/dts/framework/testbed_model/node.py<br>
@@ -12,23 +12,26 @@<br>
 from typing import Any, Callable, Type, Union<br>
<br>
 from framework.config import (<br>
+    OS,<br>
     BuildTargetConfiguration,<br>
     ExecutionConfiguration,<br>
     NodeConfiguration,<br>
 )<br>
+from framework.exception import ConfigurationError<br>
 from framework.logger import DTSLOG, getLogger<br>
-from framework.remote_session import InteractiveShellType, OSSession, create_session<br>
 from framework.settings import SETTINGS<br>
<br>
-from .hw import (<br>
+from .cpu import (<br>
     LogicalCore,<br>
     LogicalCoreCount,<br>
     LogicalCoreList,<br>
     LogicalCoreListFilter,<br>
-    VirtualDevice,<br>
     lcore_filter,<br>
 )<br>
-from .hw.port import Port<br>
+from .linux_session import LinuxSession<br>
+from .os_session import InteractiveShellType, OSSession<br>
+from .port import Port<br>
+from .virtual_device import VirtualDevice<br>
<br>
<br>
 class Node(ABC):<br>
@@ -172,9 +175,9 @@ def create_interactive_shell(<br>
<br>
         return self.main_session.create_interactive_shell(<br>
             shell_cls,<br>
-            app_args,<br>
             timeout,<br>
             privileged,<br>
+            app_args,<br>
         )<br>
<br>
     def filter_lcores(<br>
@@ -205,7 +208,7 @@ def _get_remote_cpus(self) -> None:<br>
         self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>("Getting CPU information.")<br>
         self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)<br>
<br>
-    def _setup_hugepages(self):<br>
+    def _setup_hugepages(self) -> None:<br>
         """<br>
         Setup hugepages on the Node. Different architectures can supply different<br>
         amounts of memory for hugepages and numa-based hugepage allocation may need<br>
@@ -249,3 +252,13 @@ def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:<br>
             return lambda *args: None<br>
         else:<br>
             return func<br>
+<br>
+<br>
+def create_session(<br>
+    node_config: NodeConfiguration, name: str, logger: DTSLOG<br>
+) -> OSSession:<br>
+    match node_config.os:<br>
+        case OS.linux:<br>
+            return LinuxSession(node_config, name, logger)<br>
+        case _:<br>
+            raise ConfigurationError(f"Unsupported OS {node_config.os}")<br>
diff --git a/dts/framework/remote_session/os_session.py b/dts/framework/testbed_model/os_session.py<br>
similarity index 95%<br>
rename from dts/framework/remote_session/os_session.py<br>
rename to dts/framework/testbed_model/os_session.py<br>
index 8a709eac1c..76e595a518 100644<br>
--- a/dts/framework/remote_session/os_session.py<br>
+++ b/dts/framework/testbed_model/os_session.py<br>
@@ -10,19 +10,19 @@<br>
<br>
 from framework.config import Architecture, NodeConfiguration, NodeInfo<br>
 from framework.logger import DTSLOG<br>
-from framework.remote_session.remote import InteractiveShell<br>
-from framework.settings import SETTINGS<br>
-from framework.testbed_model import LogicalCore<br>
-from framework.testbed_model.hw.port import Port<br>
-from framework.utils import MesonArgs<br>
-<br>
-from .remote import (<br>
+from framework.remote_session import (<br>
     CommandResult,<br>
     InteractiveRemoteSession,<br>
+    InteractiveShell,<br>
     RemoteSession,<br>
     create_interactive_session,<br>
     create_remote_session,<br>
 )<br>
+from framework.settings import SETTINGS<br>
+from framework.utils import MesonArgs<br>
+<br>
+from .cpu import LogicalCore<br>
+from .port import Port<br>
<br>
 InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)<br>
<br>
@@ -85,9 +85,9 @@ def send_command(<br>
     def create_interactive_shell(<br>
         self,<br>
         shell_cls: Type[InteractiveShellType],<br>
-        eal_parameters: str,<br>
         timeout: float,<br>
         privileged: bool,<br>
+        app_args: str,<br>
     ) -> InteractiveShellType:<br>
         """<br>
         See "create_interactive_shell" in SutNode<br>
@@ -96,7 +96,7 @@ def create_interactive_shell(<br>
             self.interactive_session.session,<br>
             self._logger,<br>
             self._get_privileged_command if privileged else None,<br>
-            eal_parameters,<br>
+            app_args,<br>
             timeout,<br>
         )<br>
<br>
@@ -113,7 +113,7 @@ def _get_privileged_command(command: str) -> str:<br>
         """<br>
<br>
     @abstractmethod<br>
-    def guess_dpdk_remote_dir(self, remote_dir) -> PurePath:<br>
+    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:<br>
         """<br>
         Try to find DPDK remote dir in remote_dir.<br>
         """<br>
@@ -227,7 +227,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:<br>
         """<br>
<br>
     @abstractmethod<br>
-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:<br>
+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:<br>
         """<br>
         Get the DPDK file prefix that will be used when running DPDK apps.<br>
         """<br>
diff --git a/dts/framework/testbed_model/hw/port.py b/dts/framework/testbed_model/port.py<br>
similarity index 100%<br>
rename from dts/framework/testbed_model/hw/port.py<br>
rename to dts/framework/testbed_model/port.py<br>
diff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/testbed_model/posix_session.py<br>
similarity index 98%<br>
rename from dts/framework/remote_session/posix_session.py<br>
rename to dts/framework/testbed_model/posix_session.py<br>
index 5da0516e05..1d1d5b1b26 100644<br>
--- a/dts/framework/remote_session/posix_session.py<br>
+++ b/dts/framework/testbed_model/posix_session.py<br>
@@ -32,7 +32,7 @@ def combine_short_options(**opts: bool) -> str:<br>
<br>
         return ret_opts<br>
<br>
-    def guess_dpdk_remote_dir(self, remote_dir) -> PurePosixPath:<br>
+    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePosixPath:<br>
         remote_guess = self.join_remote_path(remote_dir, "dpdk-*")<br>
         result = self.send_command(f"ls -d {remote_guess} | tail -1")<br>
         return PurePosixPath(result.stdout)<br>
@@ -219,7 +219,7 @@ def _remove_dpdk_runtime_dirs(<br>
         for dpdk_runtime_dir in dpdk_runtime_dirs:<br>
             self.remove_remote_dir(dpdk_runtime_dir)<br>
<br>
-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:<br>
+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:<br>
         return ""<br>
<br>
     def get_compiler_version(self, compiler_name: str) -> str:<br>
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py<br>
index 4161d3a4d5..17deea06e2 100644<br>
--- a/dts/framework/testbed_model/sut_node.py<br>
+++ b/dts/framework/testbed_model/sut_node.py<br>
@@ -15,12 +15,14 @@<br>
     NodeInfo,<br>
     SutNodeConfiguration,<br>
 )<br>
-from framework.remote_session import CommandResult, InteractiveShellType, OSSession<br>
+from framework.remote_session import CommandResult<br>
 from framework.settings import SETTINGS<br>
 from framework.utils import MesonArgs<br>
<br>
-from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice<br>
+from .cpu import LogicalCoreCount, LogicalCoreList<br>
 from .node import Node<br>
+from .os_session import InteractiveShellType, OSSession<br>
+from .virtual_device import VirtualDevice<br>
<br>
<br>
 class EalParameters(object):<br>
@@ -307,7 +309,7 @@ def create_eal_parameters(<br>
         prefix: str = "dpdk",<br>
         append_prefix_timestamp: bool = True,<br>
         no_pci: bool = False,<br>
-        vdevs: list[VirtualDevice] = None,<br>
+        vdevs: list[VirtualDevice] | None = None,<br>
         other_eal_param: str = "",<br>
     ) -> "EalParameters":<br>
         """<br>
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py<br>
index 27025cfa31..166eb8430e 100644<br>
--- a/dts/framework/testbed_model/tg_node.py<br>
+++ b/dts/framework/testbed_model/tg_node.py<br>
@@ -16,16 +16,11 @@<br>
<br>
 from scapy.packet import Packet  # type: ignore[import]<br>
<br>
-from framework.config import (<br>
-    ScapyTrafficGeneratorConfig,<br>
-    TGNodeConfiguration,<br>
-    TrafficGeneratorType,<br>
-)<br>
-from framework.exception import ConfigurationError<br>
-<br>
-from .capturing_traffic_generator import CapturingTrafficGenerator<br>
-from .hw.port import Port<br>
+from framework.config import TGNodeConfiguration<br>
+<br>
 from .node import Node<br>
+from .port import Port<br>
+from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator<br>
<br>
<br>
 class TGNode(Node):<br>
@@ -80,20 +75,3 @@ def close(self) -> None:<br>
         """Free all resources used by the node"""<br>
         self.traffic_generator.close()<br>
         super(TGNode, self).close()<br>
-<br>
-<br>
-def create_traffic_generator(<br>
-    tg_node: TGNode, traffic_generator_config: ScapyTrafficGeneratorConfig<br>
-) -> CapturingTrafficGenerator:<br>
-    """A factory function for creating traffic generator object from user config."""<br>
-<br>
-    from .scapy import ScapyTrafficGenerator<br>
-<br>
-    match traffic_generator_config.traffic_generator_type:<br>
-        case TrafficGeneratorType.SCAPY:<br>
-            return ScapyTrafficGenerator(tg_node, traffic_generator_config)<br>
-        case _:<br>
-            raise ConfigurationError(<br>
-                "Unknown traffic generator: "<br>
-                f"{traffic_generator_config.traffic_generator_type}"<br>
-            )<br>
diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py<br>
new file mode 100644<br>
index 0000000000..11bfa1ee0f<br>
--- /dev/null<br>
+++ b/dts/framework/testbed_model/traffic_generator/__init__.py<br>
@@ -0,0 +1,24 @@<br>
+# SPDX-License-Identifier: BSD-3-Clause<br>
+# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
+<br>
+from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorType<br>
+from framework.exception import ConfigurationError<br>
+from framework.testbed_model.node import Node<br>
+<br>
+from .capturing_traffic_generator import CapturingTrafficGenerator<br>
+from .scapy import ScapyTrafficGenerator<br>
+<br>
+<br>
+def create_traffic_generator(<br>
+    tg_node: Node, traffic_generator_config: ScapyTrafficGeneratorConfig<br>
+) -> CapturingTrafficGenerator:<br>
+    """A factory function for creating traffic generator object from user config."""<br>
+<br>
+    match traffic_generator_config.traffic_generator_type:<br>
+        case TrafficGeneratorType.SCAPY:<br>
+            return ScapyTrafficGenerator(tg_node, traffic_generator_config)<br>
+        case _:<br>
+            raise ConfigurationError(<br>
+                "Unknown traffic generator: "<br>
+                f"{traffic_generator_config.traffic_generator_type}"<br>
+            )<br>
diff --git a/dts/framework/testbed_model/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py<br>
similarity index 96%<br>
rename from dts/framework/testbed_model/capturing_traffic_generator.py<br>
rename to dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py<br>
index ab98987f8e..e521211ef0 100644<br>
--- a/dts/framework/testbed_model/capturing_traffic_generator.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py<br>
@@ -16,9 +16,9 @@<br>
 from scapy.packet import Packet  # type: ignore[import]<br>
<br>
 from framework.settings import SETTINGS<br>
+from framework.testbed_model.port import Port<br>
 from framework.utils import get_packet_summaries<br>
<br>
-from .hw.port import Port<br>
 from .traffic_generator import TrafficGenerator<br>
<br>
<br>
@@ -130,7 +130,9 @@ def _send_packets_and_capture(<br>
         for the specified duration. It must be able to handle no received packets.<br>
         """<br>
<br>
-    def _write_capture_from_packets(self, capture_name: str, packets: list[Packet]):<br>
+    def _write_capture_from_packets(<br>
+        self, capture_name: str, packets: list[Packet]<br>
+    ) -> None:<br>
         file_name = f"{SETTINGS.output_dir}/{capture_name}.pcap"<br>
         self._logger.debug(f"Writing packets to {file_name}.")<br>
         scapy.utils.wrpcap(file_name, packets)<br>
diff --git a/dts/framework/testbed_model/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py<br>
similarity index 95%<br>
rename from dts/framework/testbed_model/scapy.py<br>
rename to dts/framework/testbed_model/traffic_generator/scapy.py<br>
index af0d4dbb25..51864b6e6b 100644<br>
--- a/dts/framework/testbed_model/scapy.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py<br>
@@ -24,16 +24,15 @@<br>
 from scapy.packet import Packet  # type: ignore[import]<br>
<br>
 from framework.config import OS, ScapyTrafficGeneratorConfig<br>
-from framework.logger import DTSLOG, getLogger<br>
 from framework.remote_session import PythonShell<br>
 from framework.settings import SETTINGS<br>
+from framework.testbed_model.node import Node<br>
+from framework.testbed_model.port import Port<br>
<br>
 from .capturing_traffic_generator import (<br>
     CapturingTrafficGenerator,<br>
     _get_default_capture_name,<br>
 )<br>
-from .hw.port import Port<br>
-from .tg_node import TGNode<br>
<br>
 """<br>
 ========= BEGIN RPC FUNCTIONS =========<br>
@@ -146,7 +145,7 @@ def quit(self) -> None:<br>
         self._BaseServer__shutdown_request = True<br>
         return None<br>
<br>
-    def add_rpc_function(self, name: str, function_bytes: xmlrpc.client.Binary):<br>
+    def add_rpc_function(self, name: str, function_bytes: xmlrpc.client.Binary) -> None:<br>
         """Add a function to the server.<br>
<br>
         This is meant to be executed remotely.<br>
@@ -191,15 +190,9 @@ class ScapyTrafficGenerator(CapturingTrafficGenerator):<br>
     session: PythonShell<br>
     rpc_server_proxy: xmlrpc.client.ServerProxy<br>
     _config: ScapyTrafficGeneratorConfig<br>
-    _tg_node: TGNode<br>
-    _logger: DTSLOG<br>
-<br>
-    def __init__(self, tg_node: TGNode, config: ScapyTrafficGeneratorConfig):<br>
-        self._config = config<br>
-        self._tg_node = tg_node<br>
-        self._logger = getLogger(<br>
-            f"{self._<a href="http://tg_node.name" rel="noreferrer" target="_blank">tg_node.name</a>} {self._config.traffic_generator_type}"<br>
-        )<br>
+<br>
+    def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):<br>
+        super().__init__(tg_node, config)<br>
<br>
         assert (<br>
             self._tg_node.config.os == OS.linux<br>
@@ -235,7 +228,7 @@ def __init__(self, tg_node: TGNode, config: ScapyTrafficGeneratorConfig):<br>
             function_bytes = marshal.dumps(function.__code__)<br>
             self.rpc_server_proxy.add_rpc_function(function.__name__, function_bytes)<br>
<br>
-    def _start_xmlrpc_server_in_remote_python(self, listen_port: int):<br>
+    def _start_xmlrpc_server_in_remote_python(self, listen_port: int) -> None:<br>
         # load the source of the function<br>
         src = inspect.getsource(QuittableXMLRPCServer)<br>
         # Lines with only whitespace break the repl if in the middle of a function<br>
@@ -280,7 +273,7 @@ def _send_packets_and_capture(<br>
         scapy_packets = [Ether(packet.data) for packet in xmlrpc_packets]<br>
         return scapy_packets<br>
<br>
-    def close(self):<br>
+    def close(self) -> None:<br>
         try:<br>
             self.rpc_server_proxy.quit()<br>
         except ConnectionRefusedError:<br>
diff --git a/dts/framework/testbed_model/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
similarity index 80%<br>
rename from dts/framework/testbed_model/traffic_generator.py<br>
rename to dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
index 28c35d3ce4..ea7c3963da 100644<br>
--- a/dts/framework/testbed_model/traffic_generator.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
@@ -12,11 +12,12 @@<br>
<br>
 from scapy.packet import Packet  # type: ignore[import]<br>
<br>
-from framework.logger import DTSLOG<br>
+from framework.config import TrafficGeneratorConfig<br>
+from framework.logger import DTSLOG, getLogger<br>
+from framework.testbed_model.node import Node<br>
+from framework.testbed_model.port import Port<br>
 from framework.utils import get_packet_summaries<br>
<br>
-from .hw.port import Port<br>
-<br>
<br>
 class TrafficGenerator(ABC):<br>
     """The base traffic generator.<br>
@@ -24,8 +25,17 @@ class TrafficGenerator(ABC):<br>
     Defines the few basic methods that each traffic generator must implement.<br>
     """<br>
<br>
+    _config: TrafficGeneratorConfig<br>
+    _tg_node: Node<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Is there a benefit to changing this to be a node instead of a TGNode? Wouldn't we want the capabilities of the TGNode to be accessible in the TrafficGenerator class?<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">
     _logger: DTSLOG<br>
<br>
+    def __init__(self, tg_node: Node, config: TrafficGeneratorConfig):<br>
+        self._config = config<br>
+        self._tg_node = tg_node<br>
+        self._logger = getLogger(<br>
+            f"{self._<a href="http://tg_node.name" rel="noreferrer" target="_blank">tg_node.name</a>} {self._config.traffic_generator_type}"<br>
+        )<br>
+<br>
     def send_packet(self, packet: Packet, port: Port) -> None:<br>
         """Send a packet and block until it is fully sent.<br>
<br>
diff --git a/dts/framework/testbed_model/hw/virtual_device.py b/dts/framework/testbed_model/virtual_device.py<br>
similarity index 100%<br>
rename from dts/framework/testbed_model/hw/virtual_device.py<br>
rename to dts/framework/testbed_model/virtual_device.py<br>
diff --git a/dts/framework/utils.py b/dts/framework/utils.py<br>
index d27c2c5b5f..f0c916471c 100644<br>
--- a/dts/framework/utils.py<br>
+++ b/dts/framework/utils.py<br>
@@ -7,7 +7,6 @@<br>
 import json<br>
 import os<br>
 import subprocess<br>
-import sys<br>
 from enum import Enum<br>
 from pathlib import Path<br>
 from subprocess import SubprocessError<br>
@@ -16,35 +15,7 @@<br>
<br>
 from .exception import ConfigurationError<br>
<br>
-<br>
-class StrEnum(Enum):<br>
-    @staticmethod<br>
-    def _generate_next_value_(<br>
-        name: str, start: int, count: int, last_values: object<br>
-    ) -> str:<br>
-        return name<br>
-<br>
-    def __str__(self) -> str:<br>
-        return <a href="http://self.name" rel="noreferrer" target="_blank">self.name</a><br>
-<br>
-<br>
-REGEX_FOR_PCI_ADDRESS = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"<br>
-<br>
-<br>
-def check_dts_python_version() -> None:<br>
-    if sys.version_info.major < 3 or (<br>
-        sys.version_info.major == 3 and sys.version_info.minor < 10<br>
-    ):<br>
-        print(<br>
-            RED(<br>
-                (<br>
-                    "WARNING: DTS execution node's python version is lower than"<br>
-                    "python 3.10, is deprecated and will not work in future releases."<br>
-                )<br>
-            ),<br>
-            file=sys.stderr,<br>
-        )<br>
-        print(RED("Please use Python >= 3.10 instead"), file=sys.stderr)<br>
+REGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"<br>
<br>
<br>
 def expand_range(range_str: str) -> list[int]:<br>
@@ -67,7 +38,7 @@ def expand_range(range_str: str) -> list[int]:<br>
     return expanded_range<br>
<br>
<br>
-def get_packet_summaries(packets: list[Packet]):<br>
+def get_packet_summaries(packets: list[Packet]) -> str:<br>
     if len(packets) == 1:<br>
         packet_summaries = packets[0].summary()<br>
     else:<br>
@@ -77,8 +48,15 @@ def get_packet_summaries(packets: list[Packet]):<br>
     return f"Packet contents: \n{packet_summaries}"<br>
<br>
<br>
-def RED(text: str) -> str:<br>
-    return f"\u001B[31;1m{str(text)}\u001B[0m"<br>
+class StrEnum(Enum):<br>
+    @staticmethod<br>
+    def _generate_next_value_(<br>
+        name: str, start: int, count: int, last_values: object<br>
+    ) -> str:<br>
+        return name<br>
+<br>
+    def __str__(self) -> str:<br>
+        return <a href="http://self.name" rel="noreferrer" target="_blank">self.name</a><br>
<br>
<br>
 class MesonArgs(object):<br>
@@ -225,5 +203,5 @@ def _delete_tarball(self) -> None:<br>
         if self._tarball_path and os.path.exists(self._tarball_path):<br>
             os.remove(self._tarball_path)<br>
<br>
-    def __fspath__(self):<br>
+    def __fspath__(self) -> str:<br>
         return str(self._tarball_path)<br>
diff --git a/dts/main.py b/dts/main.py<br>
index 43311fa847..5d4714b0c3 100755<br>
--- a/dts/main.py<br>
+++ b/dts/main.py<br>
@@ -10,10 +10,17 @@<br>
<br>
 import logging<br>
<br>
-from framework import dts<br>
+from framework import settings<br>
<br>
<br>
 def main() -> None:<br>
+    """Set DTS settings, then run DTS.<br>
+<br>
+    The DTS settings are taken from the command line arguments and the environment variables.<br>
+    """<br>
+    settings.SETTINGS = settings.get_settings()<br>
+    from framework import dts<br>
+<br>
     dts.run_all()<br>
<br>
<br>
-- <br>
2.34.1<br>
<br>
</blockquote></div></div>