xref: /dpdk/dts/framework/testbed_model/node.py (revision 3da59f30a23f2e795d2315f3d949e1b3e0ce0c3d)
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"""Common functionality for node management.
7
8A node is any host/server DTS connects to.
9
10The base class, :class:`Node`, provides features common to all nodes and is supposed
11to be extended by subclasses with features specific to each node type.
12The :func:`~Node.skip_setup` decorator can be used without subclassing.
13"""
14
15from abc import ABC
16from ipaddress import IPv4Interface, IPv6Interface
17from typing import Any, Callable, Type, Union
18
19from framework.config import (
20    OS,
21    BuildTargetConfiguration,
22    ExecutionConfiguration,
23    NodeConfiguration,
24)
25from framework.exception import ConfigurationError
26from framework.logger import DTSLOG, getLogger
27from framework.settings import SETTINGS
28
29from .cpu import (
30    LogicalCore,
31    LogicalCoreCount,
32    LogicalCoreList,
33    LogicalCoreListFilter,
34    lcore_filter,
35)
36from .linux_session import LinuxSession
37from .os_session import InteractiveShellType, OSSession
38from .port import Port
39from .virtual_device import VirtualDevice
40
41
42class Node(ABC):
43    """The base class for node management.
44
45    It shouldn't be instantiated, but rather subclassed.
46    It implements common methods to manage any node:
47
48        * Connection to the node,
49        * Hugepages setup.
50
51    Attributes:
52        main_session: The primary OS-aware remote session used to communicate with the node.
53        config: The node configuration.
54        name: The name of the node.
55        lcores: The list of logical cores that DTS can use on the node.
56            It's derived from logical cores present on the node and the test run configuration.
57        ports: The ports of this node specified in the test run configuration.
58        virtual_devices: The virtual devices used on the node.
59    """
60
61    main_session: OSSession
62    config: NodeConfiguration
63    name: str
64    lcores: list[LogicalCore]
65    ports: list[Port]
66    _logger: DTSLOG
67    _other_sessions: list[OSSession]
68    _execution_config: ExecutionConfiguration
69    virtual_devices: list[VirtualDevice]
70
71    def __init__(self, node_config: NodeConfiguration):
72        """Connect to the node and gather info during initialization.
73
74        Extra gathered information:
75
76        * The list of available logical CPUs. This is then filtered by
77          the ``lcores`` configuration in the YAML test run configuration file,
78        * Information about ports from the YAML test run configuration file.
79
80        Args:
81            node_config: The node's test run configuration.
82        """
83        self.config = node_config
84        self.name = node_config.name
85        self._logger = getLogger(self.name)
86        self.main_session = create_session(self.config, self.name, self._logger)
87
88        self._logger.info(f"Connected to node: {self.name}")
89
90        self._get_remote_cpus()
91        # filter the node lcores according to the test run configuration
92        self.lcores = LogicalCoreListFilter(
93            self.lcores, LogicalCoreList(self.config.lcores)
94        ).filter()
95
96        self._other_sessions = []
97        self.virtual_devices = []
98        self._init_ports()
99
100    def _init_ports(self) -> None:
101        self.ports = [Port(self.name, port_config) for port_config in self.config.ports]
102        self.main_session.update_ports(self.ports)
103        for port in self.ports:
104            self.configure_port_state(port)
105
106    def set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
107        """Execution setup steps.
108
109        Configure hugepages and call :meth:`_set_up_execution` where
110        the rest of the configuration steps (if any) are implemented.
111
112        Args:
113            execution_config: The execution test run configuration according to which
114                the setup steps will be taken.
115        """
116        self._setup_hugepages()
117        self._set_up_execution(execution_config)
118        self._execution_config = execution_config
119        for vdev in execution_config.vdevs:
120            self.virtual_devices.append(VirtualDevice(vdev))
121
122    def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
123        """Optional additional execution setup steps for subclasses.
124
125        Subclasses should override this if they need to add additional execution setup steps.
126        """
127
128    def tear_down_execution(self) -> None:
129        """Execution teardown steps.
130
131        There are currently no common execution teardown steps common to all DTS node types.
132        """
133        self.virtual_devices = []
134        self._tear_down_execution()
135
136    def _tear_down_execution(self) -> None:
137        """Optional additional execution teardown steps for subclasses.
138
139        Subclasses should override this if they need to add additional execution teardown steps.
140        """
141
142    def set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None:
143        """Build target setup steps.
144
145        There are currently no common build target setup steps common to all DTS node types.
146
147        Args:
148            build_target_config: The build target test run configuration according to which
149                the setup steps will be taken.
150        """
151        self._set_up_build_target(build_target_config)
152
153    def _set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None:
154        """Optional additional build target setup steps for subclasses.
155
156        Subclasses should override this if they need to add additional build target setup steps.
157        """
158
159    def tear_down_build_target(self) -> None:
160        """Build target teardown steps.
161
162        There are currently no common build target teardown steps common to all DTS node types.
163        """
164        self._tear_down_build_target()
165
166    def _tear_down_build_target(self) -> None:
167        """Optional additional build target teardown steps for subclasses.
168
169        Subclasses should override this if they need to add additional build target teardown steps.
170        """
171
172    def create_session(self, name: str) -> OSSession:
173        """Create and return a new OS-aware remote session.
174
175        The returned session won't be used by the node creating it. The session must be used by
176        the caller. The session will be maintained for the entire lifecycle of the node object,
177        at the end of which the session will be cleaned up automatically.
178
179        Note:
180            Any number of these supplementary sessions may be created.
181
182        Args:
183            name: The name of the session.
184
185        Returns:
186            A new OS-aware remote session.
187        """
188        session_name = f"{self.name} {name}"
189        connection = create_session(
190            self.config,
191            session_name,
192            getLogger(session_name, node=self.name),
193        )
194        self._other_sessions.append(connection)
195        return connection
196
197    def create_interactive_shell(
198        self,
199        shell_cls: Type[InteractiveShellType],
200        timeout: float = SETTINGS.timeout,
201        privileged: bool = False,
202        app_args: str = "",
203    ) -> InteractiveShellType:
204        """Factory for interactive session handlers.
205
206        Instantiate `shell_cls` according to the remote OS specifics.
207
208        Args:
209            shell_cls: The class of the shell.
210            timeout: Timeout for reading output from the SSH channel. If you are reading from
211                the buffer and don't receive any data within the timeout it will throw an error.
212            privileged: Whether to run the shell with administrative privileges.
213            app_args: The arguments to be passed to the application.
214
215        Returns:
216            An instance of the desired interactive application shell.
217        """
218        if not shell_cls.dpdk_app:
219            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
220
221        return self.main_session.create_interactive_shell(
222            shell_cls,
223            timeout,
224            privileged,
225            app_args,
226        )
227
228    def filter_lcores(
229        self,
230        filter_specifier: LogicalCoreCount | LogicalCoreList,
231        ascending: bool = True,
232    ) -> list[LogicalCore]:
233        """Filter the node's logical cores that DTS can use.
234
235        Logical cores that DTS can use are the ones that are present on the node, but filtered
236        according to the test run configuration. The `filter_specifier` will filter cores from
237        those logical cores.
238
239        Args:
240            filter_specifier: Two different filters can be used, one that specifies the number
241                of logical cores per core, cores per socket and the number of sockets,
242                and another one that specifies a logical core list.
243            ascending: If :data:`True`, use cores with the lowest numerical id first and continue
244                in ascending order. If :data:`False`, start with the highest id and continue
245                in descending order. This ordering affects which sockets to consider first as well.
246
247        Returns:
248            The filtered logical cores.
249        """
250        self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.")
251        return lcore_filter(
252            self.lcores,
253            filter_specifier,
254            ascending,
255        ).filter()
256
257    def _get_remote_cpus(self) -> None:
258        """Scan CPUs in the remote OS and store a list of LogicalCores."""
259        self._logger.info("Getting CPU information.")
260        self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)
261
262    def _setup_hugepages(self) -> None:
263        """Setup hugepages on the node.
264
265        Configure the hugepages only if they're specified in the node's test run configuration.
266        """
267        if self.config.hugepages:
268            self.main_session.setup_hugepages(
269                self.config.hugepages.amount, self.config.hugepages.force_first_numa
270            )
271
272    def configure_port_state(self, port: Port, enable: bool = True) -> None:
273        """Enable/disable `port`.
274
275        Args:
276            port: The port to enable/disable.
277            enable: :data:`True` to enable, :data:`False` to disable.
278        """
279        self.main_session.configure_port_state(port, enable)
280
281    def configure_port_ip_address(
282        self,
283        address: Union[IPv4Interface, IPv6Interface],
284        port: Port,
285        delete: bool = False,
286    ) -> None:
287        """Add an IP address to `port` on this node.
288
289        Args:
290            address: The IP address with mask in CIDR format. Can be either IPv4 or IPv6.
291            port: The port to which to add the address.
292            delete: If :data:`True`, will delete the address from the port instead of adding it.
293        """
294        self.main_session.configure_port_ip_address(address, port, delete)
295
296    def close(self) -> None:
297        """Close all connections and free other resources."""
298        if self.main_session:
299            self.main_session.close()
300        for session in self._other_sessions:
301            session.close()
302        self._logger.logger_exit()
303
304    @staticmethod
305    def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:
306        """Skip the decorated function.
307
308        The :option:`--skip-setup` command line argument and the :envvar:`DTS_SKIP_SETUP`
309        environment variable enable the decorator.
310        """
311        if SETTINGS.skip_setup:
312            return lambda *args: None
313        else:
314            return func
315
316
317def create_session(node_config: NodeConfiguration, name: str, logger: DTSLOG) -> OSSession:
318    """Factory for OS-aware sessions.
319
320    Args:
321        node_config: The test run configuration of the node to connect to.
322        name: The name of the session.
323        logger: The logger instance this session will use.
324    """
325    match node_config.os:
326        case OS.linux:
327            return LinuxSession(node_config, name, logger)
328        case _:
329            raise ConfigurationError(f"Unsupported OS {node_config.os}")
330