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