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