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.common_lib import error, global_config
8from autotest_lib.server import utils as server_utils
9from autotest_lib.server.hosts import cros_host, ssh_host
10from autotest_lib.server.hosts import moblab_host, sonic_host
11from autotest_lib.server.hosts import adb_host, testbed
12
13
14SSH_ENGINE = global_config.global_config.get_config_value('AUTOSERV',
15                                                          'ssh_engine',
16                                                          type=str)
17
18# Default ssh options used in creating a host.
19DEFAULT_SSH_USER = 'root'
20DEFAULT_SSH_PASS = ''
21DEFAULT_SSH_PORT = 22
22DEFAULT_SSH_VERBOSITY = ''
23DEFAULT_SSH_OPTIONS = ''
24
25# for tracking which hostnames have already had job_start called
26_started_hostnames = set()
27
28# A list of all the possible host types, ordered according to frequency of
29# host types in the lab, so the more common hosts don't incur a repeated ssh
30# overhead in checking for less common host types.
31host_types = [cros_host.CrosHost, moblab_host.MoblabHost, sonic_host.SonicHost,
32              adb_host.ADBHost,]
33OS_HOST_DICT = {'cros' : cros_host.CrosHost,
34                'android': adb_host.ADBHost}
35
36
37def _get_host_arguments():
38    """Returns parameters needed to ssh into a host.
39
40    There are currently 2 use cases for creating a host.
41    1. Through the server_job, in which case the server_job injects
42       the appropriate ssh parameters into our name space and they
43       are available as the variables ssh_user, ssh_pass etc.
44    2. Directly through factory.create_host, in which case we use
45       the same defaults as used in the server job to create a host.
46
47    @returns: A tuple of parameters needed to create an ssh connection, ordered
48              as: ssh_user, ssh_pass, ssh_port, ssh_verbosity, ssh_options.
49    """
50    g = globals()
51    return (g.get('ssh_user', DEFAULT_SSH_USER),
52            g.get('ssh_pass', DEFAULT_SSH_PASS),
53            g.get('ssh_port', DEFAULT_SSH_PORT),
54            g.get('ssh_verbosity_flag', DEFAULT_SSH_VERBOSITY),
55            g.get('ssh_options', DEFAULT_SSH_OPTIONS))
56
57
58def _detect_host(connectivity_class, hostname, **args):
59    """Detect host type.
60
61    Goes through all the possible host classes, calling check_host with a
62    basic host object. Currently this is an ssh host, but theoretically it
63    can be any host object that the check_host method of appropriate host
64    type knows to use.
65
66    @param connectivity_class: connectivity class to use to talk to the host
67                               (ParamikoHost or SSHHost)
68    @param hostname: A string representing the host name of the device.
69    @param args: Args that will be passed to the constructor of
70                 the host class.
71
72    @returns: Class type of the first host class that returns True to the
73              check_host method.
74    """
75    # TODO crbug.com/302026 (sbasi) - adjust this pathway for ADBHost in
76    # the future should a host require verify/repair.
77    with closing(connectivity_class(hostname, **args)) as host:
78        for host_module in host_types:
79            if host_module.check_host(host, timeout=10):
80                return host_module
81
82    logging.warning('Unable to apply conventional host detection methods, '
83                    'defaulting to chromeos host.')
84    return cros_host.CrosHost
85
86
87def _choose_connectivity_class(hostname, ssh_port):
88    """Choose a connectivity class for this hostname.
89
90    @param hostname: hostname that we need a connectivity class for.
91    @param ssh_port: SSH port to connect to the host.
92
93    @returns a connectivity host class.
94    """
95    if (hostname == 'localhost' and ssh_port == DEFAULT_SSH_PORT):
96        return local_host.LocalHost
97    # by default assume we're using SSH support
98    elif SSH_ENGINE == 'paramiko':
99        # Not all systems have paramiko installed so only import paramiko host
100        # if the global_config settings call for it.
101        from autotest_lib.server.hosts import paramiko_host
102        return paramiko_host.ParamikoHost
103    elif SSH_ENGINE == 'raw_ssh':
104        return ssh_host.SSHHost
105    else:
106        raise error.AutoServError("Unknown SSH engine %s. Please verify the "
107                                  "value of the configuration key 'ssh_engine' "
108                                  "on autotest's global_config.ini file." %
109                                  SSH_ENGINE)
110
111
112# TODO(kevcheng): Update the creation method so it's not a research project
113# determining the class inheritance model.
114def create_host(machine, host_class=None, connectivity_class=None, **args):
115    """Create a host object.
116
117    This method mixes host classes that are needed into a new subclass
118    and creates a instance of the new class.
119
120    @param machine: A dict representing the device under test or a String
121                    representing the DUT hostname (for legacy caller support).
122                    If it is a machine dict, the 'hostname' key is required.
123                    Optional 'host_attributes' key will pipe in host_attributes
124                    from the autoserv runtime or the AFE.
125    @param host_class: Host class to use, if None, will attempt to detect
126                       the correct class.
127    @param connectivity_class: Connectivity class to use, if None will decide
128                               based off of hostname and config settings.
129    @param args: Args that will be passed to the constructor of
130                 the new host class.
131
132    @returns: A host object which is an instance of the newly created
133              host class.
134    """
135    hostname, host_attributes = server_utils.get_host_info_from_machine(
136            machine)
137    args['host_attributes'] = host_attributes
138    ssh_user, ssh_pass, ssh_port, ssh_verbosity_flag, ssh_options = \
139            _get_host_arguments()
140
141    hostname, args['user'], args['password'], ssh_port = \
142            server_utils.parse_machine(hostname, ssh_user, ssh_pass, ssh_port)
143    args['ssh_verbosity_flag'] = ssh_verbosity_flag
144    args['ssh_options'] = ssh_options
145    args['port'] = ssh_port
146
147    if not connectivity_class:
148        connectivity_class = _choose_connectivity_class(hostname, ssh_port)
149    host_attributes = args.get('host_attributes', {})
150    host_class = host_class or OS_HOST_DICT.get(host_attributes.get('os_type'))
151    if host_class:
152        classes = [host_class, connectivity_class]
153    else:
154        classes = [_detect_host(connectivity_class, hostname, **args),
155                   connectivity_class]
156
157    # create a custom host class for this machine and return an instance of it
158    host_class = type("%s_host" % hostname, tuple(classes), {})
159    host_instance = host_class(hostname, **args)
160
161    # call job_start if this is the first time this host is being used
162    if hostname not in _started_hostnames:
163        host_instance.job_start()
164        _started_hostnames.add(hostname)
165
166    return host_instance
167
168
169def create_testbed(machine, **kwargs):
170    """Create the testbed object.
171
172    @param machine: A dict representing the test bed under test or a String
173                    representing the testbed hostname (for legacy caller
174                    support).
175                    If it is a machine dict, the 'hostname' key is required.
176                    Optional 'host_attributes' key will pipe in host_attributes
177                    from the autoserv runtime or the AFE.
178    @param kwargs: Keyword args to pass to the testbed initialization.
179
180    @returns: The testbed object with all associated host objects instantiated.
181    """
182    hostname, host_attributes = server_utils.get_host_info_from_machine(
183            machine)
184    kwargs['host_attributes'] = host_attributes
185    return testbed.TestBed(hostname, **kwargs)
186
187
188def create_target_machine(machine, **kwargs):
189    """Create the target machine which could be a testbed or a *Host.
190
191    @param machine: A dict representing the test bed under test or a String
192                    representing the testbed hostname (for legacy caller
193                    support).
194                    If it is a machine dict, the 'hostname' key is required.
195                    Optional 'host_attributes' key will pipe in host_attributes
196                    from the autoserv runtime or the AFE.
197    @param kwargs: Keyword args to pass to the testbed initialization.
198
199    @returns: The target machine to be used for verify/repair.
200    """
201    # TODO(kevcheng): We'll want to have a smarter way of figuring out which
202    # host to create (checking host labels).
203    if server_utils.machine_is_testbed(machine):
204        return create_testbed(machine, **kwargs)
205    return create_host(machine, **kwargs)
206