xref: /dpdk/dts/framework/testbed_model/node.py (revision bfad0948df75e95e04cba1804a2749cfc91c56fb)
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# Copyright(c) 2024 Arm Limited
6
7"""Common functionality for node management.
8
9A node is any host/server DTS connects to.
10
11The base class, :class:`Node`, provides features common to all nodes and is supposed
12to be extended by subclasses with features specific to each node type.
13The :func:`~Node.skip_setup` decorator can be used without subclassing.
14"""
15
16from abc import ABC
17from ipaddress import IPv4Interface, IPv6Interface
18from typing import Any, Callable, Union
19
20from framework.config import OS, NodeConfiguration, TestRunConfiguration
21from framework.exception import ConfigurationError
22from framework.logger import DTSLogger, get_dts_logger
23from framework.settings import SETTINGS
24
25from .cpu import (
26    LogicalCore,
27    LogicalCoreCount,
28    LogicalCoreList,
29    LogicalCoreListFilter,
30    lcore_filter,
31)
32from .linux_session import LinuxSession
33from .os_session import OSSession
34from .port import Port
35
36
37class Node(ABC):
38    """The base class for node management.
39
40    It shouldn't be instantiated, but rather subclassed.
41    It implements common methods to manage any node:
42
43        * Connection to the node,
44        * Hugepages setup.
45
46    Attributes:
47        main_session: The primary OS-aware remote session used to communicate with the node.
48        config: The node configuration.
49        name: The name of the node.
50        lcores: The list of logical cores that DTS can use on the node.
51            It's derived from logical cores present on the node and the test run configuration.
52        ports: The ports of this node specified in the test run configuration.
53    """
54
55    main_session: OSSession
56    config: NodeConfiguration
57    name: str
58    lcores: list[LogicalCore]
59    ports: list[Port]
60    _logger: DTSLogger
61    _other_sessions: list[OSSession]
62    _test_run_config: TestRunConfiguration
63
64    def __init__(self, node_config: NodeConfiguration):
65        """Connect to the node and gather info during initialization.
66
67        Extra gathered information:
68
69        * The list of available logical CPUs. This is then filtered by
70          the ``lcores`` configuration in the YAML test run configuration file,
71        * Information about ports from the YAML test run configuration file.
72
73        Args:
74            node_config: The node's test run configuration.
75        """
76        self.config = node_config
77        self.name = node_config.name
78        self._logger = get_dts_logger(self.name)
79        self.main_session = create_session(self.config, self.name, self._logger)
80
81        self._logger.info(f"Connected to node: {self.name}")
82
83        self._get_remote_cpus()
84        # filter the node lcores according to the test run configuration
85        self.lcores = LogicalCoreListFilter(
86            self.lcores, LogicalCoreList(self.config.lcores)
87        ).filter()
88
89        self._other_sessions = []
90        self._init_ports()
91
92    def _init_ports(self) -> None:
93        self.ports = [Port(self.name, port_config) for port_config in self.config.ports]
94        self.main_session.update_ports(self.ports)
95        for port in self.ports:
96            self.configure_port_state(port)
97
98    def set_up_test_run(self, test_run_config: TestRunConfiguration) -> None:
99        """Test run setup steps.
100
101        Configure hugepages on all DTS node types. Additional steps can be added by
102        extending the method in subclasses with the use of super().
103
104        Args:
105            test_run_config: A test run configuration according to which
106                the setup steps will be taken.
107        """
108        self._setup_hugepages()
109
110    def tear_down_test_run(self) -> None:
111        """Test run teardown steps.
112
113        There are currently no common execution teardown steps common to all DTS node types.
114        Additional steps can be added by extending the method in subclasses with the use of super().
115        """
116
117    def create_session(self, name: str) -> OSSession:
118        """Create and return a new OS-aware remote session.
119
120        The returned session won't be used by the node creating it. The session must be used by
121        the caller. The session will be maintained for the entire lifecycle of the node object,
122        at the end of which the session will be cleaned up automatically.
123
124        Note:
125            Any number of these supplementary sessions may be created.
126
127        Args:
128            name: The name of the session.
129
130        Returns:
131            A new OS-aware remote session.
132        """
133        session_name = f"{self.name} {name}"
134        connection = create_session(
135            self.config,
136            session_name,
137            get_dts_logger(session_name),
138        )
139        self._other_sessions.append(connection)
140        return connection
141
142    def filter_lcores(
143        self,
144        filter_specifier: LogicalCoreCount | LogicalCoreList,
145        ascending: bool = True,
146    ) -> list[LogicalCore]:
147        """Filter the node's logical cores that DTS can use.
148
149        Logical cores that DTS can use are the ones that are present on the node, but filtered
150        according to the test run configuration. The `filter_specifier` will filter cores from
151        those logical cores.
152
153        Args:
154            filter_specifier: Two different filters can be used, one that specifies the number
155                of logical cores per core, cores per socket and the number of sockets,
156                and another one that specifies a logical core list.
157            ascending: If :data:`True`, use cores with the lowest numerical id first and continue
158                in ascending order. If :data:`False`, start with the highest id and continue
159                in descending order. This ordering affects which sockets to consider first as well.
160
161        Returns:
162            The filtered logical cores.
163        """
164        self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.")
165        return lcore_filter(
166            self.lcores,
167            filter_specifier,
168            ascending,
169        ).filter()
170
171    def _get_remote_cpus(self) -> None:
172        """Scan CPUs in the remote OS and store a list of LogicalCores."""
173        self._logger.info("Getting CPU information.")
174        self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)
175
176    def _setup_hugepages(self) -> None:
177        """Setup hugepages on the node.
178
179        Configure the hugepages only if they're specified in the node's test run configuration.
180        """
181        if self.config.hugepages:
182            self.main_session.setup_hugepages(
183                self.config.hugepages.number_of,
184                self.main_session.hugepage_size,
185                self.config.hugepages.force_first_numa,
186            )
187
188    def configure_port_state(self, port: Port, enable: bool = True) -> None:
189        """Enable/disable `port`.
190
191        Args:
192            port: The port to enable/disable.
193            enable: :data:`True` to enable, :data:`False` to disable.
194        """
195        self.main_session.configure_port_state(port, enable)
196
197    def configure_port_ip_address(
198        self,
199        address: Union[IPv4Interface, IPv6Interface],
200        port: Port,
201        delete: bool = False,
202    ) -> None:
203        """Add an IP address to `port` on this node.
204
205        Args:
206            address: The IP address with mask in CIDR format. Can be either IPv4 or IPv6.
207            port: The port to which to add the address.
208            delete: If :data:`True`, will delete the address from the port instead of adding it.
209        """
210        self.main_session.configure_port_ip_address(address, port, delete)
211
212    def close(self) -> None:
213        """Close all connections and free other resources."""
214        if self.main_session:
215            self.main_session.close()
216        for session in self._other_sessions:
217            session.close()
218
219    @staticmethod
220    def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:
221        """Skip the decorated function.
222
223        The :option:`--skip-setup` command line argument and the :envvar:`DTS_SKIP_SETUP`
224        environment variable enable the decorator.
225        """
226        if SETTINGS.skip_setup:
227            return lambda *args: None
228        else:
229            return func
230
231
232def create_session(node_config: NodeConfiguration, name: str, logger: DTSLogger) -> OSSession:
233    """Factory for OS-aware sessions.
234
235    Args:
236        node_config: The test run configuration of the node to connect to.
237        name: The name of the session.
238        logger: The logger instance this session will use.
239    """
240    match node_config.os:
241        case OS.linux:
242            return LinuxSession(node_config, name, logger)
243        case _:
244            raise ConfigurationError(f"Unsupported OS {node_config.os}")
245