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