xref: /aosp_15_r20/external/autotest/server/hosts/factory.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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