1# Copyright (c) 2008 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Provides a factory method to create a host object.""" 6 7from contextlib import closing 8from contextlib import contextmanager 9import logging 10import os 11 12from autotest_lib.client.bin import local_host 13from autotest_lib.client.bin import utils 14from autotest_lib.client.common_lib import deprecation 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.common_lib import global_config 17from autotest_lib.server import utils as server_utils 18from autotest_lib.server.cros.dynamic_suite import constants 19from autotest_lib.server.hosts import android_host 20from autotest_lib.server.hosts import cros_host 21from autotest_lib.server.hosts import host_info 22from autotest_lib.server.hosts import jetstream_host 23from autotest_lib.server.hosts import moblab_host 24from autotest_lib.server.hosts import gce_host 25from autotest_lib.server.hosts import ssh_host 26from autotest_lib.server.hosts import labstation_host 27from autotest_lib.server.hosts import file_store 28 29 30CONFIG = global_config.global_config 31 32# Default ssh options used in creating a host. 33DEFAULT_SSH_USER = 'root' 34DEFAULT_SSH_PASS = '' 35DEFAULT_SSH_PORT = None 36DEFAULT_SSH_VERBOSITY = '' 37DEFAULT_SSH_OPTIONS = '' 38 39# for tracking which hostnames have already had job_start called 40_started_hostnames = set() 41 42# A list of all the possible host types, ordered according to frequency of 43# host types in the lab, so the more common hosts don't incur a repeated ssh 44# overhead in checking for less common host types. 45host_types = [cros_host.CrosHost, labstation_host.LabstationHost, 46 moblab_host.MoblabHost, jetstream_host.JetstreamHost, 47 gce_host.GceHost] 48OS_HOST_DICT = { 49 'android': android_host.AndroidHost, 50 'cros': cros_host.CrosHost, 51 'jetstream': jetstream_host.JetstreamHost, 52 'moblab': moblab_host.MoblabHost, 53 'labstation': labstation_host.LabstationHost 54} 55 56LOOKUP_DICT = { 57 'CrosHost': cros_host.CrosHost, 58 'JetstreamHost': jetstream_host.JetstreamHost, 59 'MoblabHost': moblab_host.MoblabHost, 60 'LabstationHost': labstation_host.LabstationHost 61} 62 63# Timeout for early connectivity check to the host, in seconds. 64_CONNECTIVITY_CHECK_TIMEOUT_S = 10 65 66 67def _get_host_arguments(machine, **args): 68 """Get parameters to construct a host object. 69 70 There are currently 2 use cases for creating a host. 71 1. Through the server_job, in which case the server_job injects 72 the appropriate ssh parameters into our name space and they 73 are available as the variables ssh_user, ssh_pass etc. 74 2. Directly through factory.create_host, in which case we use 75 the same defaults as used in the server job to create a host. 76 3. Through neither of the above, in which case args can be provided 77 and should be respected if a globa 78 79 @param machine: machine dict 80 @return: A dictionary containing arguments for host specifically hostname, 81 afe_host, user, password, port, ssh_verbosity_flag and 82 ssh_options. 83 """ 84 hostname, afe_host = server_utils.get_host_info_from_machine(machine) 85 connection_pool = server_utils.get_connection_pool_from_machine(machine) 86 host_info_store = host_info.get_store_from_machine(machine) 87 info = host_info_store.get() 88 89 g = globals() 90 91 # For each arg, try to fetch the arg from the globals... 92 # If its not there, then try to get it from **args. 93 # If its not there, use the default. 94 default_user = DEFAULT_SSH_USER if 'user' not in args else args['user'] 95 user = info.attributes.get('ssh_user', g.get('ssh_user', default_user)) 96 97 default_pass = DEFAULT_SSH_PASS if 'ssh_pass' not in args else args['ssh_pass'] 98 password = info.attributes.get('ssh_pass', g.get('ssh_pass', 99 default_pass)) 100 101 default_port = DEFAULT_SSH_PORT if 'ssh_port' not in args else args['ssh_port'] 102 port = info.attributes.get('ssh_port', g.get('ssh_port', default_port)) 103 104 default_verbosity = DEFAULT_SSH_VERBOSITY if 'ssh_verbosity_flag' not in args else args['ssh_verbosity_flag'] 105 ssh_verbosity_flag = info.attributes.get('ssh_verbosity_flag', 106 g.get('ssh_verbosity_flag', 107 default_verbosity)) 108 109 default_options = DEFAULT_SSH_OPTIONS if 'ssh_options' not in args else args['ssh_options'] 110 ssh_options = info.attributes.get('ssh_options', 111 g.get('ssh_options', 112 default_options)) 113 114 hostname, user, password, port = server_utils.parse_machine(hostname, user, 115 password, port) 116 if port: 117 port = int(port) 118 host_args = { 119 'hostname': hostname, 120 'afe_host': afe_host, 121 'host_info_store': host_info_store, 122 'user': user, 123 'password': password, 124 'port': port, 125 'ssh_verbosity_flag': ssh_verbosity_flag, 126 'ssh_options': ssh_options, 127 'connection_pool': connection_pool, 128 } 129 return host_args 130 131 132def _detect_host(connectivity_class, hostname, **args): 133 """Detect host type. 134 135 Goes through all the possible host classes, calling check_host with a 136 basic host object. Currently this is an ssh host, but theoretically it 137 can be any host object that the check_host method of appropriate host 138 type knows to use. 139 140 @param connectivity_class: connectivity class to use to talk to the host 141 (ParamikoHost or SSHHost) 142 @param hostname: A string representing the host name of the device. 143 @param args: Args that will be passed to the constructor of 144 the host class. 145 146 @returns: Class type of the first host class that returns True to the 147 check_host method. 148 """ 149 preset_host = _preset_host(hostname) 150 if preset_host: 151 logging.debug("Using preset_host %s for %s ", preset_host.__name__, 152 hostname) 153 return preset_host 154 with closing(connectivity_class(hostname, **args)) as host: 155 for host_module in host_types: 156 logging.info('Attempting to autodetect if host is of type %s', 157 host_module.__name__) 158 if host_module.check_host(host, timeout=10): 159 os.environ['HOST_%s' % hostname] = str(host_module.__name__) 160 return host_module 161 162 logging.warning('Unable to apply conventional host detection methods, ' 163 'defaulting to chromeos host.') 164 return cros_host.CrosHost 165 166 167def _preset_host(hostname): 168 """Check the environmental variables to see if the host type has been set. 169 170 @param hostname: A string representing the host name of the device. 171 172 @returns: Class type of the host, if previously found & set in 173 _detect_host, else None. 174 """ 175 preset_host = os.getenv('HOST_%s' % hostname) 176 if preset_host: 177 return LOOKUP_DICT.get(preset_host, None) 178 179 180def _choose_connectivity_class(hostname, ssh_port): 181 """Choose a connectivity class for this hostname. 182 183 @param hostname: hostname that we need a connectivity class for. 184 @param ssh_port: SSH port to connect to the host. 185 186 @returns a connectivity host class. 187 """ 188 if (hostname == 'localhost' and ssh_port == DEFAULT_SSH_PORT): 189 return local_host.LocalHost 190 else: 191 return ssh_host.SSHHost 192 193 194def _verify_connectivity(connectivity_class, hostname, **args): 195 """Verify connectivity to the host. 196 197 Any interaction with an unreachable host is guaranteed to fail later. By 198 checking connectivity first, duplicate errors / timeouts can be avoided. 199 """ 200 if connectivity_class == local_host.LocalHost: 201 return True 202 203 assert connectivity_class == ssh_host.SSHHost 204 with closing(ssh_host.SSHHost(hostname, **args)) as host: 205 host.run('test :', timeout=_CONNECTIVITY_CHECK_TIMEOUT_S, 206 ssh_failure_retry_ok=False, 207 ignore_timeout=False) 208 209 210def create_companion_hosts(companion_hosts): 211 """Wrapped for create_hosts for making host objects on companion duts. 212 213 @param companion_hosts: str or list of extra_host hostnames 214 215 @returns: A list of host objects for each host in companion_hosts 216 """ 217 if not isinstance(companion_hosts, list): 218 companion_hosts = [companion_hosts] 219 hosts = [] 220 for host in companion_hosts: 221 hosts.append(create_host(host)) 222 return hosts 223 224# TODO(kevcheng): Update the creation method so it's not a research project 225# determining the class inheritance model. 226def create_host(machine, host_class=None, connectivity_class=None, **args): 227 """Create a host object. 228 229 This method mixes host classes that are needed into a new subclass 230 and creates a instance of the new class. 231 232 @param machine: A dict representing the device under test or a String 233 representing the DUT hostname (for legacy caller support). 234 If it is a machine dict, the 'hostname' key is required. 235 Optional 'afe_host' key will pipe in afe_host 236 from the autoserv runtime or the AFE. 237 @param host_class: Host class to use, if None, will attempt to detect 238 the correct class. 239 @param connectivity_class: DEPRECATED. Connectivity class is determined 240 internally. 241 @param args: Args that will be passed to the constructor of 242 the new host class. 243 244 @returns: A host object which is an instance of the newly created 245 host class. 246 """ 247 # Argument deprecated 248 if connectivity_class is not None: 249 deprecation.warn('server.create_hosts:connectivity_class') 250 connectivity_class = None 251 252 detected_args = _get_host_arguments(machine, **args) 253 hostname = detected_args.pop('hostname') 254 afe_host = detected_args['afe_host'] 255 info_store = detected_args['host_info_store'].get() 256 args.update(detected_args) 257 host_os = None 258 full_os_prefix = constants.OS_PREFIX + ':' 259 # Let's grab the os from the labels if we can for host class detection. 260 for label in info_store.labels: 261 if label.startswith(full_os_prefix): 262 host_os = label[len(full_os_prefix):] 263 logging.debug('Detected host os: %s from info_store.', host_os) 264 break 265 266 connectivity_class = _choose_connectivity_class(hostname, args['port']) 267 # TODO(kevcheng): get rid of the host detection using host attributes. 268 host_class = (host_class 269 or OS_HOST_DICT.get(afe_host.attributes.get('os_type')) 270 or OS_HOST_DICT.get(host_os)) 271 272 if host_class is android_host.AndroidHost: 273 # We don't have direct ssh access to Android devices, so we do 274 # not need connectivity_class for AndroidHost here. 275 connectivity_class = None 276 277 if host_class is None: 278 # TODO(pprabhu) If we fail to verify connectivity, we skip the costly 279 # host autodetection logic. We should ideally just error out in this 280 # case, but there are a couple problems: 281 # - VMs can take a while to boot up post provision, so SSH connections 282 # to moblab vms may not be available for ~2 minutes. This requires 283 # extended timeout in _verify_connectivity() so we don't get speed 284 # benefits from bailing early. 285 # - We need to make sure stopping here does not block repair flows. 286 try: 287 _verify_connectivity(connectivity_class, hostname, **args) 288 host_class = _detect_host(connectivity_class, hostname, **args) 289 except (error.AutoservRunError, error.AutoservSSHTimeout): 290 logging.exception('Failed to verify connectivity to host.' 291 ' Skipping host auto detection logic.') 292 host_class = cros_host.CrosHost 293 logging.debug('Defaulting to CrosHost.') 294 295 # create a custom host class for this machine and return an instance of it 296 if connectivity_class: 297 classes = (host_class, connectivity_class) 298 custom_host_class = type("%s_host" % hostname, classes, {}) 299 else: 300 custom_host_class = host_class 301 302 logging.info('creating host class for {} w/ {}||'.format(hostname, args)) 303 host_instance = custom_host_class(hostname, **args) 304 305 # call job_start if this is the first time this host is being used 306 if hostname not in _started_hostnames: 307 host_instance.job_start() 308 _started_hostnames.add(hostname) 309 310 return host_instance 311 312 313def create_target_machine(machine, **kwargs): 314 """Create the target machine, accounting for containers. 315 316 @param machine: A dict representing the test bed under test or a String 317 representing the testbed hostname (for legacy caller 318 support). 319 If it is a machine dict, the 'hostname' key is required. 320 Optional 'afe_host' key will pipe in afe_host 321 from the autoserv runtime or the AFE. 322 @param kwargs: Keyword args to pass to the testbed initialization. 323 324 @returns: The target machine to be used for verify/repair. 325 """ 326 is_moblab = CONFIG.get_config_value('SSP', 'is_moblab', type=bool, 327 default=False) 328 hostname = machine['hostname'] if isinstance(machine, dict) else machine 329 if (utils.is_in_container() and is_moblab and 330 hostname in ['localhost', '127.0.0.1']): 331 hostname = CONFIG.get_config_value('SSP', 'host_container_ip', type=str, 332 default=None) 333 if isinstance(machine, dict): 334 machine['hostname'] = hostname 335 else: 336 machine = hostname 337 logging.debug('Hostname of machine is converted to %s for the test to ' 338 'run inside a container.', hostname) 339 return create_host(machine, **kwargs) 340 341@contextmanager 342def create_target_host(hostname, host_info_path=None, host_info_store=None, 343 servo_uart_logs_dir=None, **kwargs): 344 """Create the target host, accounting for containers. 345 346 @param hostname: hostname of the device 347 @param host_info_path: path to the host info file to create host_info 348 @param host_info_store: if exist then using as the primary host_info 349 instance when creaating machine 350 @param kwargs: Keyword args to pass to the testbed initialization. 351 352 @yield: The target host object to be used for you :) 353 """ 354 355 if not host_info_store and host_info_path: 356 host_info_store = file_store.FileStore(host_info_path) 357 358 if host_info_store: 359 machine = { 360 'hostname': hostname, 361 'host_info_store': host_info_store, 362 'afe_host': server_utils.EmptyAFEHost() 363 } 364 else: 365 machine = hostname 366 367 host = create_target_machine(machine, **kwargs) 368 if servo_uart_logs_dir and host.servo: 369 host.servo.uart_logs_dir = servo_uart_logs_dir 370 try: 371 yield host 372 finally: 373 host.close() 374