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