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