xref: /aosp_15_r20/external/autotest/server/hosts/chameleon_host.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright (c) 2014 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
6"""This file provides core logic for connecting a Chameleon Daemon."""
7
8import logging
9
10from autotest_lib.client.bin import utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib import global_config
13from autotest_lib.client.cros.chameleon import chameleon
14from autotest_lib.server.cros import dnsname_mangler
15from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
16from autotest_lib.server.hosts import ssh_host
17
18# Names of the host attributes in the database that represent the values for
19# the chameleon_host and chameleon_port for a servo connected to the DUT.
20CHAMELEON_HOST_ATTR = 'chameleon_host'
21CHAMELEON_PORT_ATTR = 'chameleon_port'
22
23_CONFIG = global_config.global_config
24ENABLE_SSH_TUNNEL_FOR_CHAMELEON = _CONFIG.get_config_value(
25        'CROS', 'enable_ssh_tunnel_for_chameleon', type=bool, default=False)
26
27class ChameleonHostError(Exception):
28    """Error in ChameleonHost."""
29    pass
30
31
32class ChameleonHost(ssh_host.SSHHost):
33    """Host class for a host that controls a Chameleon."""
34
35    # Chameleond process name.
36    CHAMELEOND_PROCESS = 'chameleond'
37
38
39    # TODO(waihong): Add verify and repair logic which are required while
40    # deploying to Cros Lab.
41
42
43    def _initialize(self, chameleon_host='localhost', chameleon_port=9992,
44                    *args, **dargs):
45        """Initialize a ChameleonHost instance.
46
47        A ChameleonHost instance represents a host that controls a Chameleon.
48
49        @param chameleon_host: Name of the host where the chameleond process
50                               is running.
51                               If this is passed in by IP address, it will be
52                               treated as not in lab.
53        @param chameleon_port: Port the chameleond process is listening on.
54
55        """
56        super(ChameleonHost, self)._initialize(hostname=chameleon_host,
57                                               *args, **dargs)
58
59        self._is_in_lab = None
60        self._check_if_is_in_lab()
61
62        self._chameleon_port = chameleon_port
63        self._local_port = None
64        self._tunneling_process = None
65
66        try:
67            if self._is_in_lab and not ENABLE_SSH_TUNNEL_FOR_CHAMELEON:
68                self._chameleon_connection = chameleon.ChameleonConnection(
69                        self.hostname, chameleon_port)
70            else:
71                # A proxy generator is passed as an argument so that a proxy
72                # could be re-created on demand in ChameleonConnection
73                # whenever needed, e.g., after a reboot.
74                proxy_generator = (
75                        lambda: self.rpc_server_tracker.xmlrpc_connect(
76                                None, chameleon_port,
77                                ready_test_name=chameleon.CHAMELEON_READY_TEST,
78                                timeout_seconds=60))
79                self._chameleon_connection = chameleon.ChameleonConnection(
80                        None, proxy_generator=proxy_generator)
81
82        except Exception as e:
83            raise ChameleonHostError('Can not connect to Chameleon: %s(%s)',
84                                     e.__class__, e)
85
86
87    def _check_if_is_in_lab(self):
88        """Checks if Chameleon host is in lab and set self._is_in_lab.
89
90        If self.hostname is an IP address, we treat it as is not in lab zone.
91
92        """
93        self._is_in_lab = (False if dnsname_mangler.is_ip_address(self.hostname)
94                           else utils.host_is_in_lab_zone(self.hostname))
95
96
97    def is_in_lab(self):
98        """Check whether the chameleon host is a lab device.
99
100        @returns: True if the chameleon host is in Cros Lab, otherwise False.
101
102        """
103        return self._is_in_lab
104
105
106    def get_wait_up_processes(self):
107        """Get the list of local processes to wait for in wait_up.
108
109        Override get_wait_up_processes in
110        autotest_lib.client.common_lib.hosts.base_classes.Host.
111        Wait for chameleond process to go up. Called by base class when
112        rebooting the device.
113
114        """
115        processes = [self.CHAMELEOND_PROCESS]
116        return processes
117
118
119    def create_chameleon_board(self):
120        """Create a ChameleonBoard object with error recovery.
121
122        This function will reboot the chameleon board once and retry if we can't
123        create chameleon board.
124
125        @return A ChameleonBoard object.
126        """
127        # TODO(waihong): Add verify and repair logic which are required while
128        # deploying to Cros Lab.
129        try:
130            chameleon_board = chameleon.ChameleonBoard(
131                    self._chameleon_connection, self)
132            return chameleon_board
133        except Exception as e:
134            raise ChameleonHostError('Can not create chameleon board: %s(%s)',
135                                     e.__class__, e)
136
137
138def create_chameleon_host(dut, chameleon_args):
139    """Create a ChameleonHost object.
140
141    There three possible cases:
142    1) If the DUT is in Cros Lab and has a chameleon board, then create
143       a ChameleonHost object pointing to the board. chameleon_args
144       is ignored.
145    2) If not case 1) and chameleon_args is neither None nor empty, then
146       create a ChameleonHost object using chameleon_args.
147    3) If neither case 1) or 2) applies, return None.
148
149    @param dut: host name of the host that chameleon connects. It can be used
150                to lookup the chameleon in test lab using naming convention.
151                If dut is an IP address, it can not be used to lookup the
152                chameleon in test lab.
153    @param chameleon_args: A dictionary that contains args for creating
154                           a ChameleonHost object,
155                           e.g. {'chameleon_host': '172.11.11.112',
156                                 'chameleon_port': 9992}.
157
158    @returns: A ChameleonHost object or None.
159
160    """
161    if not utils.is_in_container():
162        is_moblab = utils.is_moblab()
163    else:
164        is_moblab = _CONFIG.get_config_value(
165                'SSP', 'is_moblab', type=bool, default=False)
166
167    if not is_moblab:
168        dut_is_hostname = not dnsname_mangler.is_ip_address(dut)
169        if dut_is_hostname:
170            chameleon_hostname = chameleon.make_chameleon_hostname(dut)
171            if utils.host_is_in_lab_zone(chameleon_hostname):
172                # Be more tolerant on chameleon in the lab because
173                # we don't want dead chameleon blocks non-chameleon tests.
174                # We use ssh ping here as BeyondCorp-only hosts cannot make ICMP
175                # ping to chameleon test devices.
176                try:
177                    ssh_host.SSHHost(chameleon_hostname).ssh_ping()
178                except (error.AutoservSSHTimeout,
179                        error.AutoservSshPermissionDeniedError,
180                        error.AutoservSshPingHostError) as e:
181                    logging.warning(
182                            'Chameleon %s is not accessible. Please file a bug'
183                            ' to test lab: %s', chameleon_hostname, e)
184                    return None
185                return ChameleonHost(chameleon_host=chameleon_hostname)
186        if chameleon_args:
187            return ChameleonHost(**chameleon_args)
188        else:
189            return None
190    else:
191        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
192        hosts = afe.get_hosts(hostname=dut)
193        if hosts and CHAMELEON_HOST_ATTR in hosts[0].attributes:
194            return ChameleonHost(
195                chameleon_host=hosts[0].attributes[CHAMELEON_HOST_ATTR],
196                chameleon_port=hosts[0].attributes.get(
197                    CHAMELEON_PORT_ATTR, 9992)
198            )
199        else:
200            return None
201
202
203def create_btpeer_host(dut, btpeer_args_list):
204    """Create a ChameleonHost object for a Bluetooth peer
205
206    This is similar to create_chameleon_host but unlike chameleon board
207    there can be multiple btpeers with a single DUT
208
209    There four possible cases:
210    1) If the DUT is in Cros Lab then assume that it can have up to 4 bluetooth
211       peers. Ping the url and create a Chameleon host for each Bluetooth peer
212       present. btpeer_args_list is ignored.
213    2) If not case 1) and btpeer_args_list is not empty, then
214       create a BtpeerHost object for each host specified in btpeer_args_list.
215    3) If neither case 1) or 2) applies, return None.
216    4) This DUT is controlled  by moblab. This case is not implemented.
217
218
219    @param dut: host name of the host that btpeer connects. It can be used
220                to lookup the btpeer in test lab using naming convention.
221                If dut is an IP address, it can not be used to lookup the
222                btpeer in test lab. Naming convention in the lab is
223                <hostname>-btpeer[1-4]
224    @param btpeer_args_list: A list of dictionaries that contains args for
225                            creating a BtpeerHost object,
226                           e.g. {'btpeer_host': '172.11.11.112',
227                                 'btpeer_port': 9992}.
228
229    @returns: A list of BtpeerHost objects
230
231    """
232    def _convert_btpeer_args(args):
233        """Convert btpeer args to format accepted by ChameleonHost."""
234        ret_args = {}
235        if 'btpeer_host' in args:
236            ret_args['chameleon_host'] = args['btpeer_host']
237        if 'btpeer_port' in args:
238            ret_args['chameleon_port'] = int(args['btpeer_port'])
239        if 'btpeer_ssh_port' in args:
240            ret_args['port'] = int(args['btpeer_ssh_port'])
241        return ret_args
242
243    if not utils.is_in_container():
244        is_moblab = utils.is_moblab()
245    else:
246        is_moblab = _CONFIG.get_config_value(
247                'SSP', 'is_moblab', type=bool, default=False)
248
249    btpeer_hosts = []
250
251    if not is_moblab:
252        if (not dnsname_mangler.is_ip_address(dut) and
253            utils.host_is_in_lab_zone(dut)):
254            # This is a device in the lab. Ignore any arguments passed and
255            # derive peer hostnames from the DUT hostname
256            btpeer_hostnames = chameleon.make_btpeer_hostnames(dut)
257            for btpeer_hostname in btpeer_hostnames:
258                # Not all test bed have 4 Bluetooth peers
259                if utils.ping(btpeer_hostname, deadline=3):
260                    logging.warning('Btpeer %s is not accessible. This maybe '
261                                    'expected or it maybe an issue with the '
262                                    'Bluetooth peer. Please Check the test bed.'
263                                    , btpeer_hostname)
264                    continue
265                else:
266                    logging.debug("Creating btpeer from %s",btpeer_hostname)
267                    btpeer_hosts.append(
268                        ChameleonHost(chameleon_host=btpeer_hostname))
269            return btpeer_hosts
270        else:
271            # IP address given or DNS address is not in lab.
272            # Create the Bluetooth peers from the arguments passed
273            return [ ChameleonHost(**_convert_btpeer_args(btpeer_args))
274                     for btpeer_args in btpeer_args_list]
275    else:
276        # TODO(b:149606762)
277        # moblab still create Bluetooth peer from chameleon_args
278        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
279        hosts = afe.get_hosts(hostname=dut)
280        if hosts and CHAMELEON_HOST_ATTR in hosts[0].attributes:
281            return [ChameleonHost(
282                chameleon_host=hosts[0].attributes[CHAMELEON_HOST_ATTR],
283                chameleon_port=hosts[0].attributes.get(
284                    CHAMELEON_PORT_ATTR, 9992)
285            )]
286        else:
287            return []
288