xref: /dpdk/dts/framework/testbed_model/node.py (revision cecfe0aabf585fe49548e19b3b5d7ed98f45783b)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2010-2014 Intel Corporation
3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4# Copyright(c) 2022-2023 University of New Hampshire
5
6"""
7A node is a generic host that DTS connects to and manages.
8"""
9
10from abc import ABC
11from typing import Any, Callable, Type
12
13from framework.config import (
14    BuildTargetConfiguration,
15    ExecutionConfiguration,
16    NodeConfiguration,
17)
18from framework.logger import DTSLOG, getLogger
19from framework.remote_session import InteractiveShellType, OSSession, create_session
20from framework.settings import SETTINGS
21
22from .hw import (
23    LogicalCore,
24    LogicalCoreCount,
25    LogicalCoreList,
26    LogicalCoreListFilter,
27    VirtualDevice,
28    lcore_filter,
29)
30from .hw.port import Port
31
32
33class Node(ABC):
34    """
35    Basic class for node management. This class implements methods that
36    manage a node, such as information gathering (of CPU/PCI/NIC) and
37    environment setup.
38    """
39
40    main_session: OSSession
41    config: NodeConfiguration
42    name: str
43    lcores: list[LogicalCore]
44    ports: list[Port]
45    _logger: DTSLOG
46    _other_sessions: list[OSSession]
47    _execution_config: ExecutionConfiguration
48    virtual_devices: list[VirtualDevice]
49
50    def __init__(self, node_config: NodeConfiguration):
51        self.config = node_config
52        self.name = node_config.name
53        self._logger = getLogger(self.name)
54        self.main_session = create_session(self.config, self.name, self._logger)
55
56        self._logger.info(f"Connected to node: {self.name}")
57
58        self._get_remote_cpus()
59        # filter the node lcores according to user config
60        self.lcores = LogicalCoreListFilter(
61            self.lcores, LogicalCoreList(self.config.lcores)
62        ).filter()
63
64        self._other_sessions = []
65        self.virtual_devices = []
66        self._init_ports()
67
68    def _init_ports(self) -> None:
69        self.ports = [Port(self.name, port_config) for port_config in self.config.ports]
70        self.main_session.update_ports(self.ports)
71        for port in self.ports:
72            self.configure_port_state(port)
73
74    def set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
75        """
76        Perform the execution setup that will be done for each execution
77        this node is part of.
78        """
79        self._setup_hugepages()
80        self._set_up_execution(execution_config)
81        self._execution_config = execution_config
82        for vdev in execution_config.vdevs:
83            self.virtual_devices.append(VirtualDevice(vdev))
84
85    def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
86        """
87        This method exists to be optionally overwritten by derived classes and
88        is not decorated so that the derived class doesn't have to use the decorator.
89        """
90
91    def tear_down_execution(self) -> None:
92        """
93        Perform the execution teardown that will be done after each execution
94        this node is part of concludes.
95        """
96        self.virtual_devices = []
97        self._tear_down_execution()
98
99    def _tear_down_execution(self) -> None:
100        """
101        This method exists to be optionally overwritten by derived classes and
102        is not decorated so that the derived class doesn't have to use the decorator.
103        """
104
105    def set_up_build_target(
106        self, build_target_config: BuildTargetConfiguration
107    ) -> None:
108        """
109        Perform the build target setup that will be done for each build target
110        tested on this node.
111        """
112        self._set_up_build_target(build_target_config)
113
114    def _set_up_build_target(
115        self, build_target_config: BuildTargetConfiguration
116    ) -> None:
117        """
118        This method exists to be optionally overwritten by derived classes and
119        is not decorated so that the derived class doesn't have to use the decorator.
120        """
121
122    def tear_down_build_target(self) -> None:
123        """
124        Perform the build target teardown that will be done after each build target
125        tested on this node.
126        """
127        self._tear_down_build_target()
128
129    def _tear_down_build_target(self) -> None:
130        """
131        This method exists to be optionally overwritten by derived classes and
132        is not decorated so that the derived class doesn't have to use the decorator.
133        """
134
135    def create_session(self, name: str) -> OSSession:
136        """
137        Create and return a new OSSession tailored to the remote OS.
138        """
139        session_name = f"{self.name} {name}"
140        connection = create_session(
141            self.config,
142            session_name,
143            getLogger(session_name, node=self.name),
144        )
145        self._other_sessions.append(connection)
146        return connection
147
148    def create_interactive_shell(
149        self,
150        shell_cls: Type[InteractiveShellType],
151        timeout: float = SETTINGS.timeout,
152        privileged: bool = False,
153        app_args: str = "",
154    ) -> InteractiveShellType:
155        """Create a handler for an interactive session.
156
157        Instantiate shell_cls according to the remote OS specifics.
158
159        Args:
160            shell_cls: The class of the shell.
161            timeout: Timeout for reading output from the SSH channel. If you are
162                reading from the buffer and don't receive any data within the timeout
163                it will throw an error.
164            privileged: Whether to run the shell with administrative privileges.
165            app_args: The arguments to be passed to the application.
166        Returns:
167            Instance of the desired interactive application.
168        """
169        if not shell_cls.dpdk_app:
170            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
171
172        return self.main_session.create_interactive_shell(
173            shell_cls,
174            app_args,
175            timeout,
176            privileged,
177        )
178
179    def filter_lcores(
180        self,
181        filter_specifier: LogicalCoreCount | LogicalCoreList,
182        ascending: bool = True,
183    ) -> list[LogicalCore]:
184        """
185        Filter the LogicalCores found on the Node according to
186        a LogicalCoreCount or a LogicalCoreList.
187
188        If ascending is True, use cores with the lowest numerical id first
189        and continue in ascending order. If False, start with the highest
190        id and continue in descending order. This ordering affects which
191        sockets to consider first as well.
192        """
193        self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.")
194        return lcore_filter(
195            self.lcores,
196            filter_specifier,
197            ascending,
198        ).filter()
199
200    def _get_remote_cpus(self) -> None:
201        """
202        Scan CPUs in the remote OS and store a list of LogicalCores.
203        """
204        self._logger.info("Getting CPU information.")
205        self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)
206
207    def _setup_hugepages(self):
208        """
209        Setup hugepages on the Node. Different architectures can supply different
210        amounts of memory for hugepages and numa-based hugepage allocation may need
211        to be considered.
212        """
213        if self.config.hugepages:
214            self.main_session.setup_hugepages(
215                self.config.hugepages.amount, self.config.hugepages.force_first_numa
216            )
217
218    def configure_port_state(self, port: Port, enable: bool = True) -> None:
219        """
220        Enable/disable port.
221        """
222        self.main_session.configure_port_state(port, enable)
223
224    def close(self) -> None:
225        """
226        Close all connections and free other resources.
227        """
228        if self.main_session:
229            self.main_session.close()
230        for session in self._other_sessions:
231            session.close()
232        self._logger.logger_exit()
233
234    @staticmethod
235    def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:
236        if SETTINGS.skip_setup:
237            return lambda *args: None
238        else:
239            return func
240