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 error, global_config
9from autotest_lib.server import utils as server_utils
10from autotest_lib.server.cros.dynamic_suite import constants
11from autotest_lib.server.hosts import adb_host, cros_host, emulated_adb_host
12from autotest_lib.server.hosts import iota_host, moblab_host, sonic_host
13from autotest_lib.server.hosts import ssh_host, testbed
14
15
16CONFIG = global_config.global_config
17
18SSH_ENGINE = CONFIG.get_config_value('AUTOSERV', 'ssh_engine', type=str)
19
20# Default ssh options used in creating a host.
21DEFAULT_SSH_USER = 'root'
22DEFAULT_SSH_PASS = ''
23DEFAULT_SSH_PORT = 22
24DEFAULT_SSH_VERBOSITY = ''
25DEFAULT_SSH_OPTIONS = ''
26
27# for tracking which hostnames have already had job_start called
28_started_hostnames = set()
29
30# A list of all the possible host types, ordered according to frequency of
31# host types in the lab, so the more common hosts don't incur a repeated ssh
32# overhead in checking for less common host types.
33host_types = [cros_host.CrosHost, moblab_host.MoblabHost, sonic_host.SonicHost,
34              adb_host.ADBHost,]
35OS_HOST_DICT = {'android': adb_host.ADBHost,
36                'brillo': adb_host.ADBHost,
37                'cros' : cros_host.CrosHost,
38                'emulated_brillo': emulated_adb_host.EmulatedADBHost,
39                'iota': iota_host.IotaHost,
40                'moblab': moblab_host.MoblabHost}
41
42
43def _get_host_arguments(machine):
44    """Get parameters to construct a host object.
45
46    There are currently 2 use cases for creating a host.
47    1. Through the server_job, in which case the server_job injects
48       the appropriate ssh parameters into our name space and they
49       are available as the variables ssh_user, ssh_pass etc.
50    2. Directly through factory.create_host, in which case we use
51       the same defaults as used in the server job to create a host.
52
53    @param machine: machine dict
54    @return: A dictionary containing arguments for host specifically hostname,
55              afe_host, user, password, port, ssh_verbosity_flag and
56              ssh_options.
57    """
58    hostname, afe_host = server_utils.get_host_info_from_machine(
59            machine)
60
61    g = globals()
62    user = afe_host.attributes.get('ssh_user', g.get('ssh_user',
63                                                     DEFAULT_SSH_USER))
64    password = afe_host.attributes.get('ssh_pass', g.get('ssh_pass',
65                                                         DEFAULT_SSH_PASS))
66    port = afe_host.attributes.get('ssh_port', g.get('ssh_port',
67                                                     DEFAULT_SSH_PORT))
68    ssh_verbosity_flag = afe_host.attributes.get('ssh_verbosity_flag',
69                                                 g.get('ssh_verbosity_flag',
70                                                       DEFAULT_SSH_VERBOSITY))
71    ssh_options = afe_host.attributes.get('ssh_options',
72                                          g.get('ssh_options',
73                                                DEFAULT_SSH_OPTIONS))
74
75    hostname, user, password, port = server_utils.parse_machine(hostname, user,
76                                                                password, port)
77
78    host_args = {
79            'hostname': hostname,
80            'afe_host': afe_host,
81            'user': user,
82            'password': password,
83            'port': int(port),
84            'ssh_verbosity_flag': ssh_verbosity_flag,
85            'ssh_options': ssh_options,
86    }
87    if isinstance(machine, dict) and 'host_info_store' in machine:
88        host_args['host_info_store'] = machine['host_info_store']
89    return host_args
90
91
92def _detect_host(connectivity_class, hostname, **args):
93    """Detect host type.
94
95    Goes through all the possible host classes, calling check_host with a
96    basic host object. Currently this is an ssh host, but theoretically it
97    can be any host object that the check_host method of appropriate host
98    type knows to use.
99
100    @param connectivity_class: connectivity class to use to talk to the host
101                               (ParamikoHost or SSHHost)
102    @param hostname: A string representing the host name of the device.
103    @param args: Args that will be passed to the constructor of
104                 the host class.
105
106    @returns: Class type of the first host class that returns True to the
107              check_host method.
108    """
109    # TODO crbug.com/302026 (sbasi) - adjust this pathway for ADBHost in
110    # the future should a host require verify/repair.
111    with closing(connectivity_class(hostname, **args)) as host:
112        for host_module in host_types:
113            if host_module.check_host(host, timeout=10):
114                return host_module
115
116    logging.warning('Unable to apply conventional host detection methods, '
117                    'defaulting to chromeos host.')
118    return cros_host.CrosHost
119
120
121def _choose_connectivity_class(hostname, ssh_port):
122    """Choose a connectivity class for this hostname.
123
124    @param hostname: hostname that we need a connectivity class for.
125    @param ssh_port: SSH port to connect to the host.
126
127    @returns a connectivity host class.
128    """
129    if (hostname == 'localhost' and ssh_port == DEFAULT_SSH_PORT):
130        return local_host.LocalHost
131    # by default assume we're using SSH support
132    elif SSH_ENGINE == 'raw_ssh':
133        return ssh_host.SSHHost
134    else:
135        raise error.AutoservError("Unknown SSH engine %s. Please verify the "
136                                  "value of the configuration key 'ssh_engine' "
137                                  "on autotest's global_config.ini file." %
138                                  SSH_ENGINE)
139
140
141# TODO(kevcheng): Update the creation method so it's not a research project
142# determining the class inheritance model.
143def create_host(machine, host_class=None, connectivity_class=None, **args):
144    """Create a host object.
145
146    This method mixes host classes that are needed into a new subclass
147    and creates a instance of the new class.
148
149    @param machine: A dict representing the device under test or a String
150                    representing the DUT hostname (for legacy caller support).
151                    If it is a machine dict, the 'hostname' key is required.
152                    Optional 'afe_host' key will pipe in afe_host
153                    from the autoserv runtime or the AFE.
154    @param host_class: Host class to use, if None, will attempt to detect
155                       the correct class.
156    @param connectivity_class: Connectivity class to use, if None will decide
157                               based off of hostname and config settings.
158    @param args: Args that will be passed to the constructor of
159                 the new host class.
160
161    @returns: A host object which is an instance of the newly created
162              host class.
163    """
164    detected_args = _get_host_arguments(machine)
165    hostname = detected_args.pop('hostname')
166    afe_host = detected_args['afe_host']
167    args.update(detected_args)
168
169    host_os = None
170    full_os_prefix = constants.OS_PREFIX + ':'
171    # Let's grab the os from the labels if we can for host class detection.
172    for label in afe_host.labels:
173        if label.startswith(full_os_prefix):
174            host_os = label[len(full_os_prefix):]
175            break
176
177    if not connectivity_class:
178        connectivity_class = _choose_connectivity_class(hostname, args['port'])
179    # TODO(kevcheng): get rid of the host detection using host attributes.
180    host_class = (host_class
181                  or OS_HOST_DICT.get(afe_host.attributes.get('os_type'))
182                  or OS_HOST_DICT.get(host_os)
183                  or _detect_host(connectivity_class, hostname, **args))
184
185    # create a custom host class for this machine and return an instance of it
186    classes = (host_class, connectivity_class)
187    custom_host_class = type("%s_host" % hostname, classes, {})
188    host_instance = custom_host_class(hostname, **args)
189
190    # call job_start if this is the first time this host is being used
191    if hostname not in _started_hostnames:
192        host_instance.job_start()
193        _started_hostnames.add(hostname)
194
195    return host_instance
196
197
198def create_testbed(machine, **kwargs):
199    """Create the testbed object.
200
201    @param machine: A dict representing the test bed under test or a String
202                    representing the testbed hostname (for legacy caller
203                    support).
204                    If it is a machine dict, the 'hostname' key is required.
205                    Optional 'afe_host' key will pipe in afe_host from
206                    the afe_host object from the autoserv runtime or the AFE.
207    @param kwargs: Keyword args to pass to the testbed initialization.
208
209    @returns: The testbed object with all associated host objects instantiated.
210    """
211    detected_args = _get_host_arguments(machine)
212    hostname = detected_args.pop('hostname')
213    kwargs.update(detected_args)
214    return testbed.TestBed(hostname, **kwargs)
215
216
217def create_target_machine(machine, **kwargs):
218    """Create the target machine which could be a testbed or a *Host.
219
220    @param machine: A dict representing the test bed under test or a String
221                    representing the testbed hostname (for legacy caller
222                    support).
223                    If it is a machine dict, the 'hostname' key is required.
224                    Optional 'afe_host' key will pipe in afe_host
225                    from the autoserv runtime or the AFE.
226    @param kwargs: Keyword args to pass to the testbed initialization.
227
228    @returns: The target machine to be used for verify/repair.
229    """
230    # For Brillo/Android devices connected to moblab, the `machine` name is
231    # either `localhost` or `127.0.0.1`. It needs to be translated to the host
232    # container IP if the code is running inside a container. This way, autoserv
233    # can ssh to the moblab and run actual adb/fastboot commands.
234    is_moblab = CONFIG.get_config_value('SSP', 'is_moblab', type=bool,
235                                        default=False)
236    hostname = machine['hostname'] if isinstance(machine, dict) else machine
237    if (utils.is_in_container() and is_moblab and
238        hostname in ['localhost', '127.0.0.1']):
239        hostname = CONFIG.get_config_value('SSP', 'host_container_ip', type=str,
240                                           default=None)
241        if isinstance(machine, dict):
242            machine['hostname'] = hostname
243        else:
244            machine = hostname
245        logging.debug('Hostname of machine is converted to %s for the test to '
246                      'run inside a container.', hostname)
247
248    # TODO(kevcheng): We'll want to have a smarter way of figuring out which
249    # host to create (checking host labels).
250    if server_utils.machine_is_testbed(machine):
251        return create_testbed(machine, **kwargs)
252    return create_host(machine, **kwargs)
253