get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

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

{
    "id": 140005,
    "url": "http://patchwork.dpdk.org/api/patches/140005/?format=api",
    "web_url": "http://patchwork.dpdk.org/project/dpdk/patch/20240509112057.1167947-8-luca.vizzarro@arm.com/",
    "project": {
        "id": 1,
        "url": "http://patchwork.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": "<20240509112057.1167947-8-luca.vizzarro@arm.com>",
    "list_archive_url": "https://inbox.dpdk.org/dev/20240509112057.1167947-8-luca.vizzarro@arm.com",
    "date": "2024-05-09T11:20:56",
    "name": "[v2,7/8] dts: rework interactive shells",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "0c46d2d262def47334c8a995a7a703e552e1a0fa",
    "submitter": {
        "id": 3197,
        "url": "http://patchwork.dpdk.org/api/people/3197/?format=api",
        "name": "Luca Vizzarro",
        "email": "luca.vizzarro@arm.com"
    },
    "delegate": {
        "id": 1,
        "url": "http://patchwork.dpdk.org/api/users/1/?format=api",
        "username": "tmonjalo",
        "first_name": "Thomas",
        "last_name": "Monjalon",
        "email": "thomas@monjalon.net"
    },
    "mbox": "http://patchwork.dpdk.org/project/dpdk/patch/20240509112057.1167947-8-luca.vizzarro@arm.com/mbox/",
    "series": [
        {
            "id": 31897,
            "url": "http://patchwork.dpdk.org/api/series/31897/?format=api",
            "web_url": "http://patchwork.dpdk.org/project/dpdk/list/?series=31897",
            "date": "2024-05-09T11:20:49",
            "name": "dts: add testpmd params",
            "version": 2,
            "mbox": "http://patchwork.dpdk.org/series/31897/mbox/"
        }
    ],
    "comments": "http://patchwork.dpdk.org/api/patches/140005/comments/",
    "check": "success",
    "checks": "http://patchwork.dpdk.org/api/patches/140005/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 8AD2543F7C;\n\tThu,  9 May 2024 13:22:05 +0200 (CEST)",
            "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id B841840A7A;\n\tThu,  9 May 2024 13:21:22 +0200 (CEST)",
            "from foss.arm.com (foss.arm.com [217.140.110.172])\n by mails.dpdk.org (Postfix) with ESMTP id 22DD4402F1\n for <dev@dpdk.org>; Thu,  9 May 2024 13:21:16 +0200 (CEST)",
            "from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14])\n by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 3771312FC;\n Thu,  9 May 2024 04:21:41 -0700 (PDT)",
            "from localhost.localdomain (unknown [10.1.194.74])\n by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 0BF7A3F6A8;\n Thu,  9 May 2024 04:21:14 -0700 (PDT)"
        ],
        "From": "Luca Vizzarro <luca.vizzarro@arm.com>",
        "To": "dev@dpdk.org",
        "Cc": "=?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>,\n Jeremy Spewock <jspewock@iol.unh.edu>, Luca Vizzarro <luca.vizzarro@arm.com>,\n Paul Szczepanek <paul.szczepanek@arm.com>",
        "Subject": "[PATCH v2 7/8] dts: rework interactive shells",
        "Date": "Thu,  9 May 2024 12:20:56 +0100",
        "Message-Id": "<20240509112057.1167947-8-luca.vizzarro@arm.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20240509112057.1167947-1-luca.vizzarro@arm.com>",
        "References": "<20240326190422.577028-1-luca.vizzarro@arm.com>\n <20240509112057.1167947-1-luca.vizzarro@arm.com>",
        "MIME-Version": "1.0",
        "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 way nodes and interactive shells interact makes it difficult to\ndevelop for static type checking and hinting. The current system relies\non a top-down approach, attempting to give a generic interface to the\ntest developer, hiding the interaction of concrete shell classes as much\nas possible. When working with strong typing this approach is not ideal,\nas Python's implementation of generics is still rudimentary.\n\nThis rework reverses the tests interaction to a bottom-up approach,\nallowing the test developer to call concrete shell classes directly,\nand let them ingest nodes independently. While also re-enforcing type\nchecking and making the code easier to read.\n\nSigned-off-by: Luca Vizzarro <luca.vizzarro@arm.com>\nReviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>\n---\n dts/framework/params/eal.py                   |   6 +-\n dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++\n .../remote_session/interactive_shell.py       |  75 +++++++-----\n dts/framework/remote_session/python_shell.py  |   4 +-\n dts/framework/remote_session/testpmd_shell.py |  64 +++++-----\n dts/framework/testbed_model/node.py           |  36 +-----\n dts/framework/testbed_model/os_session.py     |  36 +-----\n dts/framework/testbed_model/sut_node.py       | 112 +-----------------\n .../testbed_model/traffic_generator/scapy.py  |   4 +-\n dts/tests/TestSuite_hello_world.py            |   7 +-\n dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--\n dts/tests/TestSuite_smoke_tests.py            |   2 +-\n 12 files changed, 201 insertions(+), 270 deletions(-)\n create mode 100644 dts/framework/remote_session/dpdk_shell.py",
    "diff": "diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py\nindex bbdbc8f334..8d7766fefc 100644\n--- a/dts/framework/params/eal.py\n+++ b/dts/framework/params/eal.py\n@@ -35,9 +35,9 @@ class EalParams(Params):\n                 ``other_eal_param='--single-file-segments'``\n     \"\"\"\n \n-    lcore_list: LogicalCoreList = field(metadata=Params.short(\"l\"))\n-    memory_channels: int = field(metadata=Params.short(\"n\"))\n-    prefix: str = field(metadata=Params.long(\"file-prefix\"))\n+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short(\"l\"))\n+    memory_channels: int | None = field(default=None, metadata=Params.short(\"n\"))\n+    prefix: str = field(default=\"dpdk\", metadata=Params.long(\"file-prefix\"))\n     no_pci: Switch = None\n     vdevs: list[VirtualDevice] | None = field(\n         default=None, metadata=Params.multiple() | Params.long(\"vdev\")\ndiff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py\nnew file mode 100644\nindex 0000000000..78caae36ea\n--- /dev/null\n+++ b/dts/framework/remote_session/dpdk_shell.py\n@@ -0,0 +1,104 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2024 Arm Limited\n+\n+\"\"\"DPDK-based interactive shell.\n+\n+Provides a base class to create interactive shells based on DPDK.\n+\"\"\"\n+\n+\n+from abc import ABC\n+\n+from framework.params.eal import EalParams\n+from framework.remote_session.interactive_shell import InteractiveShell\n+from framework.settings import SETTINGS\n+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList\n+from framework.testbed_model.sut_node import SutNode\n+\n+\n+def compute_eal_params(\n+    node: SutNode,\n+    params: EalParams | None = None,\n+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),\n+    ascending_cores: bool = True,\n+    append_prefix_timestamp: bool = True,\n+) -> EalParams:\n+    \"\"\"Compute EAL parameters based on the node's specifications.\n+\n+    Args:\n+        node: The SUT node to compute the values for.\n+        params: The EalParams object to amend, if set to None a new object is created and returned.\n+        lcore_filter_specifier: A number of lcores/cores/sockets to use\n+            or a list of lcore ids to use.\n+            The default will select one lcore for each of two cores\n+            on one socket, in ascending order of core ids.\n+        ascending_cores: Sort cores in ascending order (lowest to highest IDs).\n+            If :data:`False`, sort in descending order.\n+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.\n+    \"\"\"\n+    if params is None:\n+        params = EalParams()\n+\n+    if params.lcore_list is None:\n+        params.lcore_list = LogicalCoreList(\n+            node.filter_lcores(lcore_filter_specifier, ascending_cores)\n+        )\n+\n+    prefix = params.prefix\n+    if append_prefix_timestamp:\n+        prefix = f\"{prefix}_{node._dpdk_timestamp}\"\n+    prefix = node.main_session.get_dpdk_file_prefix(prefix)\n+    if prefix:\n+        node._dpdk_prefix_list.append(prefix)\n+    params.prefix = prefix\n+\n+    if params.ports is None:\n+        params.ports = node.ports\n+\n+    return params\n+\n+\n+class DPDKShell(InteractiveShell, ABC):\n+    \"\"\"The base class for managing DPDK-based interactive shells.\n+\n+    This class shouldn't be instantiated directly, but instead be extended.\n+    It automatically injects computed EAL parameters based on the node in the\n+    supplied app parameters.\n+    \"\"\"\n+\n+    _node: SutNode\n+    _app_params: EalParams\n+\n+    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList\n+    _ascending_cores: bool\n+    _append_prefix_timestamp: bool\n+\n+    def __init__(\n+        self,\n+        node: SutNode,\n+        app_params: EalParams,\n+        privileged: bool = True,\n+        timeout: float = SETTINGS.timeout,\n+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),\n+        ascending_cores: bool = True,\n+        append_prefix_timestamp: bool = True,\n+        start_on_init: bool = True,\n+    ) -> None:\n+        \"\"\"Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`.\"\"\"\n+        self._lcore_filter_specifier = lcore_filter_specifier\n+        self._ascending_cores = ascending_cores\n+        self._append_prefix_timestamp = append_prefix_timestamp\n+\n+        super().__init__(node, app_params, privileged, timeout, start_on_init)\n+\n+    def __post_init__(self):\n+        \"\"\"Computes EAL params based on the node capabilities before start.\"\"\"\n+        self._app_params = compute_eal_params(\n+            self._node,\n+            self._app_params,\n+            self._lcore_filter_specifier,\n+            self._ascending_cores,\n+            self._append_prefix_timestamp,\n+        )\n+\n+        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))\ndiff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py\nindex 9da66d1c7e..8163c8f247 100644\n--- a/dts/framework/remote_session/interactive_shell.py\n+++ b/dts/framework/remote_session/interactive_shell.py\n@@ -17,13 +17,14 @@\n \n from abc import ABC\n from pathlib import PurePath\n-from typing import Callable, ClassVar\n+from typing import ClassVar\n \n-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]\n+from paramiko import Channel, channel  # type: ignore[import-untyped]\n \n from framework.logger import DTSLogger\n from framework.params import Params\n from framework.settings import SETTINGS\n+from framework.testbed_model.node import Node\n \n \n class InteractiveShell(ABC):\n@@ -36,13 +37,14 @@ class InteractiveShell(ABC):\n     session.\n     \"\"\"\n \n-    _interactive_session: SSHClient\n+    _node: Node\n     _stdin: channel.ChannelStdinFile\n     _stdout: channel.ChannelFile\n     _ssh_channel: Channel\n     _logger: DTSLogger\n     _timeout: float\n     _app_params: Params\n+    _privileged: bool\n \n     #: Prompt to expect at the end of output when sending a command.\n     #: This is often overridden by subclasses.\n@@ -56,57 +58,66 @@ class InteractiveShell(ABC):\n     #: Path to the executable to start the interactive application.\n     path: ClassVar[PurePath]\n \n-    #: Whether this application is a DPDK app. If it is, the build directory\n-    #: for DPDK on the node will be prepended to the path to the executable.\n-    dpdk_app: ClassVar[bool] = False\n-\n     def __init__(\n         self,\n-        interactive_session: SSHClient,\n-        logger: DTSLogger,\n-        get_privileged_command: Callable[[str], str] | None,\n+        node: Node,\n         app_params: Params = Params(),\n+        privileged: bool = False,\n         timeout: float = SETTINGS.timeout,\n+        start_on_init: bool = True,\n     ) -> None:\n         \"\"\"Create an SSH channel during initialization.\n \n         Args:\n-            interactive_session: The SSH session dedicated to interactive shells.\n-            logger: The logger instance this session will use.\n-            get_privileged_command: A method for modifying a command to allow it to use\n-                elevated privileges. If :data:`None`, the application will not be started\n-                with elevated privileges.\n+            node: The node on which to run start the interactive shell.\n             app_params: The command line parameters to be passed to the application on startup.\n+            privileged: Enables the shell to run as superuser.\n             timeout: The timeout used for the SSH channel that is dedicated to this interactive\n                 shell. This timeout is for collecting output, so if reading from the buffer\n                 and no output is gathered within the timeout, an exception is thrown.\n+            start_on_init: Start interactive shell automatically after object initialisation.\n         \"\"\"\n-        self._interactive_session = interactive_session\n-        self._ssh_channel = self._interactive_session.invoke_shell()\n+        self._node = node\n+        self._logger = node._logger\n+        self._app_params = app_params\n+        self._privileged = privileged\n+        self._timeout = timeout\n+        # Ensure path is properly formatted for the host\n+        self._update_path(self._node.main_session.join_remote_path(self.path))\n+\n+        self.__post_init__()\n+\n+        if start_on_init:\n+            self.start_application()\n+\n+    def __post_init__(self):\n+        \"\"\"Overridable. Method called after the object init and before application start.\"\"\"\n+        pass\n+\n+    def _setup_ssh_channel(self):\n+        self._ssh_channel = self._node.main_session.interactive_session.session.invoke_shell()\n         self._stdin = self._ssh_channel.makefile_stdin(\"w\")\n         self._stdout = self._ssh_channel.makefile(\"r\")\n-        self._ssh_channel.settimeout(timeout)\n+        self._ssh_channel.settimeout(self._timeout)\n         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams\n-        self._logger = logger\n-        self._timeout = timeout\n-        self._app_params = app_params\n-        self._start_application(get_privileged_command)\n \n-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:\n+    def start_application(self) -> None:\n         \"\"\"Starts a new interactive application based on the path to the app.\n \n         This method is often overridden by subclasses as their process for\n         starting may look different.\n-\n-        Args:\n-            get_privileged_command: A function (but could be any callable) that produces\n-                the version of the command with elevated privileges.\n         \"\"\"\n-        start_command = f\"{self.path} {self._app_params}\"\n-        if get_privileged_command is not None:\n-            start_command = get_privileged_command(start_command)\n+        self._setup_ssh_channel()\n+\n+        start_command = self._make_start_command()\n+        if self._privileged:\n+            start_command = self._node.main_session._get_privileged_command(start_command)\n         self.send_command(start_command)\n \n+    def _make_start_command(self) -> str:\n+        \"\"\"Makes the command that starts the interactive shell.\"\"\"\n+        return f\"{self.path} {self._app_params or ''}\"\n+\n     def send_command(self, command: str, prompt: str | None = None) -> str:\n         \"\"\"Send `command` and get all output before the expected ending string.\n \n@@ -150,3 +161,7 @@ def close(self) -> None:\n     def __del__(self) -> None:\n         \"\"\"Make sure the session is properly closed before deleting the object.\"\"\"\n         self.close()\n+\n+    @classmethod\n+    def _update_path(cls, path: PurePath) -> None:\n+        cls.path = path\ndiff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py\nindex ccfd3783e8..953ed100df 100644\n--- a/dts/framework/remote_session/python_shell.py\n+++ b/dts/framework/remote_session/python_shell.py\n@@ -6,9 +6,7 @@\n Typical usage example in a TestSuite::\n \n     from framework.remote_session import PythonShell\n-    python_shell = self.tg_node.create_interactive_shell(\n-        PythonShell, timeout=5, privileged=True\n-    )\n+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)\n     python_shell.send_command(\"print('Hello World')\")\n     python_shell.close()\n \"\"\"\ndiff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py\nindex ef3f23c582..92930d7fbb 100644\n--- a/dts/framework/remote_session/testpmd_shell.py\n+++ b/dts/framework/remote_session/testpmd_shell.py\n@@ -7,9 +7,7 @@\n \n Typical usage example in a TestSuite::\n \n-    testpmd_shell = self.sut_node.create_interactive_shell(\n-            TestPmdShell, privileged=True\n-        )\n+    testpmd_shell = TestPmdShell()\n     devices = testpmd_shell.get_devices()\n     for device in devices:\n         print(device)\n@@ -18,13 +16,14 @@\n \n import time\n from pathlib import PurePath\n-from typing import Callable, ClassVar\n+from typing import ClassVar\n \n from framework.exception import InteractiveCommandExecutionError\n from framework.params.testpmd import SimpleForwardingModes, TestPmdParams\n+from framework.remote_session.dpdk_shell import DPDKShell\n from framework.settings import SETTINGS\n-\n-from .interactive_shell import InteractiveShell\n+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList\n+from framework.testbed_model.sut_node import SutNode\n \n \n class TestPmdDevice(object):\n@@ -49,52 +48,48 @@ def __str__(self) -> str:\n         return self.pci_address\n \n \n-class TestPmdShell(InteractiveShell):\n+class TestPmdShell(DPDKShell):\n     \"\"\"Testpmd interactive shell.\n \n     The testpmd shell users should never use\n     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather\n     call specialized methods. If there isn't one that satisfies a need, it should be added.\n-\n-    Attributes:\n-        number_of_ports: The number of ports which were allowed on the command-line when testpmd\n-            was started.\n     \"\"\"\n \n-    number_of_ports: int\n+    _app_params: TestPmdParams\n \n     #: The path to the testpmd executable.\n     path: ClassVar[PurePath] = PurePath(\"app\", \"dpdk-testpmd\")\n \n-    #: Flag this as a DPDK app so that it's clear this is not a system app and\n-    #: needs to be looked in a specific path.\n-    dpdk_app: ClassVar[bool] = True\n-\n     #: The testpmd's prompt.\n     _default_prompt: ClassVar[str] = \"testpmd>\"\n \n     #: This forces the prompt to appear after sending a command.\n     _command_extra_chars: ClassVar[str] = \"\\n\"\n \n-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:\n-        \"\"\"Overrides :meth:`~.interactive_shell._start_application`.\n-\n-        Add flags for starting testpmd in interactive mode and disabling messages for link state\n-        change events before starting the application. Link state is verified before starting\n-        packet forwarding and the messages create unexpected newlines in the terminal which\n-        complicates output collection.\n-\n-        Also find the number of pci addresses which were allowed on the command line when the app\n-        was started.\n-        \"\"\"\n-        assert isinstance(self._app_params, TestPmdParams)\n-\n-        self.number_of_ports = (\n-            len(self._app_params.ports) if self._app_params.ports is not None else 0\n+    def __init__(\n+        self,\n+        node: SutNode,\n+        privileged: bool = True,\n+        timeout: float = SETTINGS.timeout,\n+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),\n+        ascending_cores: bool = True,\n+        append_prefix_timestamp: bool = True,\n+        start_on_init: bool = True,\n+        **app_params,\n+    ) -> None:\n+        \"\"\"Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs.\"\"\"\n+        super().__init__(\n+            node,\n+            TestPmdParams(**app_params),\n+            privileged,\n+            timeout,\n+            lcore_filter_specifier,\n+            ascending_cores,\n+            append_prefix_timestamp,\n+            start_on_init,\n         )\n \n-        super()._start_application(get_privileged_command)\n-\n     def start(self, verify: bool = True) -> None:\n         \"\"\"Start packet forwarding with the current configuration.\n \n@@ -114,7 +109,8 @@ def start(self, verify: bool = True) -> None:\n                 self._logger.debug(f\"Failed to start packet forwarding: \\n{start_cmd_output}\")\n                 raise InteractiveCommandExecutionError(\"Testpmd failed to start packet forwarding.\")\n \n-            for port_id in range(self.number_of_ports):\n+            number_of_ports = len(self._app_params.ports or [])\n+            for port_id in range(number_of_ports):\n                 if not self.wait_link_status_up(port_id):\n                     raise InteractiveCommandExecutionError(\n                         \"Not all ports came up after starting packet forwarding in testpmd.\"\ndiff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py\nindex 6af4f25a3c..88395faabe 100644\n--- a/dts/framework/testbed_model/node.py\n+++ b/dts/framework/testbed_model/node.py\n@@ -15,7 +15,7 @@\n \n from abc import ABC\n from ipaddress import IPv4Interface, IPv6Interface\n-from typing import Any, Callable, Type, Union\n+from typing import Any, Callable, Union\n \n from framework.config import (\n     OS,\n@@ -25,7 +25,6 @@\n )\n from framework.exception import ConfigurationError\n from framework.logger import DTSLogger, get_dts_logger\n-from framework.params import Params\n from framework.settings import SETTINGS\n \n from .cpu import (\n@@ -36,7 +35,7 @@\n     lcore_filter,\n )\n from .linux_session import LinuxSession\n-from .os_session import InteractiveShellType, OSSession\n+from .os_session import OSSession\n from .port import Port\n from .virtual_device import VirtualDevice\n \n@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:\n         self._other_sessions.append(connection)\n         return connection\n \n-    def create_interactive_shell(\n-        self,\n-        shell_cls: Type[InteractiveShellType],\n-        timeout: float = SETTINGS.timeout,\n-        privileged: bool = False,\n-        app_params: Params = Params(),\n-    ) -> InteractiveShellType:\n-        \"\"\"Factory for interactive session handlers.\n-\n-        Instantiate `shell_cls` according to the remote OS specifics.\n-\n-        Args:\n-            shell_cls: The class of the shell.\n-            timeout: Timeout for reading output from the SSH channel. If you are reading from\n-                the buffer and don't receive any data within the timeout it will throw an error.\n-            privileged: Whether to run the shell with administrative privileges.\n-            app_args: The arguments to be passed to the application.\n-\n-        Returns:\n-            An instance of the desired interactive application shell.\n-        \"\"\"\n-        if not shell_cls.dpdk_app:\n-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)\n-\n-        return self.main_session.create_interactive_shell(\n-            shell_cls,\n-            timeout,\n-            privileged,\n-            app_params,\n-        )\n-\n     def filter_lcores(\n         self,\n         filter_specifier: LogicalCoreCount | LogicalCoreList,\ndiff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py\nindex e5f5fcbe0e..e7e6c9d670 100644\n--- a/dts/framework/testbed_model/os_session.py\n+++ b/dts/framework/testbed_model/os_session.py\n@@ -26,18 +26,16 @@\n from collections.abc import Iterable\n from ipaddress import IPv4Interface, IPv6Interface\n from pathlib import PurePath\n-from typing import Type, TypeVar, Union\n+from typing import Union\n \n from framework.config import Architecture, NodeConfiguration, NodeInfo\n from framework.logger import DTSLogger\n-from framework.params import Params\n from framework.remote_session import (\n     InteractiveRemoteSession,\n     RemoteSession,\n     create_interactive_session,\n     create_remote_session,\n )\n-from framework.remote_session.interactive_shell import InteractiveShell\n from framework.remote_session.remote_session import CommandResult\n from framework.settings import SETTINGS\n from framework.utils import MesonArgs\n@@ -45,8 +43,6 @@\n from .cpu import LogicalCore\n from .port import Port\n \n-InteractiveShellType = TypeVar(\"InteractiveShellType\", bound=InteractiveShell)\n-\n \n class OSSession(ABC):\n     \"\"\"OS-unaware to OS-aware translation API definition.\n@@ -131,36 +127,6 @@ def send_command(\n \n         return self.remote_session.send_command(command, timeout, verify, env)\n \n-    def create_interactive_shell(\n-        self,\n-        shell_cls: Type[InteractiveShellType],\n-        timeout: float,\n-        privileged: bool,\n-        app_args: Params,\n-    ) -> InteractiveShellType:\n-        \"\"\"Factory for interactive session handlers.\n-\n-        Instantiate `shell_cls` according to the remote OS specifics.\n-\n-        Args:\n-            shell_cls: The class of the shell.\n-            timeout: Timeout for reading output from the SSH channel. If you are\n-                reading from the buffer and don't receive any data within the timeout\n-                it will throw an error.\n-            privileged: Whether to run the shell with administrative privileges.\n-            app_args: The arguments to be passed to the application.\n-\n-        Returns:\n-            An instance of the desired interactive application shell.\n-        \"\"\"\n-        return shell_cls(\n-            self.interactive_session.session,\n-            self._logger,\n-            self._get_privileged_command if privileged else None,\n-            app_args,\n-            timeout,\n-        )\n-\n     @staticmethod\n     @abstractmethod\n     def _get_privileged_command(command: str) -> str:\ndiff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py\nindex 83ad06ae2d..727170b7fc 100644\n--- a/dts/framework/testbed_model/sut_node.py\n+++ b/dts/framework/testbed_model/sut_node.py\n@@ -16,7 +16,6 @@\n import tarfile\n import time\n from pathlib import PurePath\n-from typing import Type\n \n from framework.config import (\n     BuildTargetConfiguration,\n@@ -24,17 +23,13 @@\n     NodeInfo,\n     SutNodeConfiguration,\n )\n-from framework.params import Params, Switch\n from framework.params.eal import EalParams\n from framework.remote_session.remote_session import CommandResult\n from framework.settings import SETTINGS\n from framework.utils import MesonArgs\n \n-from .cpu import LogicalCoreCount, LogicalCoreList\n from .node import Node\n-from .os_session import InteractiveShellType, OSSession\n-from .port import Port\n-from .virtual_device import VirtualDevice\n+from .os_session import OSSession\n \n \n class SutNode(Node):\n@@ -289,68 +284,6 @@ def kill_cleanup_dpdk_apps(self) -> None:\n             self._dpdk_kill_session = self.create_session(\"dpdk_kill\")\n         self._dpdk_prefix_list = []\n \n-    def create_eal_parameters(\n-        self,\n-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),\n-        ascending_cores: bool = True,\n-        prefix: str = \"dpdk\",\n-        append_prefix_timestamp: bool = True,\n-        no_pci: Switch = None,\n-        vdevs: list[VirtualDevice] | None = None,\n-        ports: list[Port] | None = None,\n-        other_eal_param: str = \"\",\n-    ) -> EalParams:\n-        \"\"\"Compose the EAL parameters.\n-\n-        Process the list of cores and the DPDK prefix and pass that along with\n-        the rest of the arguments.\n-\n-        Args:\n-            lcore_filter_specifier: A number of lcores/cores/sockets to use\n-                or a list of lcore ids to use.\n-                The default will select one lcore for each of two cores\n-                on one socket, in ascending order of core ids.\n-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).\n-                If :data:`False`, sort in descending order.\n-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.\n-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.\n-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.\n-            vdevs: Virtual devices, e.g.::\n-\n-                vdevs=[\n-                    VirtualDevice('net_ring0'),\n-                    VirtualDevice('net_ring1')\n-                ]\n-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`\n-                will be allowed.\n-            other_eal_param: user defined DPDK EAL parameters, e.g.:\n-                ``other_eal_param='--single-file-segments'``.\n-\n-        Returns:\n-            An EAL param string, such as\n-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.\n-        \"\"\"\n-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))\n-\n-        if append_prefix_timestamp:\n-            prefix = f\"{prefix}_{self._dpdk_timestamp}\"\n-        prefix = self.main_session.get_dpdk_file_prefix(prefix)\n-        if prefix:\n-            self._dpdk_prefix_list.append(prefix)\n-\n-        if ports is None:\n-            ports = self.ports\n-\n-        return EalParams(\n-            lcore_list=lcore_list,\n-            memory_channels=self.config.memory_channels,\n-            prefix=prefix,\n-            no_pci=no_pci,\n-            vdevs=vdevs,\n-            ports=ports,\n-            other_eal_param=Params.from_str(other_eal_param),\n-        )\n-\n     def run_dpdk_app(\n         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30\n     ) -> CommandResult:\n@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:\n         \"\"\"\n         self.main_session.configure_ipv4_forwarding(enable)\n \n-    def create_interactive_shell(\n-        self,\n-        shell_cls: Type[InteractiveShellType],\n-        timeout: float = SETTINGS.timeout,\n-        privileged: bool = False,\n-        app_params: Params = Params(),\n-        eal_params: EalParams | None = None,\n-    ) -> InteractiveShellType:\n-        \"\"\"Extend the factory for interactive session handlers.\n-\n-        The extensions are SUT node specific:\n-\n-            * The default for `eal_parameters`,\n-            * The interactive shell path `shell_cls.path` is prepended with path to the remote\n-              DPDK build directory for DPDK apps.\n-\n-        Args:\n-            shell_cls: The class of the shell.\n-            timeout: Timeout for reading output from the SSH channel. If you are\n-                reading from the buffer and don't receive any data within the timeout\n-                it will throw an error.\n-            privileged: Whether to run the shell with administrative privileges.\n-            app_params: The parameters to be passed to the application.\n-            eal_params: List of EAL parameters to use to launch the app. If this\n-                isn't provided or an empty string is passed, it will default to calling\n-                :meth:`create_eal_parameters`.\n-\n-        Returns:\n-            An instance of the desired interactive application shell.\n-        \"\"\"\n-        # We need to append the build directory and add EAL parameters for DPDK apps\n-        if shell_cls.dpdk_app:\n-            if eal_params is None:\n-                eal_params = self.create_eal_parameters()\n-            eal_params.append_str(str(app_params))\n-            app_params = eal_params\n-\n-            shell_cls.path = self.main_session.join_remote_path(\n-                self.remote_dpdk_build_dir, shell_cls.path\n-            )\n-\n-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)\n-\n     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:\n         \"\"\"Bind all ports on the SUT to a driver.\n \ndiff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py\nindex 7bc1c2cc08..bf58ad1c5e 100644\n--- a/dts/framework/testbed_model/traffic_generator/scapy.py\n+++ b/dts/framework/testbed_model/traffic_generator/scapy.py\n@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):\n             self._tg_node.config.os == OS.linux\n         ), \"Linux is the only supported OS for scapy traffic generation\"\n \n-        self.session = self._tg_node.create_interactive_shell(\n-            PythonShell, timeout=5, privileged=True\n-        )\n+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)\n \n         # import libs in remote python console\n         for import_statement in SCAPY_RPC_SERVER_IMPORTS:\ndiff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py\nindex 0d6995f260..d958f99030 100644\n--- a/dts/tests/TestSuite_hello_world.py\n+++ b/dts/tests/TestSuite_hello_world.py\n@@ -7,6 +7,7 @@\n No other EAL parameters apart from cores are used.\n \"\"\"\n \n+from framework.remote_session.dpdk_shell import compute_eal_params\n from framework.test_suite import TestSuite\n from framework.testbed_model.cpu import (\n     LogicalCoreCount,\n@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:\n         # get the first usable core\n         lcore_amount = LogicalCoreCount(1, 1, 1)\n         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()\n-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)\n+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)\n         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)\n         self.verify(\n             f\"hello from core {int(lcores[0])}\" in result.stdout,\n@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:\n             \"hello from core <core_id>\"\n         \"\"\"\n         # get the maximum logical core number\n-        eal_para = self.sut_node.create_eal_parameters(\n-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)\n+        eal_para = compute_eal_params(\n+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)\n         )\n         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)\n         for lcore in self.sut_node.lcores:\ndiff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py\nindex 6d206c1a40..43cf5c61eb 100644\n--- a/dts/tests/TestSuite_pmd_buffer_scatter.py\n+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py\n@@ -16,14 +16,13 @@\n \"\"\"\n \n import struct\n-from dataclasses import asdict\n \n from scapy.layers.inet import IP  # type: ignore[import-untyped]\n from scapy.layers.l2 import Ether  # type: ignore[import-untyped]\n from scapy.packet import Raw  # type: ignore[import-untyped]\n from scapy.utils import hexstr  # type: ignore[import-untyped]\n \n-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams\n+from framework.params.testpmd import SimpleForwardingModes\n from framework.remote_session.testpmd_shell import TestPmdShell\n from framework.test_suite import TestSuite\n \n@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:\n         Test:\n             Start testpmd and run functional test with preset mbsize.\n         \"\"\"\n-        testpmd = self.sut_node.create_interactive_shell(\n-            TestPmdShell,\n-            app_params=TestPmdParams(\n-                forward_mode=SimpleForwardingModes.mac,\n-                mbcache=200,\n-                mbuf_size=[mbsize],\n-                max_pkt_len=9000,\n-                tx_offloads=0x00008000,\n-                **asdict(self.sut_node.create_eal_parameters()),\n-            ),\n-            privileged=True,\n+        testpmd = TestPmdShell(\n+            self.sut_node,\n+            forward_mode=SimpleForwardingModes.mac,\n+            mbcache=200,\n+            mbuf_size=[mbsize],\n+            max_pkt_len=9000,\n+            tx_offloads=0x00008000,\n         )\n         testpmd.start()\n \ndiff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py\nindex ca678f662d..eca27acfd8 100644\n--- a/dts/tests/TestSuite_smoke_tests.py\n+++ b/dts/tests/TestSuite_smoke_tests.py\n@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:\n         Test:\n             List all devices found in testpmd and verify the configured devices are among them.\n         \"\"\"\n-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)\n+        testpmd_driver = TestPmdShell(self.sut_node)\n         dev_list = [str(x) for x in testpmd_driver.get_devices()]\n         for nic in self.nics_in_node:\n             self.verify(\n",
    "prefixes": [
        "v2",
        "7/8"
    ]
}