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