xref: /dpdk/dts/framework/testbed_model/node.py (revision da78bca74ff1f7afe00a9c4498b8927efdb447c8)
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(self, build_target_config: BuildTargetConfiguration) -> None:
107        """
108        Perform the build target setup that will be done for each build target
109        tested on this node.
110        """
111        self._set_up_build_target(build_target_config)
112
113    def _set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None:
114        """
115        This method exists to be optionally overwritten by derived classes and
116        is not decorated so that the derived class doesn't have to use the decorator.
117        """
118
119    def tear_down_build_target(self) -> None:
120        """
121        Perform the build target teardown that will be done after each build target
122        tested on this node.
123        """
124        self._tear_down_build_target()
125
126    def _tear_down_build_target(self) -> None:
127        """
128        This method exists to be optionally overwritten by derived classes and
129        is not decorated so that the derived class doesn't have to use the decorator.
130        """
131
132    def create_session(self, name: str) -> OSSession:
133        """
134        Create and return a new OSSession tailored to the remote OS.
135        """
136        session_name = f"{self.name} {name}"
137        connection = create_session(
138            self.config,
139            session_name,
140            getLogger(session_name, node=self.name),
141        )
142        self._other_sessions.append(connection)
143        return connection
144
145    def create_interactive_shell(
146        self,
147        shell_cls: Type[InteractiveShellType],
148        timeout: float = SETTINGS.timeout,
149        privileged: bool = False,
150        app_args: str = "",
151    ) -> InteractiveShellType:
152        """Create a handler for an interactive session.
153
154        Instantiate shell_cls according to the remote OS specifics.
155
156        Args:
157            shell_cls: The class of the shell.
158            timeout: Timeout for reading output from the SSH channel. If you are
159                reading from the buffer and don't receive any data within the timeout
160                it will throw an error.
161            privileged: Whether to run the shell with administrative privileges.
162            app_args: The arguments to be passed to the application.
163        Returns:
164            Instance of the desired interactive application.
165        """
166        if not shell_cls.dpdk_app:
167            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
168
169        return self.main_session.create_interactive_shell(
170            shell_cls,
171            app_args,
172            timeout,
173            privileged,
174        )
175
176    def filter_lcores(
177        self,
178        filter_specifier: LogicalCoreCount | LogicalCoreList,
179        ascending: bool = True,
180    ) -> list[LogicalCore]:
181        """
182        Filter the LogicalCores found on the Node according to
183        a LogicalCoreCount or a LogicalCoreList.
184
185        If ascending is True, use cores with the lowest numerical id first
186        and continue in ascending order. If False, start with the highest
187        id and continue in descending order. This ordering affects which
188        sockets to consider first as well.
189        """
190        self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.")
191        return lcore_filter(
192            self.lcores,
193            filter_specifier,
194            ascending,
195        ).filter()
196
197    def _get_remote_cpus(self) -> None:
198        """
199        Scan CPUs in the remote OS and store a list of LogicalCores.
200        """
201        self._logger.info("Getting CPU information.")
202        self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)
203
204    def _setup_hugepages(self):
205        """
206        Setup hugepages on the Node. Different architectures can supply different
207        amounts of memory for hugepages and numa-based hugepage allocation may need
208        to be considered.
209        """
210        if self.config.hugepages:
211            self.main_session.setup_hugepages(
212                self.config.hugepages.amount, self.config.hugepages.force_first_numa
213            )
214
215    def configure_port_state(self, port: Port, enable: bool = True) -> None:
216        """
217        Enable/disable port.
218        """
219        self.main_session.configure_port_state(port, enable)
220
221    def configure_port_ip_address(
222        self,
223        address: Union[IPv4Interface, IPv6Interface],
224        port: Port,
225        delete: bool = False,
226    ) -> None:
227        """
228        Configure the IP address of a port on this node.
229        """
230        self.main_session.configure_port_ip_address(address, port, delete)
231
232    def close(self) -> None:
233        """
234        Close all connections and free other resources.
235        """
236        if self.main_session:
237            self.main_session.close()
238        for session in self._other_sessions:
239            session.close()
240        self._logger.logger_exit()
241
242    @staticmethod
243    def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]:
244        if SETTINGS.skip_setup:
245            return lambda *args: None
246        else:
247            return func
248