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