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 from autotest_lib.client.common_lib.cros.network import interface
     14 
     15 # Flag file used to tell backchannel script it's okay to run.
     16 BACKCHANNEL_FILE = '/mnt/stateful_partition/etc/enable_backchannel_network'
     17 # Backchannel interface name.
     18 BACKCHANNEL_IFACE_NAME = 'eth_test'
     19 # Script that handles backchannel heavy lifting.
     20 BACKCHANNEL_SCRIPT = '/usr/local/lib/flimflam/test/backchannel'
     21 
     22 
     23 class 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         result = self._run(
    151                 'ethtool %s' % BACKCHANNEL_IFACE_NAME, ignore_status=True)
    152         if result.exit_status:
    153             return False
    154         match = re.search('Port: (.+)', result.stdout)
    155         return match and _is_ethernet_port(match.group(1))
    156 
    157 
    158     def _reset_usb_ethernet_device(self):
    159         try:
    160             # Use the absolute path to the USB device instead of accessing it
    161             # via the path with the interface name because once we
    162             # deauthorize the USB device, the interface name will be gone.
    163             usb_authorized_path = os.path.realpath(
    164                     '/sys/class/net/%s/device/../authorized' % self.interface)
    165             logging.info('Reset ethernet device at %s', usb_authorized_path)
    166             utils.system('echo 0 > %s' % usb_authorized_path)
    167             time.sleep(10)
    168             utils.system('echo 1 > %s' % usb_authorized_path)
    169         except error.CmdError:
    170             pass
    171 
    172 
    173     def _get_default_route(self):
    174         """Retrieves default route information."""
    175         cmd = "route -n | awk '/^0.0.0.0/ { print $2, $8 }'"
    176         return self._run(cmd).stdout.split('\n')[0]
    177 
    178 
    179     def _is_test_iface_running(self):
    180         """Checks whether the test interface is running."""
    181         return interface.Interface(BACKCHANNEL_IFACE_NAME).is_link_operational()
    182 
    183 
    184     def _is_route_ready(self):
    185         """Checks for a route to the specified destination."""
    186         dest = self.gateway
    187         result = self._run('ping -c 1 %s' % dest, ignore_status=True)
    188         if result.exit_status:
    189             logging.warning('Route to %s is not ready.', dest)
    190             return False
    191         logging.info('Route to %s is ready.', dest)
    192         return True
    193 
    194 
    195 def is_network_iface_running(name):
    196     """
    197     Checks to see if the interface is running.
    198 
    199     @param name: Name of the interface to check.
    200 
    201     @returns True if the interface is running.
    202 
    203     """
    204     try:
    205         out = utils.system_output('ip link show dev %s' % name)
    206     except error.CmdError, e:
    207         logging.info(e)
    208         return False
    209 
    210     return out.find('state UP') >= 0
    211 
    212 
    213 def _is_ethernet_port(port):
    214     # Some versions of ethtool may report the full name.
    215     ETHTOOL_PORT_TWISTED_PAIR = 'TP'
    216     ETHTOOL_PORT_TWISTED_PAIR_FULL = 'Twisted Pair'
    217     ETHTOOL_PORT_MEDIA_INDEPENDENT_INTERFACE = 'MII'
    218     ETHTOOL_PORT_MEDIA_INDEPENDENT_INTERFACE_FULL = \
    219             'Media Independent Interface'
    220     return port in [ETHTOOL_PORT_TWISTED_PAIR,
    221                     ETHTOOL_PORT_TWISTED_PAIR_FULL,
    222                     ETHTOOL_PORT_MEDIA_INDEPENDENT_INTERFACE,
    223                     ETHTOOL_PORT_MEDIA_INDEPENDENT_INTERFACE_FULL]
    224 
    225 
    226 def is_backchannel_using_ethernet():
    227     """
    228     Checks to see if the backchannel is using an ethernet device.
    229 
    230     @returns True if the backchannel is using an ethernet device.
    231 
    232     """
    233     ethtool_output = utils.system_output(
    234             'ethtool %s' % BACKCHANNEL_IFACE_NAME, ignore_status=True)
    235     match = re.search('Port: (.+)', ethtool_output)
    236     return match and _is_ethernet_port(match.group(1))
    237