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