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