xref: /aosp_15_r20/external/autotest/client/cros/backchannel.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Liimport logging
7*9c5db199SXin Liimport os
8*9c5db199SXin Liimport re
9*9c5db199SXin Liimport time
10*9c5db199SXin Li
11*9c5db199SXin Lifrom autotest_lib.client.bin import local_host
12*9c5db199SXin Lifrom autotest_lib.client.bin import utils
13*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
14*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import interface
15*9c5db199SXin Li
16*9c5db199SXin Li# Flag file used to tell backchannel script it's okay to run.
17*9c5db199SXin LiBACKCHANNEL_FILE = '/mnt/stateful_partition/etc/enable_backchannel_network'
18*9c5db199SXin Li# Backchannel interface name.
19*9c5db199SXin LiBACKCHANNEL_IFACE_NAME = 'eth_test'
20*9c5db199SXin Li# Script that handles backchannel heavy lifting.
21*9c5db199SXin LiBACKCHANNEL_SCRIPT = '/usr/local/lib/flimflam/test/backchannel'
22*9c5db199SXin Li
23*9c5db199SXin Li
24*9c5db199SXin Liclass Backchannel(object):
25*9c5db199SXin Li    """Wrap backchannel in a context manager so it can be used with with.
26*9c5db199SXin Li
27*9c5db199SXin Li    Example usage:
28*9c5db199SXin Li         with backchannel.Backchannel():
29*9c5db199SXin Li                block
30*9c5db199SXin Li    The backchannel will be torn down whether or not 'block' throws.
31*9c5db199SXin Li    """
32*9c5db199SXin Li
33*9c5db199SXin Li    def __init__(self, host=None, *args, **kwargs):
34*9c5db199SXin Li        self.args = args
35*9c5db199SXin Li        self.kwargs = kwargs
36*9c5db199SXin Li        self.gateway = None
37*9c5db199SXin Li        self.interface = None
38*9c5db199SXin Li        if host is not None:
39*9c5db199SXin Li            self.host = host
40*9c5db199SXin Li        else:
41*9c5db199SXin Li            self.host = local_host.LocalHost()
42*9c5db199SXin Li        self._run = self.host.run
43*9c5db199SXin Li
44*9c5db199SXin Li    def __enter__(self):
45*9c5db199SXin Li        self.setup(*self.args, **self.kwargs)
46*9c5db199SXin Li        return self
47*9c5db199SXin Li
48*9c5db199SXin Li    def __exit__(self, exception, value, traceback):
49*9c5db199SXin Li        self.teardown()
50*9c5db199SXin Li        return False
51*9c5db199SXin Li
52*9c5db199SXin Li    def setup(self, create_ssh_routes=True):
53*9c5db199SXin Li        """
54*9c5db199SXin Li        Enables the backchannel interface.
55*9c5db199SXin Li
56*9c5db199SXin Li        @param create_ssh_routes: If True set up routes so that all existing
57*9c5db199SXin Li                SSH sessions will remain open.
58*9c5db199SXin Li
59*9c5db199SXin Li        @returns True if the backchannel is already set up, or was set up by
60*9c5db199SXin Li                this call, otherwise False.
61*9c5db199SXin Li
62*9c5db199SXin Li        """
63*9c5db199SXin Li
64*9c5db199SXin Li        # If the backchannel interface is already up there's nothing
65*9c5db199SXin Li        # for us to do.
66*9c5db199SXin Li        if self._is_test_iface_running():
67*9c5db199SXin Li            return True
68*9c5db199SXin Li
69*9c5db199SXin Li        # Retrieve the gateway for the default route.
70*9c5db199SXin Li        try:
71*9c5db199SXin Li            # Poll here until we have route information.
72*9c5db199SXin Li            # If shill was recently started, it will take some time before
73*9c5db199SXin Li            # DHCP gives us an address.
74*9c5db199SXin Li            line = utils.poll_for_condition(
75*9c5db199SXin Li                    lambda: self._get_default_route(),
76*9c5db199SXin Li                    exception=utils.TimeoutError(
77*9c5db199SXin Li                            'Timed out waiting for route information'),
78*9c5db199SXin Li                    timeout=30)
79*9c5db199SXin Li            self.gateway, self.interface = line.strip().split(' ')
80*9c5db199SXin Li
81*9c5db199SXin Li            # Retrieve list of open ssh sessions so we can reopen
82*9c5db199SXin Li            # routes afterward.
83*9c5db199SXin Li            if create_ssh_routes:
84*9c5db199SXin Li                out = self._run(
85*9c5db199SXin Li                        "netstat -tanp | grep :22 | "
86*9c5db199SXin Li                        "grep ESTABLISHED | awk '{print $5}'").stdout
87*9c5db199SXin Li                # Extract IP from IP:PORT listing. Uses set to remove
88*9c5db199SXin Li                # duplicates.
89*9c5db199SXin Li                open_ssh = list(set(item.strip().split(':')[0] for item in
90*9c5db199SXin Li                                    out.split('\n') if item.strip()))
91*9c5db199SXin Li
92*9c5db199SXin Li            # Build a command that will set up the test interface and add
93*9c5db199SXin Li            # ssh routes in one shot. This is necessary since we'll lose
94*9c5db199SXin Li            # connectivity to a remote host between these steps.
95*9c5db199SXin Li            cmd = '%s setup %s' % (BACKCHANNEL_SCRIPT, self.interface)
96*9c5db199SXin Li            if create_ssh_routes:
97*9c5db199SXin Li                for ip in open_ssh:
98*9c5db199SXin Li                    # Add route using the pre-backchannel gateway.
99*9c5db199SXin Li                    cmd += '&& %s reach %s %s' % (BACKCHANNEL_SCRIPT, ip,
100*9c5db199SXin Li                            self.gateway)
101*9c5db199SXin Li
102*9c5db199SXin Li            self._run(cmd)
103*9c5db199SXin Li
104*9c5db199SXin Li            # Make sure we have a route to the gateway before continuing.
105*9c5db199SXin Li            logging.info('Waiting for route to gateway %s', self.gateway)
106*9c5db199SXin Li            utils.poll_for_condition(
107*9c5db199SXin Li                    lambda: self._is_route_ready(),
108*9c5db199SXin Li                    exception=utils.TimeoutError('Timed out waiting for route'),
109*9c5db199SXin Li                    timeout=30)
110*9c5db199SXin Li        except Exception as e:
111*9c5db199SXin Li            logging.error(e)
112*9c5db199SXin Li            return False
113*9c5db199SXin Li        finally:
114*9c5db199SXin Li            # Remove backchannel file flag so system reverts to normal
115*9c5db199SXin Li            # on reboot.
116*9c5db199SXin Li            if os.path.isfile(BACKCHANNEL_FILE):
117*9c5db199SXin Li                os.remove(BACKCHANNEL_FILE)
118*9c5db199SXin Li
119*9c5db199SXin Li        return True
120*9c5db199SXin Li
121*9c5db199SXin Li    def teardown(self):
122*9c5db199SXin Li        """Tears down the backchannel."""
123*9c5db199SXin Li        if self.interface:
124*9c5db199SXin Li            self._run('%s teardown %s' % (BACKCHANNEL_SCRIPT, self.interface))
125*9c5db199SXin Li
126*9c5db199SXin Li        # Hack around broken Asix network adaptors that may flake out when we
127*9c5db199SXin Li        # bring them up and down (crbug.com/349264).
128*9c5db199SXin Li        # TODO(thieule): Remove this when the adaptor/driver is fixed
129*9c5db199SXin Li        # (crbug.com/350172).
130*9c5db199SXin Li        try:
131*9c5db199SXin Li            if self.gateway:
132*9c5db199SXin Li                logging.info('Waiting for route restore to gateway %s',
133*9c5db199SXin Li                             self.gateway)
134*9c5db199SXin Li                utils.poll_for_condition(
135*9c5db199SXin Li                        lambda: self._is_route_ready(),
136*9c5db199SXin Li                        exception=utils.TimeoutError(
137*9c5db199SXin Li                                'Timed out waiting for route'),
138*9c5db199SXin Li                        timeout=30)
139*9c5db199SXin Li        except utils.TimeoutError:
140*9c5db199SXin Li            if self.host is None:
141*9c5db199SXin Li                self._reset_usb_ethernet_device()
142*9c5db199SXin Li
143*9c5db199SXin Li
144*9c5db199SXin Li    def is_using_ethernet(self):
145*9c5db199SXin Li        """
146*9c5db199SXin Li        Checks to see if the backchannel is using an ethernet device.
147*9c5db199SXin Li
148*9c5db199SXin Li        @returns True if the backchannel is using an ethernet device.
149*9c5db199SXin Li
150*9c5db199SXin Li        """
151*9c5db199SXin Li        # Check the port type reported by ethtool.
152*9c5db199SXin Li        result = self._run('ethtool %s' % BACKCHANNEL_IFACE_NAME,
153*9c5db199SXin Li                           ignore_status=True)
154*9c5db199SXin Li        if (result.exit_status == 0 and
155*9c5db199SXin Li            re.search('Port: (TP|Twisted Pair|MII|Media Independent Interface)',
156*9c5db199SXin Li                      result.stdout)):
157*9c5db199SXin Li            return True
158*9c5db199SXin Li
159*9c5db199SXin Li        # ethtool doesn't report the port type for some Ethernet adapters.
160*9c5db199SXin Li        # Fall back to check against a list of known Ethernet adapters:
161*9c5db199SXin Li        #
162*9c5db199SXin Li        #   13b1:0041 - Linksys USB3GIG USB 3.0 Gigabit Ethernet Adapter
163*9c5db199SXin Li        properties = self._get_udev_properties(BACKCHANNEL_IFACE_NAME)
164*9c5db199SXin Li        # Depending on the udev version, ID_VENDOR_ID/ID_MODEL_ID may or may
165*9c5db199SXin Li        # not have the 0x prefix, so we convert them to an integer value first.
166*9c5db199SXin Li        bus = properties.get('ID_BUS', 'unknown').lower()
167*9c5db199SXin Li        vendor_id = int(properties.get('ID_VENDOR_ID', '0000'), 16)
168*9c5db199SXin Li        model_id = int(properties.get('ID_MODEL_ID', '0000'), 16)
169*9c5db199SXin Li        device_id = '%s:%04x:%04x' % (bus, vendor_id, model_id)
170*9c5db199SXin Li        if device_id in ['usb:13b1:0041']:
171*9c5db199SXin Li            return True
172*9c5db199SXin Li
173*9c5db199SXin Li        return False
174*9c5db199SXin Li
175*9c5db199SXin Li
176*9c5db199SXin Li    def _get_udev_properties(self, iface):
177*9c5db199SXin Li        properties = {}
178*9c5db199SXin Li        result = self._run('udevadm info -q property /sys/class/net/%s' % iface,
179*9c5db199SXin Li                           ignore_status=True)
180*9c5db199SXin Li        if result.exit_status == 0:
181*9c5db199SXin Li            for line in result.stdout.splitlines():
182*9c5db199SXin Li                key, value = line.split('=', 1)
183*9c5db199SXin Li                properties[key] = value
184*9c5db199SXin Li
185*9c5db199SXin Li        return properties
186*9c5db199SXin Li
187*9c5db199SXin Li
188*9c5db199SXin Li    def _reset_usb_ethernet_device(self):
189*9c5db199SXin Li        try:
190*9c5db199SXin Li            # Use the absolute path to the USB device instead of accessing it
191*9c5db199SXin Li            # via the path with the interface name because once we
192*9c5db199SXin Li            # deauthorize the USB device, the interface name will be gone.
193*9c5db199SXin Li            usb_authorized_path = os.path.realpath(
194*9c5db199SXin Li                    '/sys/class/net/%s/device/../authorized' % self.interface)
195*9c5db199SXin Li            logging.info('Reset ethernet device at %s', usb_authorized_path)
196*9c5db199SXin Li            utils.system('echo 0 > %s' % usb_authorized_path)
197*9c5db199SXin Li            time.sleep(10)
198*9c5db199SXin Li            utils.system('echo 1 > %s' % usb_authorized_path)
199*9c5db199SXin Li        except error.CmdError:
200*9c5db199SXin Li            pass
201*9c5db199SXin Li
202*9c5db199SXin Li
203*9c5db199SXin Li    def _get_default_route(self):
204*9c5db199SXin Li        """Retrieves default route information."""
205*9c5db199SXin Li        # Get default routes and parse out the gateway and interface.
206*9c5db199SXin Li        cmd = "ip -4 route show table 0 | awk '/^default via/ { print $3, $5 }'"
207*9c5db199SXin Li        return self._run(cmd).stdout.split('\n')[0]
208*9c5db199SXin Li
209*9c5db199SXin Li
210*9c5db199SXin Li    def _is_test_iface_running(self):
211*9c5db199SXin Li        """Checks whether the test interface is running."""
212*9c5db199SXin Li        return interface.Interface(BACKCHANNEL_IFACE_NAME).is_link_operational()
213*9c5db199SXin Li
214*9c5db199SXin Li
215*9c5db199SXin Li    def _is_route_ready(self):
216*9c5db199SXin Li        """Checks for a route to the specified destination."""
217*9c5db199SXin Li        dest = self.gateway
218*9c5db199SXin Li        result = self._run('ping -c 1 %s' % dest, ignore_status=True)
219*9c5db199SXin Li        if result.exit_status:
220*9c5db199SXin Li            logging.warning('Route to %s is not ready.', dest)
221*9c5db199SXin Li            return False
222*9c5db199SXin Li        logging.info('Route to %s is ready.', dest)
223*9c5db199SXin Li        return True
224