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"""Common functionality for node management. 7 8A node is any host/server DTS connects to. 9 10The base class, :class:`Node`, provides features common to all nodes and is supposed 11to be extended by subclasses with features specific to each node type. 12The :func:`~Node.skip_setup` decorator can be used without subclassing. 13""" 14 15from abc import ABC 16from ipaddress import IPv4Interface, IPv6Interface 17from typing import Any, Callable, Type, Union 18 19from framework.config import ( 20 OS, 21 BuildTargetConfiguration, 22 ExecutionConfiguration, 23 NodeConfiguration, 24) 25from framework.exception import ConfigurationError 26from framework.logger import DTSLOG, getLogger 27from framework.settings import SETTINGS 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 InteractiveShellType, OSSession 38from .port import Port 39from .virtual_device import VirtualDevice 40 41 42class Node(ABC): 43 """The base class for node management. 44 45 It shouldn't be instantiated, but rather subclassed. 46 It implements common methods to manage any node: 47 48 * Connection to the node, 49 * Hugepages setup. 50 51 Attributes: 52 main_session: The primary OS-aware remote session used to communicate with the node. 53 config: The node configuration. 54 name: The name of the node. 55 lcores: The list of logical cores that DTS can use on the node. 56 It's derived from logical cores present on the node and the test run configuration. 57 ports: The ports of this node specified in the test run configuration. 58 virtual_devices: The virtual devices used on the node. 59 """ 60 61 main_session: OSSession 62 config: NodeConfiguration 63 name: str 64 lcores: list[LogicalCore] 65 ports: list[Port] 66 _logger: DTSLOG 67 _other_sessions: list[OSSession] 68 _execution_config: ExecutionConfiguration 69 virtual_devices: list[VirtualDevice] 70 71 def __init__(self, node_config: NodeConfiguration): 72 """Connect to the node and gather info during initialization. 73 74 Extra gathered information: 75 76 * The list of available logical CPUs. This is then filtered by 77 the ``lcores`` configuration in the YAML test run configuration file, 78 * Information about ports from the YAML test run configuration file. 79 80 Args: 81 node_config: The node's test run configuration. 82 """ 83 self.config = node_config 84 self.name = node_config.name 85 self._logger = getLogger(self.name) 86 self.main_session = create_session(self.config, self.name, self._logger) 87 88 self._logger.info(f"Connected to node: {self.name}") 89 90 self._get_remote_cpus() 91 # filter the node lcores according to the test run configuration 92 self.lcores = LogicalCoreListFilter( 93 self.lcores, LogicalCoreList(self.config.lcores) 94 ).filter() 95 96 self._other_sessions = [] 97 self.virtual_devices = [] 98 self._init_ports() 99 100 def _init_ports(self) -> None: 101 self.ports = [Port(self.name, port_config) for port_config in self.config.ports] 102 self.main_session.update_ports(self.ports) 103 for port in self.ports: 104 self.configure_port_state(port) 105 106 def set_up_execution(self, execution_config: ExecutionConfiguration) -> None: 107 """Execution setup steps. 108 109 Configure hugepages and call :meth:`_set_up_execution` where 110 the rest of the configuration steps (if any) are implemented. 111 112 Args: 113 execution_config: The execution test run configuration according to which 114 the setup steps will be taken. 115 """ 116 self._setup_hugepages() 117 self._set_up_execution(execution_config) 118 self._execution_config = execution_config 119 for vdev in execution_config.vdevs: 120 self.virtual_devices.append(VirtualDevice(vdev)) 121 122 def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None: 123 """Optional additional execution setup steps for subclasses. 124 125 Subclasses should override this if they need to add additional execution setup steps. 126 """ 127 128 def tear_down_execution(self) -> None: 129 """Execution teardown steps. 130 131 There are currently no common execution teardown steps common to all DTS node types. 132 """ 133 self.virtual_devices = [] 134 self._tear_down_execution() 135 136 def _tear_down_execution(self) -> None: 137 """Optional additional execution teardown steps for subclasses. 138 139 Subclasses should override this if they need to add additional execution teardown steps. 140 """ 141 142 def set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 143 """Build target setup steps. 144 145 There are currently no common build target setup steps common to all DTS node types. 146 147 Args: 148 build_target_config: The build target test run configuration according to which 149 the setup steps will be taken. 150 """ 151 self._set_up_build_target(build_target_config) 152 153 def _set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 154 """Optional additional build target setup steps for subclasses. 155 156 Subclasses should override this if they need to add additional build target setup steps. 157 """ 158 159 def tear_down_build_target(self) -> None: 160 """Build target teardown steps. 161 162 There are currently no common build target teardown steps common to all DTS node types. 163 """ 164 self._tear_down_build_target() 165 166 def _tear_down_build_target(self) -> None: 167 """Optional additional build target teardown steps for subclasses. 168 169 Subclasses should override this if they need to add additional build target teardown steps. 170 """ 171 172 def create_session(self, name: str) -> OSSession: 173 """Create and return a new OS-aware remote session. 174 175 The returned session won't be used by the node creating it. The session must be used by 176 the caller. The session will be maintained for the entire lifecycle of the node object, 177 at the end of which the session will be cleaned up automatically. 178 179 Note: 180 Any number of these supplementary sessions may be created. 181 182 Args: 183 name: The name of the session. 184 185 Returns: 186 A new OS-aware remote session. 187 """ 188 session_name = f"{self.name} {name}" 189 connection = create_session( 190 self.config, 191 session_name, 192 getLogger(session_name, node=self.name), 193 ) 194 self._other_sessions.append(connection) 195 return connection 196 197 def create_interactive_shell( 198 self, 199 shell_cls: Type[InteractiveShellType], 200 timeout: float = SETTINGS.timeout, 201 privileged: bool = False, 202 app_args: str = "", 203 ) -> InteractiveShellType: 204 """Factory for interactive session handlers. 205 206 Instantiate `shell_cls` according to the remote OS specifics. 207 208 Args: 209 shell_cls: The class of the shell. 210 timeout: Timeout for reading output from the SSH channel. If you are reading from 211 the buffer and don't receive any data within the timeout it will throw an error. 212 privileged: Whether to run the shell with administrative privileges. 213 app_args: The arguments to be passed to the application. 214 215 Returns: 216 An instance of the desired interactive application shell. 217 """ 218 if not shell_cls.dpdk_app: 219 shell_cls.path = self.main_session.join_remote_path(shell_cls.path) 220 221 return self.main_session.create_interactive_shell( 222 shell_cls, 223 timeout, 224 privileged, 225 app_args, 226 ) 227 228 def filter_lcores( 229 self, 230 filter_specifier: LogicalCoreCount | LogicalCoreList, 231 ascending: bool = True, 232 ) -> list[LogicalCore]: 233 """Filter the node's logical cores that DTS can use. 234 235 Logical cores that DTS can use are the ones that are present on the node, but filtered 236 according to the test run configuration. The `filter_specifier` will filter cores from 237 those logical cores. 238 239 Args: 240 filter_specifier: Two different filters can be used, one that specifies the number 241 of logical cores per core, cores per socket and the number of sockets, 242 and another one that specifies a logical core list. 243 ascending: If :data:`True`, use cores with the lowest numerical id first and continue 244 in ascending order. If :data:`False`, start with the highest id and continue 245 in descending order. This ordering affects which sockets to consider first as well. 246 247 Returns: 248 The filtered logical cores. 249 """ 250 self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.") 251 return lcore_filter( 252 self.lcores, 253 filter_specifier, 254 ascending, 255 ).filter() 256 257 def _get_remote_cpus(self) -> None: 258 """Scan CPUs in the remote OS and store a list of LogicalCores.""" 259 self._logger.info("Getting CPU information.") 260 self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core) 261 262 def _setup_hugepages(self) -> None: 263 """Setup hugepages on the node. 264 265 Configure the hugepages only if they're specified in the node's test run configuration. 266 """ 267 if self.config.hugepages: 268 self.main_session.setup_hugepages( 269 self.config.hugepages.amount, self.config.hugepages.force_first_numa 270 ) 271 272 def configure_port_state(self, port: Port, enable: bool = True) -> None: 273 """Enable/disable `port`. 274 275 Args: 276 port: The port to enable/disable. 277 enable: :data:`True` to enable, :data:`False` to disable. 278 """ 279 self.main_session.configure_port_state(port, enable) 280 281 def configure_port_ip_address( 282 self, 283 address: Union[IPv4Interface, IPv6Interface], 284 port: Port, 285 delete: bool = False, 286 ) -> None: 287 """Add an IP address to `port` on this node. 288 289 Args: 290 address: The IP address with mask in CIDR format. Can be either IPv4 or IPv6. 291 port: The port to which to add the address. 292 delete: If :data:`True`, will delete the address from the port instead of adding it. 293 """ 294 self.main_session.configure_port_ip_address(address, port, delete) 295 296 def close(self) -> None: 297 """Close all connections and free other resources.""" 298 if self.main_session: 299 self.main_session.close() 300 for session in self._other_sessions: 301 session.close() 302 self._logger.logger_exit() 303 304 @staticmethod 305 def skip_setup(func: Callable[..., Any]) -> Callable[..., Any]: 306 """Skip the decorated function. 307 308 The :option:`--skip-setup` command line argument and the :envvar:`DTS_SKIP_SETUP` 309 environment variable enable the decorator. 310 """ 311 if SETTINGS.skip_setup: 312 return lambda *args: None 313 else: 314 return func 315 316 317def create_session(node_config: NodeConfiguration, name: str, logger: DTSLOG) -> OSSession: 318 """Factory for OS-aware sessions. 319 320 Args: 321 node_config: The test run configuration of the node to connect to. 322 name: The name of the session. 323 logger: The logger instance this session will use. 324 """ 325 match node_config.os: 326 case OS.linux: 327 return LinuxSession(node_config, name, logger) 328 case _: 329 raise ConfigurationError(f"Unsupported OS {node_config.os}") 330