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