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