Home | History | Annotate | Download | only in cros
      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 
      5 import logging
      6 import os
      7 import re
      8 import time
      9 
     10 from autotest_lib.client.bin import local_host
     11 from autotest_lib.client.bin import utils
     12 from autotest_lib.client.common_lib import error
     13 
     14 # Flag file used to tell backchannel script it's okay to run.
     15 BACKCHANNEL_FILE = '/mnt/stateful_partition/etc/enable_backchannel_network'
     16 # Backchannel interface name.
     17 BACKCHANNEL_IFACE_NAME = 'eth_test'
     18 # Script that handles backchannel heavy lifting.
     19 BACKCHANNEL_SCRIPT = '/usr/local/lib/flimflam/test/backchannel'
     20 
     21 
     22 class 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 
    198 def 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 
    217 def _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 
    230 def 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