get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/133902/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 133902,
    "url": "http://patches.dpdk.org/api/patches/133902/?format=api",
    "web_url": "http://patches.dpdk.org/project/dpdk/patch/20231106171601.160749-2-juraj.linkes@pantheon.tech/",
    "project": {
        "id": 1,
        "url": "http://patches.dpdk.org/api/projects/1/?format=api",
        "name": "DPDK",
        "link_name": "dpdk",
        "list_id": "dev.dpdk.org",
        "list_email": "dev@dpdk.org",
        "web_url": "http://core.dpdk.org",
        "scm_url": "git://dpdk.org/dpdk",
        "webscm_url": "http://git.dpdk.org/dpdk",
        "list_archive_url": "https://inbox.dpdk.org/dev",
        "list_archive_url_format": "https://inbox.dpdk.org/dev/{}",
        "commit_url_format": ""
    },
    "msgid": "<20231106171601.160749-2-juraj.linkes@pantheon.tech>",
    "list_archive_url": "https://inbox.dpdk.org/dev/20231106171601.160749-2-juraj.linkes@pantheon.tech",
    "date": "2023-11-06T17:15:39",
    "name": "[v5,01/23] dts: code adjustments for doc generation",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": true,
    "hash": "3f8978f8aee980aab66e96c54ab098d4feee8594",
    "submitter": {
        "id": 1626,
        "url": "http://patches.dpdk.org/api/people/1626/?format=api",
        "name": "Juraj Linkeš",
        "email": "juraj.linkes@pantheon.tech"
    },
    "delegate": {
        "id": 2642,
        "url": "http://patches.dpdk.org/api/users/2642/?format=api",
        "username": "mcoquelin",
        "first_name": "Maxime",
        "last_name": "Coquelin",
        "email": "maxime.coquelin@redhat.com"
    },
    "mbox": "http://patches.dpdk.org/project/dpdk/patch/20231106171601.160749-2-juraj.linkes@pantheon.tech/mbox/",
    "series": [
        {
            "id": 30173,
            "url": "http://patches.dpdk.org/api/series/30173/?format=api",
            "web_url": "http://patches.dpdk.org/project/dpdk/list/?series=30173",
            "date": "2023-11-06T17:15:38",
            "name": "dts: add dts api docs",
            "version": 5,
            "mbox": "http://patches.dpdk.org/series/30173/mbox/"
        }
    ],
    "comments": "http://patches.dpdk.org/api/patches/133902/comments/",
    "check": "fail",
    "checks": "http://patches.dpdk.org/api/patches/133902/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<dev-bounces@dpdk.org>",
        "X-Original-To": "patchwork@inbox.dpdk.org",
        "Delivered-To": "patchwork@inbox.dpdk.org",
        "Received": [
            "from mails.dpdk.org (mails.dpdk.org [217.70.189.124])\n\tby inbox.dpdk.org (Postfix) with ESMTP id F2844432BB;\n\tMon,  6 Nov 2023 18:16:12 +0100 (CET)",
            "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id B2E5E40DCD;\n\tMon,  6 Nov 2023 18:16:07 +0100 (CET)",
            "from mail-ej1-f47.google.com (mail-ej1-f47.google.com\n [209.85.218.47]) by mails.dpdk.org (Postfix) with ESMTP id CBB6340633\n for <dev@dpdk.org>; Mon,  6 Nov 2023 18:16:06 +0100 (CET)",
            "by mail-ej1-f47.google.com with SMTP id\n a640c23a62f3a-991c786369cso697034466b.1\n for <dev@dpdk.org>; Mon, 06 Nov 2023 09:16:06 -0800 (PST)",
            "from jlinkes-PT-Latitude-5530.. (ip-46.34.243.197.o2inet.sk.\n [46.34.243.197]) by smtp.gmail.com with ESMTPSA id\n s10-20020a170906354a00b009b947aacb4bsm47016eja.191.2023.11.06.09.16.04\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Mon, 06 Nov 2023 09:16:05 -0800 (PST)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=pantheon.tech; s=google; t=1699290966; x=1699895766; darn=dpdk.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=o5OgTVzJODANDOBqIUh4rKdOwoKz/WvRfL+p6mvGZAE=;\n b=B3pJ7/qWJQ8fmfppypTzCQ8LjHd0dkWAc0DTyPqCqncoE4tJoyDxryQ4+UUxifOTKq\n 78K4fmLflRkkm+qts/eyHXi0mskcIx9lqcO7BE046wN4HceczkDX6TykA+FS+G1FL10G\n kknW86KvJyOC7Qw652hxK9YdxrgvGXBZ9REpguI3/k0hl/BhN0gpAjEXK0wIurpVCWiO\n FmvziZuOhj70UTlSgsoTx7dxW+YDX0lDzj94N8AxDaNVBeJmBAwJHBAIDMscBs1RurSn\n jW1nUZPgPVQyJbOdaif4p2jkPumIQNL1pun11fKOGfsjnMlB9MmkXYx8kuPN179gVCRu\n +acA==",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20230601; t=1699290966; x=1699895766;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=o5OgTVzJODANDOBqIUh4rKdOwoKz/WvRfL+p6mvGZAE=;\n b=eLTTOMLQxINgDB3RGZGQguHCac6p6SovolNZRqJoqdwChRzMyNKEt6O7XZnnvRjDWH\n Vc04N6hAasmUlz2XIfghyUYwlCdGMlRcTCq/1vdgkY1qQ7AtBEECLSm6MYnvmg0Tc5e+\n JKBxPs/+mty8A7c2MtjloZR08/7+i8Xp2AnVP7YoTLrY8yJ8Rjux093+g6b0YG9exjKZ\n OCoA0a35v29KH6WVkgxJod+q/cb7wUeuc+S5+5KKFk4NEzNVuklWfnZD/MIAQbqs+Zvp\n /JDlJrw017lECF4dRU+8J6pkryA+QG6PpXW149th/6ZURY7ES4NU9KfPmaCU9pq/xBLU\n EoyA==",
        "X-Gm-Message-State": "AOJu0YwlyASOzcNaoC3jE8/ODgA6hKOZaByqbTpp27H88UYagwSNtiWE\n 06IqJlLvXZoohYr0BKUdIeX1wg==",
        "X-Google-Smtp-Source": "\n AGHT+IHc9BIeoPAr1HtJebwtIPUV2XTojDeEl9GKME00i3L3N4oPkVAXwj/FLnd9sBW4NM6hPmJPVg==",
        "X-Received": "by 2002:a17:906:dc91:b0:9c5:7f8b:bafc with SMTP id\n cs17-20020a170906dc9100b009c57f8bbafcmr14264568ejc.22.1699290966187;\n Mon, 06 Nov 2023 09:16:06 -0800 (PST)",
        "From": "=?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>",
        "To": "thomas@monjalon.net, Honnappa.Nagarahalli@arm.com,\n bruce.richardson@intel.com, jspewock@iol.unh.edu, probb@iol.unh.edu,\n paul.szczepanek@arm.com, yoan.picchi@foss.arm.com",
        "Cc": "dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>",
        "Subject": "[PATCH v5 01/23] dts: code adjustments for doc generation",
        "Date": "Mon,  6 Nov 2023 18:15:39 +0100",
        "Message-Id": "<20231106171601.160749-2-juraj.linkes@pantheon.tech>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20231106171601.160749-1-juraj.linkes@pantheon.tech>",
        "References": "<20230831100407.59865-1-juraj.linkes@pantheon.tech>\n <20231106171601.160749-1-juraj.linkes@pantheon.tech>",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "dev@dpdk.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "DPDK patches and discussions <dev.dpdk.org>",
        "List-Unsubscribe": "<https://mails.dpdk.org/options/dev>,\n <mailto:dev-request@dpdk.org?subject=unsubscribe>",
        "List-Archive": "<http://mails.dpdk.org/archives/dev/>",
        "List-Post": "<mailto:dev@dpdk.org>",
        "List-Help": "<mailto:dev-request@dpdk.org?subject=help>",
        "List-Subscribe": "<https://mails.dpdk.org/listinfo/dev>,\n <mailto:dev-request@dpdk.org?subject=subscribe>",
        "Errors-To": "dev-bounces@dpdk.org"
    },
    "content": "The standard Python tool for generating API documentation, Sphinx,\nimports modules one-by-one when generating the documentation. This\nrequires code changes:\n* properly guarding argument parsing in the if __name__ == '__main__'\n  block,\n* the logger used by DTS runner underwent the same treatment so that it\n  doesn't create log files outside of a DTS run,\n* however, DTS uses the arguments to construct an object holding global\n  variables. The defaults for the global variables needed to be moved\n  from argument parsing elsewhere,\n* importing the remote_session module from framework resulted in\n  circular imports because of one module trying to import another\n  module. This is fixed by reorganizing the code,\n* some code reorganization was done because the resulting structure\n  makes more sense, improving documentation clarity.\n\nThe are some other changes which are documentation related:\n* added missing type annotation so they appear in the generated docs,\n* reordered arguments in some methods,\n* removed superfluous arguments and attributes,\n* change private functions/methods/attributes to private and vice-versa.\n\nThe above all appear in the generated documentation and the with them,\nthe documentation is improved.\n\nSigned-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>\n---\n dts/framework/config/__init__.py              | 10 ++-\n dts/framework/dts.py                          | 33 +++++--\n dts/framework/exception.py                    | 54 +++++-------\n dts/framework/remote_session/__init__.py      | 41 ++++-----\n .../interactive_remote_session.py             |  0\n .../{remote => }/interactive_shell.py         |  0\n .../{remote => }/python_shell.py              |  0\n .../remote_session/remote/__init__.py         | 27 ------\n .../{remote => }/remote_session.py            |  0\n .../{remote => }/ssh_session.py               | 12 +--\n .../{remote => }/testpmd_shell.py             |  0\n dts/framework/settings.py                     | 87 +++++++++++--------\n dts/framework/test_result.py                  |  4 +-\n dts/framework/test_suite.py                   |  7 +-\n dts/framework/testbed_model/__init__.py       | 12 +--\n dts/framework/testbed_model/{hw => }/cpu.py   | 13 +++\n dts/framework/testbed_model/hw/__init__.py    | 27 ------\n .../linux_session.py                          |  6 +-\n dts/framework/testbed_model/node.py           | 26 ++++--\n .../os_session.py                             | 22 ++---\n dts/framework/testbed_model/{hw => }/port.py  |  0\n .../posix_session.py                          |  4 +-\n dts/framework/testbed_model/sut_node.py       |  8 +-\n dts/framework/testbed_model/tg_node.py        | 30 +------\n .../traffic_generator/__init__.py             | 24 +++++\n .../capturing_traffic_generator.py            |  6 +-\n .../{ => traffic_generator}/scapy.py          | 23 ++---\n .../traffic_generator.py                      | 16 +++-\n .../testbed_model/{hw => }/virtual_device.py  |  0\n dts/framework/utils.py                        | 46 +++-------\n dts/main.py                                   |  9 +-\n 31 files changed, 259 insertions(+), 288 deletions(-)\n rename dts/framework/remote_session/{remote => }/interactive_remote_session.py (100%)\n rename dts/framework/remote_session/{remote => }/interactive_shell.py (100%)\n rename dts/framework/remote_session/{remote => }/python_shell.py (100%)\n delete mode 100644 dts/framework/remote_session/remote/__init__.py\n rename dts/framework/remote_session/{remote => }/remote_session.py (100%)\n rename dts/framework/remote_session/{remote => }/ssh_session.py (91%)\n rename dts/framework/remote_session/{remote => }/testpmd_shell.py (100%)\n rename dts/framework/testbed_model/{hw => }/cpu.py (95%)\n delete mode 100644 dts/framework/testbed_model/hw/__init__.py\n rename dts/framework/{remote_session => testbed_model}/linux_session.py (97%)\n rename dts/framework/{remote_session => testbed_model}/os_session.py (95%)\n rename dts/framework/testbed_model/{hw => }/port.py (100%)\n rename dts/framework/{remote_session => testbed_model}/posix_session.py (98%)\n create mode 100644 dts/framework/testbed_model/traffic_generator/__init__.py\n rename dts/framework/testbed_model/{ => traffic_generator}/capturing_traffic_generator.py (96%)\n rename dts/framework/testbed_model/{ => traffic_generator}/scapy.py (95%)\n rename dts/framework/testbed_model/{ => traffic_generator}/traffic_generator.py (80%)\n rename dts/framework/testbed_model/{hw => }/virtual_device.py (100%)",
    "diff": "diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py\nindex cb7e00ba34..2044c82611 100644\n--- a/dts/framework/config/__init__.py\n+++ b/dts/framework/config/__init__.py\n@@ -17,6 +17,7 @@\n import warlock  # type: ignore[import]\n import yaml\n \n+from framework.exception import ConfigurationError\n from framework.settings import SETTINGS\n from framework.utils import StrEnum\n \n@@ -89,7 +90,7 @@ class TrafficGeneratorConfig:\n     traffic_generator_type: TrafficGeneratorType\n \n     @staticmethod\n-    def from_dict(d: dict):\n+    def from_dict(d: dict) -> \"ScapyTrafficGeneratorConfig\":\n         # This looks useless now, but is designed to allow expansion to traffic\n         # generators that require more configuration later.\n         match TrafficGeneratorType(d[\"type\"]):\n@@ -97,6 +98,10 @@ def from_dict(d: dict):\n                 return ScapyTrafficGeneratorConfig(\n                     traffic_generator_type=TrafficGeneratorType.SCAPY\n                 )\n+            case _:\n+                raise ConfigurationError(\n+                    f'Unknown traffic generator type \"{d[\"type\"]}\".'\n+                )\n \n \n @dataclass(slots=True, frozen=True)\n@@ -324,6 +329,3 @@ def load_config() -> Configuration:\n     config: dict[str, Any] = warlock.model_factory(schema, name=\"_Config\")(config_data)\n     config_obj: Configuration = Configuration.from_dict(dict(config))\n     return config_obj\n-\n-\n-CONFIGURATION = load_config()\ndiff --git a/dts/framework/dts.py b/dts/framework/dts.py\nindex f773f0c38d..4c7fb0c40a 100644\n--- a/dts/framework/dts.py\n+++ b/dts/framework/dts.py\n@@ -6,19 +6,19 @@\n import sys\n \n from .config import (\n-    CONFIGURATION,\n     BuildTargetConfiguration,\n     ExecutionConfiguration,\n     TestSuiteConfig,\n+    load_config,\n )\n from .exception import BlockingTestSuiteError\n from .logger import DTSLOG, getLogger\n from .test_result import BuildTargetResult, DTSResult, ExecutionResult, Result\n from .test_suite import get_test_suites\n from .testbed_model import SutNode, TGNode\n-from .utils import check_dts_python_version\n \n-dts_logger: DTSLOG = getLogger(\"DTSRunner\")\n+# dummy defaults to satisfy linters\n+dts_logger: DTSLOG = None  # type: ignore[assignment]\n result: DTSResult = DTSResult(dts_logger)\n \n \n@@ -30,14 +30,18 @@ def run_all() -> None:\n     global dts_logger\n     global result\n \n+    # create a regular DTS logger and create a new result with it\n+    dts_logger = getLogger(\"DTSRunner\")\n+    result = DTSResult(dts_logger)\n+\n     # check the python version of the server that run dts\n-    check_dts_python_version()\n+    _check_dts_python_version()\n \n     sut_nodes: dict[str, SutNode] = {}\n     tg_nodes: dict[str, TGNode] = {}\n     try:\n         # for all Execution sections\n-        for execution in CONFIGURATION.executions:\n+        for execution in load_config().executions:\n             sut_node = sut_nodes.get(execution.system_under_test_node.name)\n             tg_node = tg_nodes.get(execution.traffic_generator_node.name)\n \n@@ -82,6 +86,25 @@ def run_all() -> None:\n     _exit_dts()\n \n \n+def _check_dts_python_version() -> None:\n+    def RED(text: str) -> str:\n+        return f\"\\u001B[31;1m{str(text)}\\u001B[0m\"\n+\n+    if sys.version_info.major < 3 or (\n+        sys.version_info.major == 3 and sys.version_info.minor < 10\n+    ):\n+        print(\n+            RED(\n+                (\n+                    \"WARNING: DTS execution node's python version is lower than\"\n+                    \"python 3.10, is deprecated and will not work in future releases.\"\n+                )\n+            ),\n+            file=sys.stderr,\n+        )\n+        print(RED(\"Please use Python >= 3.10 instead\"), file=sys.stderr)\n+\n+\n def _run_execution(\n     sut_node: SutNode,\n     tg_node: TGNode,\ndiff --git a/dts/framework/exception.py b/dts/framework/exception.py\nindex 001a5a5496..7489c03570 100644\n--- a/dts/framework/exception.py\n+++ b/dts/framework/exception.py\n@@ -42,19 +42,14 @@ class SSHTimeoutError(DTSError):\n     Command execution timeout.\n     \"\"\"\n \n-    command: str\n-    output: str\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR\n+    _command: str\n \n-    def __init__(self, command: str, output: str):\n-        self.command = command\n-        self.output = output\n+    def __init__(self, command: str):\n+        self._command = command\n \n     def __str__(self) -> str:\n-        return f\"TIMEOUT on {self.command}\"\n-\n-    def get_output(self) -> str:\n-        return self.output\n+        return f\"TIMEOUT on {self._command}\"\n \n \n class SSHConnectionError(DTSError):\n@@ -62,18 +57,18 @@ class SSHConnectionError(DTSError):\n     SSH connection error.\n     \"\"\"\n \n-    host: str\n-    errors: list[str]\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR\n+    _host: str\n+    _errors: list[str]\n \n     def __init__(self, host: str, errors: list[str] | None = None):\n-        self.host = host\n-        self.errors = [] if errors is None else errors\n+        self._host = host\n+        self._errors = [] if errors is None else errors\n \n     def __str__(self) -> str:\n-        message = f\"Error trying to connect with {self.host}.\"\n-        if self.errors:\n-            message += f\" Errors encountered while retrying: {', '.join(self.errors)}\"\n+        message = f\"Error trying to connect with {self._host}.\"\n+        if self._errors:\n+            message += f\" Errors encountered while retrying: {', '.join(self._errors)}\"\n \n         return message\n \n@@ -84,14 +79,14 @@ class SSHSessionDeadError(DTSError):\n     It can no longer be used.\n     \"\"\"\n \n-    host: str\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR\n+    _host: str\n \n     def __init__(self, host: str):\n-        self.host = host\n+        self._host = host\n \n     def __str__(self) -> str:\n-        return f\"SSH session with {self.host} has died\"\n+        return f\"SSH session with {self._host} has died\"\n \n \n class ConfigurationError(DTSError):\n@@ -107,18 +102,18 @@ class RemoteCommandExecutionError(DTSError):\n     Raised when a command executed on a Node returns a non-zero exit status.\n     \"\"\"\n \n-    command: str\n-    command_return_code: int\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.REMOTE_CMD_EXEC_ERR\n+    command: str\n+    _command_return_code: int\n \n     def __init__(self, command: str, command_return_code: int):\n         self.command = command\n-        self.command_return_code = command_return_code\n+        self._command_return_code = command_return_code\n \n     def __str__(self) -> str:\n         return (\n             f\"Command {self.command} returned a non-zero exit code: \"\n-            f\"{self.command_return_code}\"\n+            f\"{self._command_return_code}\"\n         )\n \n \n@@ -143,22 +138,15 @@ class TestCaseVerifyError(DTSError):\n     Used in test cases to verify the expected behavior.\n     \"\"\"\n \n-    value: str\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.TESTCASE_VERIFY_ERR\n \n-    def __init__(self, value: str):\n-        self.value = value\n-\n-    def __str__(self) -> str:\n-        return repr(self.value)\n-\n \n class BlockingTestSuiteError(DTSError):\n-    suite_name: str\n     severity: ClassVar[ErrorSeverity] = ErrorSeverity.BLOCKING_TESTSUITE_ERR\n+    _suite_name: str\n \n     def __init__(self, suite_name: str) -> None:\n-        self.suite_name = suite_name\n+        self._suite_name = suite_name\n \n     def __str__(self) -> str:\n-        return f\"Blocking suite {self.suite_name} failed.\"\n+        return f\"Blocking suite {self._suite_name} failed.\"\ndiff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py\nindex 00b6d1f03a..5e7ddb2b05 100644\n--- a/dts/framework/remote_session/__init__.py\n+++ b/dts/framework/remote_session/__init__.py\n@@ -12,29 +12,24 @@\n \n # pylama:ignore=W0611\n \n-from framework.config import OS, NodeConfiguration\n-from framework.exception import ConfigurationError\n+from framework.config import NodeConfiguration\n from framework.logger import DTSLOG\n \n-from .linux_session import LinuxSession\n-from .os_session import InteractiveShellType, OSSession\n-from .remote import (\n-    CommandResult,\n-    InteractiveRemoteSession,\n-    InteractiveShell,\n-    PythonShell,\n-    RemoteSession,\n-    SSHSession,\n-    TestPmdDevice,\n-    TestPmdShell,\n-)\n-\n-\n-def create_session(\n+from .interactive_remote_session import InteractiveRemoteSession\n+from .interactive_shell import InteractiveShell\n+from .python_shell import PythonShell\n+from .remote_session import CommandResult, RemoteSession\n+from .ssh_session import SSHSession\n+from .testpmd_shell import TestPmdShell\n+\n+\n+def create_remote_session(\n     node_config: NodeConfiguration, name: str, logger: DTSLOG\n-) -> OSSession:\n-    match node_config.os:\n-        case OS.linux:\n-            return LinuxSession(node_config, name, logger)\n-        case _:\n-            raise ConfigurationError(f\"Unsupported OS {node_config.os}\")\n+) -> RemoteSession:\n+    return SSHSession(node_config, name, logger)\n+\n+\n+def create_interactive_session(\n+    node_config: NodeConfiguration, logger: DTSLOG\n+) -> InteractiveRemoteSession:\n+    return InteractiveRemoteSession(node_config, logger)\ndiff --git a/dts/framework/remote_session/remote/interactive_remote_session.py b/dts/framework/remote_session/interactive_remote_session.py\nsimilarity index 100%\nrename from dts/framework/remote_session/remote/interactive_remote_session.py\nrename to dts/framework/remote_session/interactive_remote_session.py\ndiff --git a/dts/framework/remote_session/remote/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py\nsimilarity index 100%\nrename from dts/framework/remote_session/remote/interactive_shell.py\nrename to dts/framework/remote_session/interactive_shell.py\ndiff --git a/dts/framework/remote_session/remote/python_shell.py b/dts/framework/remote_session/python_shell.py\nsimilarity index 100%\nrename from dts/framework/remote_session/remote/python_shell.py\nrename to dts/framework/remote_session/python_shell.py\ndiff --git a/dts/framework/remote_session/remote/__init__.py b/dts/framework/remote_session/remote/__init__.py\ndeleted file mode 100644\nindex 06403691a5..0000000000\n--- a/dts/framework/remote_session/remote/__init__.py\n+++ /dev/null\n@@ -1,27 +0,0 @@\n-# SPDX-License-Identifier: BSD-3-Clause\n-# Copyright(c) 2023 PANTHEON.tech s.r.o.\n-# Copyright(c) 2023 University of New Hampshire\n-\n-# pylama:ignore=W0611\n-\n-from framework.config import NodeConfiguration\n-from framework.logger import DTSLOG\n-\n-from .interactive_remote_session import InteractiveRemoteSession\n-from .interactive_shell import InteractiveShell\n-from .python_shell import PythonShell\n-from .remote_session import CommandResult, RemoteSession\n-from .ssh_session import SSHSession\n-from .testpmd_shell import TestPmdDevice, TestPmdShell\n-\n-\n-def create_remote_session(\n-    node_config: NodeConfiguration, name: str, logger: DTSLOG\n-) -> RemoteSession:\n-    return SSHSession(node_config, name, logger)\n-\n-\n-def create_interactive_session(\n-    node_config: NodeConfiguration, logger: DTSLOG\n-) -> InteractiveRemoteSession:\n-    return InteractiveRemoteSession(node_config, logger)\ndiff --git a/dts/framework/remote_session/remote/remote_session.py b/dts/framework/remote_session/remote_session.py\nsimilarity index 100%\nrename from dts/framework/remote_session/remote/remote_session.py\nrename to dts/framework/remote_session/remote_session.py\ndiff --git a/dts/framework/remote_session/remote/ssh_session.py b/dts/framework/remote_session/ssh_session.py\nsimilarity index 91%\nrename from dts/framework/remote_session/remote/ssh_session.py\nrename to dts/framework/remote_session/ssh_session.py\nindex 8d127f1601..cee11d14d6 100644\n--- a/dts/framework/remote_session/remote/ssh_session.py\n+++ b/dts/framework/remote_session/ssh_session.py\n@@ -18,9 +18,7 @@\n     SSHException,\n )\n \n-from framework.config import NodeConfiguration\n from framework.exception import SSHConnectionError, SSHSessionDeadError, SSHTimeoutError\n-from framework.logger import DTSLOG\n \n from .remote_session import CommandResult, RemoteSession\n \n@@ -45,14 +43,6 @@ class SSHSession(RemoteSession):\n \n     session: Connection\n \n-    def __init__(\n-        self,\n-        node_config: NodeConfiguration,\n-        session_name: str,\n-        logger: DTSLOG,\n-    ):\n-        super(SSHSession, self).__init__(node_config, session_name, logger)\n-\n     def _connect(self) -> None:\n         errors = []\n         retry_attempts = 10\n@@ -117,7 +107,7 @@ def _send_command(\n \n         except CommandTimedOut as e:\n             self._logger.exception(e)\n-            raise SSHTimeoutError(command, e.result.stderr) from e\n+            raise SSHTimeoutError(command) from e\n \n         return CommandResult(\n             self.name, command, output.stdout, output.stderr, output.return_code\ndiff --git a/dts/framework/remote_session/remote/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py\nsimilarity index 100%\nrename from dts/framework/remote_session/remote/testpmd_shell.py\nrename to dts/framework/remote_session/testpmd_shell.py\ndiff --git a/dts/framework/settings.py b/dts/framework/settings.py\nindex cfa39d011b..7f5841d073 100644\n--- a/dts/framework/settings.py\n+++ b/dts/framework/settings.py\n@@ -6,7 +6,7 @@\n import argparse\n import os\n from collections.abc import Callable, Iterable, Sequence\n-from dataclasses import dataclass\n+from dataclasses import dataclass, field\n from pathlib import Path\n from typing import Any, TypeVar\n \n@@ -22,8 +22,8 @@ def __init__(\n             option_strings: Sequence[str],\n             dest: str,\n             nargs: str | int | None = None,\n-            const: str | None = None,\n-            default: str = None,\n+            const: bool | None = None,\n+            default: Any = None,\n             type: Callable[[str], _T | argparse.FileType | None] = None,\n             choices: Iterable[_T] | None = None,\n             required: bool = False,\n@@ -32,6 +32,12 @@ def __init__(\n         ) -> None:\n             env_var_value = os.environ.get(env_var)\n             default = env_var_value or default\n+            if const is not None:\n+                nargs = 0\n+                default = const if env_var_value else default\n+                type = None\n+                choices = None\n+                metavar = None\n             super(_EnvironmentArgument, self).__init__(\n                 option_strings,\n                 dest,\n@@ -52,22 +58,28 @@ def __call__(\n             values: Any,\n             option_string: str = None,\n         ) -> None:\n-            setattr(namespace, self.dest, values)\n+            if self.const is not None:\n+                setattr(namespace, self.dest, self.const)\n+            else:\n+                setattr(namespace, self.dest, values)\n \n     return _EnvironmentArgument\n \n \n-@dataclass(slots=True, frozen=True)\n-class _Settings:\n-    config_file_path: str\n-    output_dir: str\n-    timeout: float\n-    verbose: bool\n-    skip_setup: bool\n-    dpdk_tarball_path: Path\n-    compile_timeout: float\n-    test_cases: list\n-    re_run: int\n+@dataclass(slots=True)\n+class Settings:\n+    config_file_path: Path = Path(__file__).parent.parent.joinpath(\"conf.yaml\")\n+    output_dir: str = \"output\"\n+    timeout: float = 15\n+    verbose: bool = False\n+    skip_setup: bool = False\n+    dpdk_tarball_path: Path | str = \"dpdk.tar.xz\"\n+    compile_timeout: float = 1200\n+    test_cases: list[str] = field(default_factory=list)\n+    re_run: int = 0\n+\n+\n+SETTINGS: Settings = Settings()\n \n \n def _get_parser() -> argparse.ArgumentParser:\n@@ -81,7 +93,8 @@ def _get_parser() -> argparse.ArgumentParser:\n     parser.add_argument(\n         \"--config-file\",\n         action=_env_arg(\"DTS_CFG_FILE\"),\n-        default=\"conf.yaml\",\n+        default=SETTINGS.config_file_path,\n+        type=Path,\n         help=\"[DTS_CFG_FILE] configuration file that describes the test cases, SUTs \"\n         \"and targets.\",\n     )\n@@ -90,7 +103,7 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"--output-dir\",\n         \"--output\",\n         action=_env_arg(\"DTS_OUTPUT_DIR\"),\n-        default=\"output\",\n+        default=SETTINGS.output_dir,\n         help=\"[DTS_OUTPUT_DIR] Output directory where dts logs and results are saved.\",\n     )\n \n@@ -98,7 +111,7 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"-t\",\n         \"--timeout\",\n         action=_env_arg(\"DTS_TIMEOUT\"),\n-        default=15,\n+        default=SETTINGS.timeout,\n         type=float,\n         help=\"[DTS_TIMEOUT] The default timeout for all DTS operations except for \"\n         \"compiling DPDK.\",\n@@ -108,8 +121,9 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"-v\",\n         \"--verbose\",\n         action=_env_arg(\"DTS_VERBOSE\"),\n-        default=\"N\",\n-        help=\"[DTS_VERBOSE] Set to 'Y' to enable verbose output, logging all messages \"\n+        default=SETTINGS.verbose,\n+        const=True,\n+        help=\"[DTS_VERBOSE] Specify to enable verbose output, logging all messages \"\n         \"to the console.\",\n     )\n \n@@ -117,8 +131,8 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"-s\",\n         \"--skip-setup\",\n         action=_env_arg(\"DTS_SKIP_SETUP\"),\n-        default=\"N\",\n-        help=\"[DTS_SKIP_SETUP] Set to 'Y' to skip all setup steps on SUT and TG nodes.\",\n+        const=True,\n+        help=\"[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes.\",\n     )\n \n     parser.add_argument(\n@@ -126,7 +140,7 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"--snapshot\",\n         \"--git-ref\",\n         action=_env_arg(\"DTS_DPDK_TARBALL\"),\n-        default=\"dpdk.tar.xz\",\n+        default=SETTINGS.dpdk_tarball_path,\n         type=Path,\n         help=\"[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, \"\n         \"tag ID or tree ID to test. To test local changes, first commit them, \"\n@@ -136,7 +150,7 @@ def _get_parser() -> argparse.ArgumentParser:\n     parser.add_argument(\n         \"--compile-timeout\",\n         action=_env_arg(\"DTS_COMPILE_TIMEOUT\"),\n-        default=1200,\n+        default=SETTINGS.compile_timeout,\n         type=float,\n         help=\"[DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK.\",\n     )\n@@ -153,7 +167,7 @@ def _get_parser() -> argparse.ArgumentParser:\n         \"--re-run\",\n         \"--re_run\",\n         action=_env_arg(\"DTS_RERUN\"),\n-        default=0,\n+        default=SETTINGS.re_run,\n         type=int,\n         help=\"[DTS_RERUN] Re-run each test case the specified amount of times \"\n         \"if a test failure occurs\",\n@@ -162,23 +176,22 @@ def _get_parser() -> argparse.ArgumentParser:\n     return parser\n \n \n-def _get_settings() -> _Settings:\n+def get_settings() -> Settings:\n     parsed_args = _get_parser().parse_args()\n-    return _Settings(\n+    return Settings(\n         config_file_path=parsed_args.config_file,\n         output_dir=parsed_args.output_dir,\n         timeout=parsed_args.timeout,\n-        verbose=(parsed_args.verbose == \"Y\"),\n-        skip_setup=(parsed_args.skip_setup == \"Y\"),\n+        verbose=parsed_args.verbose,\n+        skip_setup=parsed_args.skip_setup,\n         dpdk_tarball_path=Path(\n-            DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir)\n-        )\n-        if not os.path.exists(parsed_args.tarball)\n-        else Path(parsed_args.tarball),\n+            Path(DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir))\n+            if not os.path.exists(parsed_args.tarball)\n+            else Path(parsed_args.tarball)\n+        ),\n         compile_timeout=parsed_args.compile_timeout,\n-        test_cases=parsed_args.test_cases.split(\",\") if parsed_args.test_cases else [],\n+        test_cases=(\n+            parsed_args.test_cases.split(\",\") if parsed_args.test_cases else []\n+        ),\n         re_run=parsed_args.re_run,\n     )\n-\n-\n-SETTINGS: _Settings = _get_settings()\ndiff --git a/dts/framework/test_result.py b/dts/framework/test_result.py\nindex f0fbe80f6f..603e18872c 100644\n--- a/dts/framework/test_result.py\n+++ b/dts/framework/test_result.py\n@@ -254,7 +254,7 @@ def add_build_target(\n         self._inner_results.append(build_target_result)\n         return build_target_result\n \n-    def add_sut_info(self, sut_info: NodeInfo):\n+    def add_sut_info(self, sut_info: NodeInfo) -> None:\n         self.sut_os_name = sut_info.os_name\n         self.sut_os_version = sut_info.os_version\n         self.sut_kernel_version = sut_info.kernel_version\n@@ -297,7 +297,7 @@ def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:\n         self._inner_results.append(execution_result)\n         return execution_result\n \n-    def add_error(self, error) -> None:\n+    def add_error(self, error: Exception) -> None:\n         self._errors.append(error)\n \n     def process(self) -> None:\ndiff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py\nindex 3b890c0451..d53553bf34 100644\n--- a/dts/framework/test_suite.py\n+++ b/dts/framework/test_suite.py\n@@ -11,7 +11,7 @@\n import re\n from ipaddress import IPv4Interface, IPv6Interface, ip_interface\n from types import MethodType\n-from typing import Union\n+from typing import Any, Union\n \n from scapy.layers.inet import IP  # type: ignore[import]\n from scapy.layers.l2 import Ether  # type: ignore[import]\n@@ -26,8 +26,7 @@\n from .logger import DTSLOG, getLogger\n from .settings import SETTINGS\n from .test_result import BuildTargetResult, Result, TestCaseResult, TestSuiteResult\n-from .testbed_model import SutNode, TGNode\n-from .testbed_model.hw.port import Port, PortLink\n+from .testbed_model import Port, PortLink, SutNode, TGNode\n from .utils import get_packet_summaries\n \n \n@@ -453,7 +452,7 @@ def _execute_test_case(\n \n \n def get_test_suites(testsuite_module_path: str) -> list[type[TestSuite]]:\n-    def is_test_suite(object) -> bool:\n+    def is_test_suite(object: Any) -> bool:\n         try:\n             if issubclass(object, TestSuite) and object is not TestSuite:\n                 return True\ndiff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py\nindex 5cbb859e47..8ced05653b 100644\n--- a/dts/framework/testbed_model/__init__.py\n+++ b/dts/framework/testbed_model/__init__.py\n@@ -9,15 +9,9 @@\n \n # pylama:ignore=W0611\n \n-from .hw import (\n-    LogicalCore,\n-    LogicalCoreCount,\n-    LogicalCoreCountFilter,\n-    LogicalCoreList,\n-    LogicalCoreListFilter,\n-    VirtualDevice,\n-    lcore_filter,\n-)\n+from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList\n from .node import Node\n+from .port import Port, PortLink\n from .sut_node import SutNode\n from .tg_node import TGNode\n+from .virtual_device import VirtualDevice\ndiff --git a/dts/framework/testbed_model/hw/cpu.py b/dts/framework/testbed_model/cpu.py\nsimilarity index 95%\nrename from dts/framework/testbed_model/hw/cpu.py\nrename to dts/framework/testbed_model/cpu.py\nindex d1918a12dc..8fe785dfe4 100644\n--- a/dts/framework/testbed_model/hw/cpu.py\n+++ b/dts/framework/testbed_model/cpu.py\n@@ -272,3 +272,16 @@ def filter(self) -> list[LogicalCore]:\n             )\n \n         return filtered_lcores\n+\n+\n+def lcore_filter(\n+    core_list: list[LogicalCore],\n+    filter_specifier: LogicalCoreCount | LogicalCoreList,\n+    ascending: bool,\n+) -> LogicalCoreFilter:\n+    if isinstance(filter_specifier, LogicalCoreList):\n+        return LogicalCoreListFilter(core_list, filter_specifier, ascending)\n+    elif isinstance(filter_specifier, LogicalCoreCount):\n+        return LogicalCoreCountFilter(core_list, filter_specifier, ascending)\n+    else:\n+        raise ValueError(f\"Unsupported filter r{filter_specifier}\")\ndiff --git a/dts/framework/testbed_model/hw/__init__.py b/dts/framework/testbed_model/hw/__init__.py\ndeleted file mode 100644\nindex 88ccac0b0e..0000000000\n--- a/dts/framework/testbed_model/hw/__init__.py\n+++ /dev/null\n@@ -1,27 +0,0 @@\n-# SPDX-License-Identifier: BSD-3-Clause\n-# Copyright(c) 2023 PANTHEON.tech s.r.o.\n-\n-# pylama:ignore=W0611\n-\n-from .cpu import (\n-    LogicalCore,\n-    LogicalCoreCount,\n-    LogicalCoreCountFilter,\n-    LogicalCoreFilter,\n-    LogicalCoreList,\n-    LogicalCoreListFilter,\n-)\n-from .virtual_device import VirtualDevice\n-\n-\n-def lcore_filter(\n-    core_list: list[LogicalCore],\n-    filter_specifier: LogicalCoreCount | LogicalCoreList,\n-    ascending: bool,\n-) -> LogicalCoreFilter:\n-    if isinstance(filter_specifier, LogicalCoreList):\n-        return LogicalCoreListFilter(core_list, filter_specifier, ascending)\n-    elif isinstance(filter_specifier, LogicalCoreCount):\n-        return LogicalCoreCountFilter(core_list, filter_specifier, ascending)\n-    else:\n-        raise ValueError(f\"Unsupported filter r{filter_specifier}\")\ndiff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/testbed_model/linux_session.py\nsimilarity index 97%\nrename from dts/framework/remote_session/linux_session.py\nrename to dts/framework/testbed_model/linux_session.py\nindex a3f1a6bf3b..f472bb8f0f 100644\n--- a/dts/framework/remote_session/linux_session.py\n+++ b/dts/framework/testbed_model/linux_session.py\n@@ -9,10 +9,10 @@\n from typing_extensions import NotRequired\n \n from framework.exception import RemoteCommandExecutionError\n-from framework.testbed_model import LogicalCore\n-from framework.testbed_model.hw.port import Port\n from framework.utils import expand_range\n \n+from .cpu import LogicalCore\n+from .port import Port\n from .posix_session import PosixSession\n \n \n@@ -64,7 +64,7 @@ def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:\n             lcores.append(LogicalCore(lcore, core, socket, node))\n         return lcores\n \n-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:\n         return dpdk_prefix\n \n     def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None:\ndiff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py\nindex fc01e0bf8e..7571e7b98d 100644\n--- a/dts/framework/testbed_model/node.py\n+++ b/dts/framework/testbed_model/node.py\n@@ -12,23 +12,26 @@\n from typing import Any, Callable, Type, Union\n \n from framework.config import (\n+    OS,\n     BuildTargetConfiguration,\n     ExecutionConfiguration,\n     NodeConfiguration,\n )\n+from framework.exception import ConfigurationError\n from framework.logger import DTSLOG, getLogger\n-from framework.remote_session import InteractiveShellType, OSSession, create_session\n from framework.settings import SETTINGS\n \n-from .hw import (\n+from .cpu import (\n     LogicalCore,\n     LogicalCoreCount,\n     LogicalCoreList,\n     LogicalCoreListFilter,\n-    VirtualDevice,\n     lcore_filter,\n )\n-from .hw.port import Port\n+from .linux_session import LinuxSession\n+from .os_session import InteractiveShellType, OSSession\n+from .port import Port\n+from .virtual_device import VirtualDevice\n \n \n class Node(ABC):\n@@ -69,6 +72,7 @@ def __init__(self, node_config: NodeConfiguration):\n     def _init_ports(self) -> None:\n         self.ports = [Port(self.name, port_config) for port_config in self.config.ports]\n         self.main_session.update_ports(self.ports)\n+\n         for port in self.ports:\n             self.configure_port_state(port)\n \n@@ -172,9 +176,9 @@ def create_interactive_shell(\n \n         return self.main_session.create_interactive_shell(\n             shell_cls,\n-            app_args,\n             timeout,\n             privileged,\n+            app_args,\n         )\n \n     def filter_lcores(\n@@ -205,7 +209,7 @@ def _get_remote_cpus(self) -> None:\n         self._logger.info(\"Getting CPU information.\")\n         self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)\n \n-    def _setup_hugepages(self):\n+    def _setup_hugepages(self) -> None:\n         \"\"\"\n         Setup hugepages on the Node. Different architectures can supply different\n         amounts of memory for hugepages and numa-based hugepage allocation may need\n@@ -249,3 +253,13 @@ def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:\n             return lambda *args: None\n         else:\n             return func\n+\n+\n+def create_session(\n+    node_config: NodeConfiguration, name: str, logger: DTSLOG\n+) -> OSSession:\n+    match node_config.os:\n+        case OS.linux:\n+            return LinuxSession(node_config, name, logger)\n+        case _:\n+            raise ConfigurationError(f\"Unsupported OS {node_config.os}\")\ndiff --git a/dts/framework/remote_session/os_session.py b/dts/framework/testbed_model/os_session.py\nsimilarity index 95%\nrename from dts/framework/remote_session/os_session.py\nrename to dts/framework/testbed_model/os_session.py\nindex 8a709eac1c..76e595a518 100644\n--- a/dts/framework/remote_session/os_session.py\n+++ b/dts/framework/testbed_model/os_session.py\n@@ -10,19 +10,19 @@\n \n from framework.config import Architecture, NodeConfiguration, NodeInfo\n from framework.logger import DTSLOG\n-from framework.remote_session.remote import InteractiveShell\n-from framework.settings import SETTINGS\n-from framework.testbed_model import LogicalCore\n-from framework.testbed_model.hw.port import Port\n-from framework.utils import MesonArgs\n-\n-from .remote import (\n+from framework.remote_session import (\n     CommandResult,\n     InteractiveRemoteSession,\n+    InteractiveShell,\n     RemoteSession,\n     create_interactive_session,\n     create_remote_session,\n )\n+from framework.settings import SETTINGS\n+from framework.utils import MesonArgs\n+\n+from .cpu import LogicalCore\n+from .port import Port\n \n InteractiveShellType = TypeVar(\"InteractiveShellType\", bound=InteractiveShell)\n \n@@ -85,9 +85,9 @@ def send_command(\n     def create_interactive_shell(\n         self,\n         shell_cls: Type[InteractiveShellType],\n-        eal_parameters: str,\n         timeout: float,\n         privileged: bool,\n+        app_args: str,\n     ) -> InteractiveShellType:\n         \"\"\"\n         See \"create_interactive_shell\" in SutNode\n@@ -96,7 +96,7 @@ def create_interactive_shell(\n             self.interactive_session.session,\n             self._logger,\n             self._get_privileged_command if privileged else None,\n-            eal_parameters,\n+            app_args,\n             timeout,\n         )\n \n@@ -113,7 +113,7 @@ def _get_privileged_command(command: str) -> str:\n         \"\"\"\n \n     @abstractmethod\n-    def guess_dpdk_remote_dir(self, remote_dir) -> PurePath:\n+    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:\n         \"\"\"\n         Try to find DPDK remote dir in remote_dir.\n         \"\"\"\n@@ -227,7 +227,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:\n         \"\"\"\n \n     @abstractmethod\n-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:\n         \"\"\"\n         Get the DPDK file prefix that will be used when running DPDK apps.\n         \"\"\"\ndiff --git a/dts/framework/testbed_model/hw/port.py b/dts/framework/testbed_model/port.py\nsimilarity index 100%\nrename from dts/framework/testbed_model/hw/port.py\nrename to dts/framework/testbed_model/port.py\ndiff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/testbed_model/posix_session.py\nsimilarity index 98%\nrename from dts/framework/remote_session/posix_session.py\nrename to dts/framework/testbed_model/posix_session.py\nindex 5da0516e05..1d1d5b1b26 100644\n--- a/dts/framework/remote_session/posix_session.py\n+++ b/dts/framework/testbed_model/posix_session.py\n@@ -32,7 +32,7 @@ def combine_short_options(**opts: bool) -> str:\n \n         return ret_opts\n \n-    def guess_dpdk_remote_dir(self, remote_dir) -> PurePosixPath:\n+    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePosixPath:\n         remote_guess = self.join_remote_path(remote_dir, \"dpdk-*\")\n         result = self.send_command(f\"ls -d {remote_guess} | tail -1\")\n         return PurePosixPath(result.stdout)\n@@ -219,7 +219,7 @@ def _remove_dpdk_runtime_dirs(\n         for dpdk_runtime_dir in dpdk_runtime_dirs:\n             self.remove_remote_dir(dpdk_runtime_dir)\n \n-    def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:\n         return \"\"\n \n     def get_compiler_version(self, compiler_name: str) -> str:\ndiff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py\nindex 202aebfd06..4e33cf02ea 100644\n--- a/dts/framework/testbed_model/sut_node.py\n+++ b/dts/framework/testbed_model/sut_node.py\n@@ -15,12 +15,14 @@\n     NodeInfo,\n     SutNodeConfiguration,\n )\n-from framework.remote_session import CommandResult, InteractiveShellType, OSSession\n+from framework.remote_session import CommandResult\n from framework.settings import SETTINGS\n from framework.utils import MesonArgs\n \n-from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice\n+from .cpu import LogicalCoreCount, LogicalCoreList\n from .node import Node\n+from .os_session import InteractiveShellType, OSSession\n+from .virtual_device import VirtualDevice\n \n \n class EalParameters(object):\n@@ -289,7 +291,7 @@ def create_eal_parameters(\n         prefix: str = \"dpdk\",\n         append_prefix_timestamp: bool = True,\n         no_pci: bool = False,\n-        vdevs: list[VirtualDevice] = None,\n+        vdevs: list[VirtualDevice] | None = None,\n         other_eal_param: str = \"\",\n     ) -> \"EalParameters\":\n         \"\"\"\ndiff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py\nindex 27025cfa31..166eb8430e 100644\n--- a/dts/framework/testbed_model/tg_node.py\n+++ b/dts/framework/testbed_model/tg_node.py\n@@ -16,16 +16,11 @@\n \n from scapy.packet import Packet  # type: ignore[import]\n \n-from framework.config import (\n-    ScapyTrafficGeneratorConfig,\n-    TGNodeConfiguration,\n-    TrafficGeneratorType,\n-)\n-from framework.exception import ConfigurationError\n-\n-from .capturing_traffic_generator import CapturingTrafficGenerator\n-from .hw.port import Port\n+from framework.config import TGNodeConfiguration\n+\n from .node import Node\n+from .port import Port\n+from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator\n \n \n class TGNode(Node):\n@@ -80,20 +75,3 @@ def close(self) -> None:\n         \"\"\"Free all resources used by the node\"\"\"\n         self.traffic_generator.close()\n         super(TGNode, self).close()\n-\n-\n-def create_traffic_generator(\n-    tg_node: TGNode, traffic_generator_config: ScapyTrafficGeneratorConfig\n-) -> CapturingTrafficGenerator:\n-    \"\"\"A factory function for creating traffic generator object from user config.\"\"\"\n-\n-    from .scapy import ScapyTrafficGenerator\n-\n-    match traffic_generator_config.traffic_generator_type:\n-        case TrafficGeneratorType.SCAPY:\n-            return ScapyTrafficGenerator(tg_node, traffic_generator_config)\n-        case _:\n-            raise ConfigurationError(\n-                \"Unknown traffic generator: \"\n-                f\"{traffic_generator_config.traffic_generator_type}\"\n-            )\ndiff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py\nnew file mode 100644\nindex 0000000000..11bfa1ee0f\n--- /dev/null\n+++ b/dts/framework/testbed_model/traffic_generator/__init__.py\n@@ -0,0 +1,24 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2023 PANTHEON.tech s.r.o.\n+\n+from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorType\n+from framework.exception import ConfigurationError\n+from framework.testbed_model.node import Node\n+\n+from .capturing_traffic_generator import CapturingTrafficGenerator\n+from .scapy import ScapyTrafficGenerator\n+\n+\n+def create_traffic_generator(\n+    tg_node: Node, traffic_generator_config: ScapyTrafficGeneratorConfig\n+) -> CapturingTrafficGenerator:\n+    \"\"\"A factory function for creating traffic generator object from user config.\"\"\"\n+\n+    match traffic_generator_config.traffic_generator_type:\n+        case TrafficGeneratorType.SCAPY:\n+            return ScapyTrafficGenerator(tg_node, traffic_generator_config)\n+        case _:\n+            raise ConfigurationError(\n+                \"Unknown traffic generator: \"\n+                f\"{traffic_generator_config.traffic_generator_type}\"\n+            )\ndiff --git a/dts/framework/testbed_model/capturing_traffic_generator.py b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py\nsimilarity index 96%\nrename from dts/framework/testbed_model/capturing_traffic_generator.py\nrename to dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py\nindex ab98987f8e..e521211ef0 100644\n--- a/dts/framework/testbed_model/capturing_traffic_generator.py\n+++ b/dts/framework/testbed_model/traffic_generator/capturing_traffic_generator.py\n@@ -16,9 +16,9 @@\n from scapy.packet import Packet  # type: ignore[import]\n \n from framework.settings import SETTINGS\n+from framework.testbed_model.port import Port\n from framework.utils import get_packet_summaries\n \n-from .hw.port import Port\n from .traffic_generator import TrafficGenerator\n \n \n@@ -130,7 +130,9 @@ def _send_packets_and_capture(\n         for the specified duration. It must be able to handle no received packets.\n         \"\"\"\n \n-    def _write_capture_from_packets(self, capture_name: str, packets: list[Packet]):\n+    def _write_capture_from_packets(\n+        self, capture_name: str, packets: list[Packet]\n+    ) -> None:\n         file_name = f\"{SETTINGS.output_dir}/{capture_name}.pcap\"\n         self._logger.debug(f\"Writing packets to {file_name}.\")\n         scapy.utils.wrpcap(file_name, packets)\ndiff --git a/dts/framework/testbed_model/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py\nsimilarity index 95%\nrename from dts/framework/testbed_model/scapy.py\nrename to dts/framework/testbed_model/traffic_generator/scapy.py\nindex af0d4dbb25..51864b6e6b 100644\n--- a/dts/framework/testbed_model/scapy.py\n+++ b/dts/framework/testbed_model/traffic_generator/scapy.py\n@@ -24,16 +24,15 @@\n from scapy.packet import Packet  # type: ignore[import]\n \n from framework.config import OS, ScapyTrafficGeneratorConfig\n-from framework.logger import DTSLOG, getLogger\n from framework.remote_session import PythonShell\n from framework.settings import SETTINGS\n+from framework.testbed_model.node import Node\n+from framework.testbed_model.port import Port\n \n from .capturing_traffic_generator import (\n     CapturingTrafficGenerator,\n     _get_default_capture_name,\n )\n-from .hw.port import Port\n-from .tg_node import TGNode\n \n \"\"\"\n ========= BEGIN RPC FUNCTIONS =========\n@@ -146,7 +145,7 @@ def quit(self) -> None:\n         self._BaseServer__shutdown_request = True\n         return None\n \n-    def add_rpc_function(self, name: str, function_bytes: xmlrpc.client.Binary):\n+    def add_rpc_function(self, name: str, function_bytes: xmlrpc.client.Binary) -> None:\n         \"\"\"Add a function to the server.\n \n         This is meant to be executed remotely.\n@@ -191,15 +190,9 @@ class ScapyTrafficGenerator(CapturingTrafficGenerator):\n     session: PythonShell\n     rpc_server_proxy: xmlrpc.client.ServerProxy\n     _config: ScapyTrafficGeneratorConfig\n-    _tg_node: TGNode\n-    _logger: DTSLOG\n-\n-    def __init__(self, tg_node: TGNode, config: ScapyTrafficGeneratorConfig):\n-        self._config = config\n-        self._tg_node = tg_node\n-        self._logger = getLogger(\n-            f\"{self._tg_node.name} {self._config.traffic_generator_type}\"\n-        )\n+\n+    def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):\n+        super().__init__(tg_node, config)\n \n         assert (\n             self._tg_node.config.os == OS.linux\n@@ -235,7 +228,7 @@ def __init__(self, tg_node: TGNode, config: ScapyTrafficGeneratorConfig):\n             function_bytes = marshal.dumps(function.__code__)\n             self.rpc_server_proxy.add_rpc_function(function.__name__, function_bytes)\n \n-    def _start_xmlrpc_server_in_remote_python(self, listen_port: int):\n+    def _start_xmlrpc_server_in_remote_python(self, listen_port: int) -> None:\n         # load the source of the function\n         src = inspect.getsource(QuittableXMLRPCServer)\n         # Lines with only whitespace break the repl if in the middle of a function\n@@ -280,7 +273,7 @@ def _send_packets_and_capture(\n         scapy_packets = [Ether(packet.data) for packet in xmlrpc_packets]\n         return scapy_packets\n \n-    def close(self):\n+    def close(self) -> None:\n         try:\n             self.rpc_server_proxy.quit()\n         except ConnectionRefusedError:\ndiff --git a/dts/framework/testbed_model/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py\nsimilarity index 80%\nrename from dts/framework/testbed_model/traffic_generator.py\nrename to dts/framework/testbed_model/traffic_generator/traffic_generator.py\nindex 28c35d3ce4..ea7c3963da 100644\n--- a/dts/framework/testbed_model/traffic_generator.py\n+++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py\n@@ -12,11 +12,12 @@\n \n from scapy.packet import Packet  # type: ignore[import]\n \n-from framework.logger import DTSLOG\n+from framework.config import TrafficGeneratorConfig\n+from framework.logger import DTSLOG, getLogger\n+from framework.testbed_model.node import Node\n+from framework.testbed_model.port import Port\n from framework.utils import get_packet_summaries\n \n-from .hw.port import Port\n-\n \n class TrafficGenerator(ABC):\n     \"\"\"The base traffic generator.\n@@ -24,8 +25,17 @@ class TrafficGenerator(ABC):\n     Defines the few basic methods that each traffic generator must implement.\n     \"\"\"\n \n+    _config: TrafficGeneratorConfig\n+    _tg_node: Node\n     _logger: DTSLOG\n \n+    def __init__(self, tg_node: Node, config: TrafficGeneratorConfig):\n+        self._config = config\n+        self._tg_node = tg_node\n+        self._logger = getLogger(\n+            f\"{self._tg_node.name} {self._config.traffic_generator_type}\"\n+        )\n+\n     def send_packet(self, packet: Packet, port: Port) -> None:\n         \"\"\"Send a packet and block until it is fully sent.\n \ndiff --git a/dts/framework/testbed_model/hw/virtual_device.py b/dts/framework/testbed_model/virtual_device.py\nsimilarity index 100%\nrename from dts/framework/testbed_model/hw/virtual_device.py\nrename to dts/framework/testbed_model/virtual_device.py\ndiff --git a/dts/framework/utils.py b/dts/framework/utils.py\nindex d27c2c5b5f..f0c916471c 100644\n--- a/dts/framework/utils.py\n+++ b/dts/framework/utils.py\n@@ -7,7 +7,6 @@\n import json\n import os\n import subprocess\n-import sys\n from enum import Enum\n from pathlib import Path\n from subprocess import SubprocessError\n@@ -16,35 +15,7 @@\n \n from .exception import ConfigurationError\n \n-\n-class StrEnum(Enum):\n-    @staticmethod\n-    def _generate_next_value_(\n-        name: str, start: int, count: int, last_values: object\n-    ) -> str:\n-        return name\n-\n-    def __str__(self) -> str:\n-        return self.name\n-\n-\n-REGEX_FOR_PCI_ADDRESS = \"/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/\"\n-\n-\n-def check_dts_python_version() -> None:\n-    if sys.version_info.major < 3 or (\n-        sys.version_info.major == 3 and sys.version_info.minor < 10\n-    ):\n-        print(\n-            RED(\n-                (\n-                    \"WARNING: DTS execution node's python version is lower than\"\n-                    \"python 3.10, is deprecated and will not work in future releases.\"\n-                )\n-            ),\n-            file=sys.stderr,\n-        )\n-        print(RED(\"Please use Python >= 3.10 instead\"), file=sys.stderr)\n+REGEX_FOR_PCI_ADDRESS: str = \"/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/\"\n \n \n def expand_range(range_str: str) -> list[int]:\n@@ -67,7 +38,7 @@ def expand_range(range_str: str) -> list[int]:\n     return expanded_range\n \n \n-def get_packet_summaries(packets: list[Packet]):\n+def get_packet_summaries(packets: list[Packet]) -> str:\n     if len(packets) == 1:\n         packet_summaries = packets[0].summary()\n     else:\n@@ -77,8 +48,15 @@ def get_packet_summaries(packets: list[Packet]):\n     return f\"Packet contents: \\n{packet_summaries}\"\n \n \n-def RED(text: str) -> str:\n-    return f\"\\u001B[31;1m{str(text)}\\u001B[0m\"\n+class StrEnum(Enum):\n+    @staticmethod\n+    def _generate_next_value_(\n+        name: str, start: int, count: int, last_values: object\n+    ) -> str:\n+        return name\n+\n+    def __str__(self) -> str:\n+        return self.name\n \n \n class MesonArgs(object):\n@@ -225,5 +203,5 @@ def _delete_tarball(self) -> None:\n         if self._tarball_path and os.path.exists(self._tarball_path):\n             os.remove(self._tarball_path)\n \n-    def __fspath__(self):\n+    def __fspath__(self) -> str:\n         return str(self._tarball_path)\ndiff --git a/dts/main.py b/dts/main.py\nindex 43311fa847..5d4714b0c3 100755\n--- a/dts/main.py\n+++ b/dts/main.py\n@@ -10,10 +10,17 @@\n \n import logging\n \n-from framework import dts\n+from framework import settings\n \n \n def main() -> None:\n+    \"\"\"Set DTS settings, then run DTS.\n+\n+    The DTS settings are taken from the command line arguments and the environment variables.\n+    \"\"\"\n+    settings.SETTINGS = settings.get_settings()\n+    from framework import dts\n+\n     dts.run_all()\n \n \n",
    "prefixes": [
        "v5",
        "01/23"
    ]
}