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