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 7import logging 8from contextlib import closing 9 10from autotest_lib.client.bin import local_host 11from autotest_lib.client.bin import utils 12from autotest_lib.client.common_lib import deprecation 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib import global_config 15from autotest_lib.server import utils as server_utils 16from autotest_lib.server.cros.dynamic_suite import constants 17from autotest_lib.server.hosts import cros_host 18from autotest_lib.server.hosts import host_info 19from autotest_lib.server.hosts import jetstream_host 20from autotest_lib.server.hosts import moblab_host 21from autotest_lib.server.hosts import gce_host 22from autotest_lib.server.hosts import sonic_host 23from autotest_lib.server.hosts import ssh_host 24from autotest_lib.server.hosts import labstation_host 25 26 27CONFIG = global_config.global_config 28 29# Default ssh options used in creating a host. 30DEFAULT_SSH_USER = 'root' 31DEFAULT_SSH_PASS = '' 32DEFAULT_SSH_PORT = 22 33DEFAULT_SSH_VERBOSITY = '' 34DEFAULT_SSH_OPTIONS = '' 35 36# for tracking which hostnames have already had job_start called 37_started_hostnames = set() 38 39# A list of all the possible host types, ordered according to frequency of 40# host types in the lab, so the more common hosts don't incur a repeated ssh 41# overhead in checking for less common host types. 42host_types = [cros_host.CrosHost, labstation_host.LabstationHost, 43 moblab_host.MoblabHost, jetstream_host.JetstreamHost, 44 sonic_host.SonicHost, gce_host.GceHost] 45OS_HOST_DICT = {'cros': cros_host.CrosHost, 46 'jetstream': jetstream_host.JetstreamHost, 47 'moblab': moblab_host.MoblabHost, 48 'labstation': labstation_host.LabstationHost} 49 50# Timeout for early connectivity check to the host, in seconds. 51_CONNECTIVITY_CHECK_TIMEOUT_S = 10 52 53 54def _get_host_arguments(machine): 55 """Get parameters to construct a host object. 56 57 There are currently 2 use cases for creating a host. 58 1. Through the server_job, in which case the server_job injects 59 the appropriate ssh parameters into our name space and they 60 are available as the variables ssh_user, ssh_pass etc. 61 2. Directly through factory.create_host, in which case we use 62 the same defaults as used in the server job to create a host. 63 64 @param machine: machine dict 65 @return: A dictionary containing arguments for host specifically hostname, 66 afe_host, user, password, port, ssh_verbosity_flag and 67 ssh_options. 68 """ 69 hostname, afe_host = server_utils.get_host_info_from_machine(machine) 70 connection_pool = server_utils.get_connection_pool_from_machine(machine) 71 host_info_store = host_info.get_store_from_machine(machine) 72 info = host_info_store.get() 73 74 g = globals() 75 user = info.attributes.get('ssh_user', g.get('ssh_user', DEFAULT_SSH_USER)) 76 password = info.attributes.get('ssh_pass', g.get('ssh_pass', 77 DEFAULT_SSH_PASS)) 78 port = info.attributes.get('ssh_port', g.get('ssh_port', DEFAULT_SSH_PORT)) 79 ssh_verbosity_flag = info.attributes.get('ssh_verbosity_flag', 80 g.get('ssh_verbosity_flag', 81 DEFAULT_SSH_VERBOSITY)) 82 ssh_options = info.attributes.get('ssh_options', 83 g.get('ssh_options', 84 DEFAULT_SSH_OPTIONS)) 85 86 hostname, user, password, port = server_utils.parse_machine(hostname, user, 87 password, port) 88 89 host_args = { 90 'hostname': hostname, 91 'afe_host': afe_host, 92 'host_info_store': host_info_store, 93 'user': user, 94 'password': password, 95 'port': int(port), 96 'ssh_verbosity_flag': ssh_verbosity_flag, 97 'ssh_options': ssh_options, 98 'connection_pool': connection_pool, 99 } 100 return host_args 101 102 103def _detect_host(connectivity_class, hostname, **args): 104 """Detect host type. 105 106 Goes through all the possible host classes, calling check_host with a 107 basic host object. Currently this is an ssh host, but theoretically it 108 can be any host object that the check_host method of appropriate host 109 type knows to use. 110 111 @param connectivity_class: connectivity class to use to talk to the host 112 (ParamikoHost or SSHHost) 113 @param hostname: A string representing the host name of the device. 114 @param args: Args that will be passed to the constructor of 115 the host class. 116 117 @returns: Class type of the first host class that returns True to the 118 check_host method. 119 """ 120 with closing(connectivity_class(hostname, **args)) as host: 121 for host_module in host_types: 122 logging.info('Attempting to autodetect if host is of type %s', 123 host_module.__name__) 124 if host_module.check_host(host, timeout=10): 125 return host_module 126 127 logging.warning('Unable to apply conventional host detection methods, ' 128 'defaulting to chromeos host.') 129 return cros_host.CrosHost 130 131 132def _choose_connectivity_class(hostname, ssh_port): 133 """Choose a connectivity class for this hostname. 134 135 @param hostname: hostname that we need a connectivity class for. 136 @param ssh_port: SSH port to connect to the host. 137 138 @returns a connectivity host class. 139 """ 140 if (hostname == 'localhost' and ssh_port == DEFAULT_SSH_PORT): 141 return local_host.LocalHost 142 else: 143 return ssh_host.SSHHost 144 145 146def _verify_connectivity(connectivity_class, hostname, **args): 147 """Verify connectivity to the host. 148 149 Any interaction with an unreachable host is guaranteed to fail later. By 150 checking connectivity first, duplicate errors / timeouts can be avoided. 151 """ 152 if connectivity_class == local_host.LocalHost: 153 return True 154 155 assert connectivity_class == ssh_host.SSHHost 156 with closing(ssh_host.SSHHost(hostname, **args)) as host: 157 host.run('test :', timeout=_CONNECTIVITY_CHECK_TIMEOUT_S, 158 ssh_failure_retry_ok=False, 159 ignore_timeout=False) 160 161 162# TODO(kevcheng): Update the creation method so it's not a research project 163# determining the class inheritance model. 164def create_host(machine, host_class=None, connectivity_class=None, **args): 165 """Create a host object. 166 167 This method mixes host classes that are needed into a new subclass 168 and creates a instance of the new class. 169 170 @param machine: A dict representing the device under test or a String 171 representing the DUT hostname (for legacy caller support). 172 If it is a machine dict, the 'hostname' key is required. 173 Optional 'afe_host' key will pipe in afe_host 174 from the autoserv runtime or the AFE. 175 @param host_class: Host class to use, if None, will attempt to detect 176 the correct class. 177 @param connectivity_class: DEPRECATED. Connectivity class is determined 178 internally. 179 @param args: Args that will be passed to the constructor of 180 the new host class. 181 182 @returns: A host object which is an instance of the newly created 183 host class. 184 """ 185 # Argument deprecated 186 if connectivity_class is not None: 187 deprecation.warn('server.create_hosts:connectivity_class') 188 connectivity_class = None 189 190 detected_args = _get_host_arguments(machine) 191 hostname = detected_args.pop('hostname') 192 afe_host = detected_args['afe_host'] 193 info_store = detected_args['host_info_store'].get() 194 args.update(detected_args) 195 host_os = None 196 full_os_prefix = constants.OS_PREFIX + ':' 197 # Let's grab the os from the labels if we can for host class detection. 198 for label in info_store.labels: 199 if label.startswith(full_os_prefix): 200 host_os = label[len(full_os_prefix):] 201 break 202 203 connectivity_class = _choose_connectivity_class(hostname, args['port']) 204 # TODO(kevcheng): get rid of the host detection using host attributes. 205 host_class = (host_class 206 or OS_HOST_DICT.get(afe_host.attributes.get('os_type')) 207 or OS_HOST_DICT.get(host_os)) 208 209 if host_class is None: 210 # TODO(pprabhu) If we fail to verify connectivity, we skip the costly 211 # host autodetection logic. We should ideally just error out in this 212 # case, but there are a couple problems: 213 # - VMs can take a while to boot up post provision, so SSH connections 214 # to moblab vms may not be available for ~2 minutes. This requires 215 # extended timeout in _verify_connectivity() so we don't get speed 216 # benefits from bailing early. 217 # - We need to make sure stopping here does not block repair flows. 218 try: 219 _verify_connectivity(connectivity_class, hostname, **args) 220 host_class = _detect_host(connectivity_class, hostname, **args) 221 except (error.AutoservRunError, error.AutoservSSHTimeout): 222 logging.exception('Failed to verify connectivity to host.' 223 ' Skipping host auto detection logic.') 224 host_class = cros_host.CrosHost 225 logging.debug('Defaulting to CrosHost.') 226 227 # create a custom host class for this machine and return an instance of it 228 classes = (host_class, connectivity_class) 229 custom_host_class = type("%s_host" % hostname, classes, {}) 230 host_instance = custom_host_class(hostname, **args) 231 232 # call job_start if this is the first time this host is being used 233 if hostname not in _started_hostnames: 234 host_instance.job_start() 235 _started_hostnames.add(hostname) 236 237 return host_instance 238 239 240def create_target_machine(machine, **kwargs): 241 """Create the target machine, accounting for containers. 242 243 @param machine: A dict representing the test bed under test or a String 244 representing the testbed hostname (for legacy caller 245 support). 246 If it is a machine dict, the 'hostname' key is required. 247 Optional 'afe_host' key will pipe in afe_host 248 from the autoserv runtime or the AFE. 249 @param kwargs: Keyword args to pass to the testbed initialization. 250 251 @returns: The target machine to be used for verify/repair. 252 """ 253 is_moblab = CONFIG.get_config_value('SSP', 'is_moblab', type=bool, 254 default=False) 255 hostname = machine['hostname'] if isinstance(machine, dict) else machine 256 if (utils.is_in_container() and is_moblab and 257 hostname in ['localhost', '127.0.0.1']): 258 hostname = CONFIG.get_config_value('SSP', 'host_container_ip', type=str, 259 default=None) 260 if isinstance(machine, dict): 261 machine['hostname'] = hostname 262 else: 263 machine = hostname 264 logging.debug('Hostname of machine is converted to %s for the test to ' 265 'run inside a container.', hostname) 266 return create_host(machine, **kwargs) 267