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