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