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