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