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