• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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