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