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