xref: /dpdk/dts/framework/testbed_model/os_session.py (revision f665790a5dbad7b645ff46f31d65e977324e7bfc)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2023 PANTHEON.tech s.r.o.
3# Copyright(c) 2023 University of New Hampshire
4# Copyright(c) 2024 Arm Limited
5
6"""OS-aware remote session.
7
8DPDK supports multiple different operating systems, meaning it can run on these different operating
9systems. This module defines the common API that OS-unaware layers use and translates the API into
10OS-aware calls/utility usage.
11
12Note:
13    Running commands with administrative privileges requires OS awareness. This is the only layer
14    that's aware of OS differences, so this is where non-privileged command get converted
15    to privileged commands.
16
17Example:
18    A user wishes to remove a directory on a remote :class:`~.sut_node.SutNode`.
19    The :class:`~.sut_node.SutNode` object isn't aware what OS the node is running - it delegates
20    the OS translation logic to :attr:`~.node.Node.main_session`. The SUT node calls
21    :meth:`~OSSession.remove_remote_dir` with a generic, OS-unaware path and
22    the :attr:`~.node.Node.main_session` translates that to ``rm -rf`` if the node's OS is Linux
23    and other commands for other OSs. It also translates the path to match the underlying OS.
24"""
25from abc import ABC, abstractmethod
26from collections.abc import Iterable
27from ipaddress import IPv4Interface, IPv6Interface
28from pathlib import PurePath
29from typing import Union
30
31from framework.config import Architecture, NodeConfiguration, NodeInfo
32from framework.logger import DTSLogger
33from framework.remote_session import (
34    InteractiveRemoteSession,
35    RemoteSession,
36    create_interactive_session,
37    create_remote_session,
38)
39from framework.remote_session.remote_session import CommandResult
40from framework.settings import SETTINGS
41from framework.utils import MesonArgs
42
43from .cpu import LogicalCore
44from .port import Port
45
46
47class OSSession(ABC):
48    """OS-unaware to OS-aware translation API definition.
49
50    The OSSession classes create a remote session to a DTS node and implement OS specific
51    behavior. There a few control methods implemented by the base class, the rest need
52    to be implemented by subclasses.
53
54    Attributes:
55        name: The name of the session.
56        remote_session: The remote session maintaining the connection to the node.
57        interactive_session: The interactive remote session maintaining the connection to the node.
58    """
59
60    _config: NodeConfiguration
61    name: str
62    _logger: DTSLogger
63    remote_session: RemoteSession
64    interactive_session: InteractiveRemoteSession
65    hugepage_size: int
66
67    def __init__(
68        self,
69        node_config: NodeConfiguration,
70        name: str,
71        logger: DTSLogger,
72    ):
73        """Initialize the OS-aware session.
74
75        Connect to the node right away and also create an interactive remote session.
76
77        Args:
78            node_config: The test run configuration of the node to connect to.
79            name: The name of the session.
80            logger: The logger instance this session will use.
81        """
82        self.hugepage_size = 2048
83        self._config = node_config
84        self.name = name
85        self._logger = logger
86        self.remote_session = create_remote_session(node_config, name, logger)
87        self.interactive_session = create_interactive_session(node_config, logger)
88
89    def is_alive(self) -> bool:
90        """Check whether the underlying remote session is still responding."""
91        return self.remote_session.is_alive()
92
93    def send_command(
94        self,
95        command: str,
96        timeout: float = SETTINGS.timeout,
97        privileged: bool = False,
98        verify: bool = False,
99        env: dict | None = None,
100    ) -> CommandResult:
101        """An all-purpose API for OS-agnostic commands.
102
103        This can be used for an execution of a portable command that's executed the same way
104        on all operating systems, such as Python.
105
106        The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT`
107        environment variable configure the timeout of command execution.
108
109        Args:
110            command: The command to execute.
111            timeout: Wait at most this long in seconds for `command` execution to complete.
112            privileged: Whether to run the command with administrative privileges.
113            verify: If :data:`True`, will check the exit code of the command.
114            env: A dictionary with environment variables to be used with the command execution.
115
116        Raises:
117            RemoteCommandExecutionError: If verify is :data:`True` and the command failed.
118        """
119        if privileged:
120            command = self._get_privileged_command(command)
121
122        return self.remote_session.send_command(command, timeout, verify, env)
123
124    def close(self) -> None:
125        """Close the underlying remote session."""
126        self.remote_session.close()
127
128    @staticmethod
129    @abstractmethod
130    def _get_privileged_command(command: str) -> str:
131        """Modify the command so that it executes with administrative privileges.
132
133        Args:
134            command: The command to modify.
135
136        Returns:
137            The modified command that executes with administrative privileges.
138        """
139
140    @abstractmethod
141    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath:
142        """Try to find DPDK directory in `remote_dir`.
143
144        The directory is the one which is created after the extraction of the tarball. The files
145        are usually extracted into a directory starting with ``dpdk-``.
146
147        Returns:
148            The absolute path of the DPDK remote directory, empty path if not found.
149        """
150
151    @abstractmethod
152    def get_remote_tmp_dir(self) -> PurePath:
153        """Get the path of the temporary directory of the remote OS.
154
155        Returns:
156            The absolute path of the temporary directory.
157        """
158
159    @abstractmethod
160    def get_dpdk_build_env_vars(self, arch: Architecture) -> dict:
161        """Create extra environment variables needed for the target architecture.
162
163        Different architectures may require different configuration, such as setting 32-bit CFLAGS.
164
165        Returns:
166            A dictionary with keys as environment variables.
167        """
168
169    @abstractmethod
170    def join_remote_path(self, *args: str | PurePath) -> PurePath:
171        """Join path parts using the path separator that fits the remote OS.
172
173        Args:
174            args: Any number of paths to join.
175
176        Returns:
177            The resulting joined path.
178        """
179
180    @abstractmethod
181    def copy_from(
182        self,
183        source_file: str | PurePath,
184        destination_file: str | PurePath,
185    ) -> None:
186        """Copy a file from the remote node to the local filesystem.
187
188        Copy `source_file` from the remote node associated with this remote
189        session to `destination_file` on the local filesystem.
190
191        Args:
192            source_file: the file on the remote node.
193            destination_file: a file or directory path on the local filesystem.
194        """
195
196    @abstractmethod
197    def copy_to(
198        self,
199        source_file: str | PurePath,
200        destination_file: str | PurePath,
201    ) -> None:
202        """Copy a file from local filesystem to the remote node.
203
204        Copy `source_file` from local filesystem to `destination_file`
205        on the remote node associated with this remote session.
206
207        Args:
208            source_file: the file on the local filesystem.
209            destination_file: a file or directory path on the remote node.
210        """
211
212    @abstractmethod
213    def remove_remote_dir(
214        self,
215        remote_dir_path: str | PurePath,
216        recursive: bool = True,
217        force: bool = True,
218    ) -> None:
219        """Remove remote directory, by default remove recursively and forcefully.
220
221        Args:
222            remote_dir_path: The path of the directory to remove.
223            recursive: If :data:`True`, also remove all contents inside the directory.
224            force: If :data:`True`, ignore all warnings and try to remove at all costs.
225        """
226
227    @abstractmethod
228    def extract_remote_tarball(
229        self,
230        remote_tarball_path: str | PurePath,
231        expected_dir: str | PurePath | None = None,
232    ) -> None:
233        """Extract remote tarball in its remote directory.
234
235        Args:
236            remote_tarball_path: The path of the tarball on the remote node.
237            expected_dir: If non-empty, check whether `expected_dir` exists after extracting
238                the archive.
239        """
240
241    @abstractmethod
242    def build_dpdk(
243        self,
244        env_vars: dict,
245        meson_args: MesonArgs,
246        remote_dpdk_dir: str | PurePath,
247        remote_dpdk_build_dir: str | PurePath,
248        rebuild: bool = False,
249        timeout: float = SETTINGS.compile_timeout,
250    ) -> None:
251        """Build DPDK on the remote node.
252
253        An extracted DPDK tarball must be present on the node. The build consists of two steps::
254
255            meson setup <meson args> remote_dpdk_dir remote_dpdk_build_dir
256            ninja -C remote_dpdk_build_dir
257
258        The :option:`--compile-timeout` command line argument and the :envvar:`DTS_COMPILE_TIMEOUT`
259        environment variable configure the timeout of DPDK build.
260
261        Args:
262            env_vars: Use these environment variables when building DPDK.
263            meson_args: Use these meson arguments when building DPDK.
264            remote_dpdk_dir: The directory on the remote node where DPDK will be built.
265            remote_dpdk_build_dir: The target build directory on the remote node.
266            rebuild: If :data:`True`, do a subsequent build with ``meson configure`` instead
267                of ``meson setup``.
268            timeout: Wait at most this long in seconds for the build execution to complete.
269        """
270
271    @abstractmethod
272    def get_dpdk_version(self, version_path: str | PurePath) -> str:
273        """Inspect the DPDK version on the remote node.
274
275        Args:
276            version_path: The path to the VERSION file containing the DPDK version.
277
278        Returns:
279            The DPDK version.
280        """
281
282    @abstractmethod
283    def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:
284        r"""Get the list of :class:`~.cpu.LogicalCore`\s on the remote node.
285
286        Args:
287            use_first_core: If :data:`False`, the first physical core won't be used.
288
289        Returns:
290            The logical cores present on the node.
291        """
292
293    @abstractmethod
294    def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:
295        """Kill and cleanup all DPDK apps.
296
297        Args:
298            dpdk_prefix_list: Kill all apps identified by `dpdk_prefix_list`.
299                If `dpdk_prefix_list` is empty, attempt to find running DPDK apps to kill and clean.
300        """
301
302    @abstractmethod
303    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
304        """Make OS-specific modification to the DPDK file prefix.
305
306        Args:
307           dpdk_prefix: The OS-unaware file prefix.
308
309        Returns:
310            The OS-specific file prefix.
311        """
312
313    @abstractmethod
314    def setup_hugepages(self, number_of: int, hugepage_size: int, force_first_numa: bool) -> None:
315        """Configure hugepages on the node.
316
317        Get the node's Hugepage Size, configure the specified count of hugepages
318        if needed and mount the hugepages if needed.
319
320        Args:
321            number_of: Configure this many hugepages.
322            hugepage_size: Configure hugepages of this size.
323            force_first_numa:  If :data:`True`, configure just on the first numa node.
324        """
325
326    @abstractmethod
327    def get_compiler_version(self, compiler_name: str) -> str:
328        """Get installed version of compiler used for DPDK.
329
330        Args:
331            compiler_name: The name of the compiler executable.
332
333        Returns:
334            The compiler's version.
335        """
336
337    @abstractmethod
338    def get_node_info(self) -> NodeInfo:
339        """Collect additional information about the node.
340
341        Returns:
342            Node information.
343        """
344
345    @abstractmethod
346    def update_ports(self, ports: list[Port]) -> None:
347        """Get additional information about ports from the operating system and update them.
348
349        The additional information is:
350
351            * Logical name (e.g. ``enp7s0``) if applicable,
352            * Mac address.
353
354        Args:
355            ports: The ports to update.
356        """
357
358    @abstractmethod
359    def configure_port_state(self, port: Port, enable: bool) -> None:
360        """Enable/disable `port` in the operating system.
361
362        Args:
363            port: The port to configure.
364            enable: If :data:`True`, enable the port, otherwise shut it down.
365        """
366
367    @abstractmethod
368    def configure_port_ip_address(
369        self,
370        address: Union[IPv4Interface, IPv6Interface],
371        port: Port,
372        delete: bool,
373    ) -> None:
374        """Configure an IP address on `port` in the operating system.
375
376        Args:
377            address: The address to configure.
378            port: The port to configure.
379            delete: If :data:`True`, remove the IP address, otherwise configure it.
380        """
381
382    @abstractmethod
383    def configure_port_mtu(self, mtu: int, port: Port) -> None:
384        """Configure `mtu` on `port`.
385
386        Args:
387            mtu: Desired MTU value.
388            port: Port to set `mtu` on.
389        """
390
391    @abstractmethod
392    def configure_ipv4_forwarding(self, enable: bool) -> None:
393        """Enable IPv4 forwarding in the operating system.
394
395        Args:
396            enable: If :data:`True`, enable the forwarding, otherwise disable it.
397        """
398