blob: 46b0a40f48d117e3309f51762a23fcaf32d191a7 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import re
import time
from autotest_lib.client.bin import local_host
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros.network import interface
# Flag file used to tell backchannel script it's okay to run.
BACKCHANNEL_FILE = '/mnt/stateful_partition/etc/enable_backchannel_network'
# Backchannel interface name.
BACKCHANNEL_IFACE_NAME = 'eth_test'
# Script that handles backchannel heavy lifting.
BACKCHANNEL_SCRIPT = '/usr/local/lib/flimflam/test/backchannel'
class Backchannel(object):
"""Wrap backchannel in a context manager so it can be used with with.
Example usage:
with backchannel.Backchannel():
block
The backchannel will be torn down whether or not 'block' throws.
"""
def __init__(self, host=None, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.gateway = None
self.interface = None
if host is not None:
self.host = host
else:
self.host = local_host.LocalHost()
self._run = self.host.run
def __enter__(self):
self.setup(*self.args, **self.kwargs)
return self
def __exit__(self, exception, value, traceback):
self.teardown()
return False
def setup(self, create_ssh_routes=True):
"""
Enables the backchannel interface.
@param create_ssh_routes: If True set up routes so that all existing
SSH sessions will remain open.
@returns True if the backchannel is already set up, or was set up by
this call, otherwise False.
"""
# If the backchannel interface is already up there's nothing
# for us to do.
if self._is_test_iface_running():
return True
# Retrieve the gateway for the default route.
try:
# Poll here until we have route information.
# If shill was recently started, it will take some time before
# DHCP gives us an address.
line = utils.poll_for_condition(
lambda: self._get_default_route(),
exception=utils.TimeoutError(
'Timed out waiting for route information'),
timeout=30)
self.gateway, self.interface = line.strip().split(' ')
# Retrieve list of open ssh sessions so we can reopen
# routes afterward.
if create_ssh_routes:
out = self._run(
"netstat -tanp | grep :22 | "
"grep ESTABLISHED | awk '{print $5}'").stdout
# Extract IP from IP:PORT listing. Uses set to remove
# duplicates.
open_ssh = list(set(item.strip().split(':')[0] for item in
out.split('\n') if item.strip()))
# Build a command that will set up the test interface and add
# ssh routes in one shot. This is necessary since we'll lose
# connectivity to a remote host between these steps.
cmd = '%s setup %s' % (BACKCHANNEL_SCRIPT, self.interface)
if create_ssh_routes:
for ip in open_ssh:
# Add route using the pre-backchannel gateway.
cmd += '&& %s reach %s %s' % (BACKCHANNEL_SCRIPT, ip,
self.gateway)
self._run(cmd)
# Make sure we have a route to the gateway before continuing.
logging.info('Waiting for route to gateway %s', self.gateway)
utils.poll_for_condition(
lambda: self._is_route_ready(),
exception=utils.TimeoutError('Timed out waiting for route'),
timeout=30)
except Exception, e:
logging.error(e)
return False
finally:
# Remove backchannel file flag so system reverts to normal
# on reboot.
if os.path.isfile(BACKCHANNEL_FILE):
os.remove(BACKCHANNEL_FILE)
return True
def teardown(self):
"""Tears down the backchannel."""
if self.interface:
self._run('%s teardown %s' % (BACKCHANNEL_SCRIPT, self.interface))
# Hack around broken Asix network adaptors that may flake out when we
# bring them up and down (crbug.com/349264).
# TODO(thieule): Remove this when the adaptor/driver is fixed
# (crbug.com/350172).
try:
if self.gateway:
logging.info('Waiting for route restore to gateway %s',
self.gateway)
utils.poll_for_condition(
lambda: self._is_route_ready(),
exception=utils.TimeoutError(
'Timed out waiting for route'),
timeout=30)
except utils.TimeoutError:
if self.host is None:
self._reset_usb_ethernet_device()
def is_using_ethernet(self):
"""
Checks to see if the backchannel is using an ethernet device.
@returns True if the backchannel is using an ethernet device.
"""
# Check the port type reported by ethtool.
result = self._run('ethtool %s' % BACKCHANNEL_IFACE_NAME,
ignore_status=True)
if (result.exit_status == 0 and
re.search('Port: (TP|Twisted Pair|MII|Media Independent Interface)',
result.stdout)):
return True
# ethtool doesn't report the port type for some Ethernet adapters.
# Fall back to check against a list of known Ethernet adapters:
#
# 13b1:0041 - Linksys USB3GIG USB 3.0 Gigabit Ethernet Adapter
properties = self._get_udev_properties(BACKCHANNEL_IFACE_NAME)
# Depending on the udev version, ID_VENDOR_ID/ID_MODEL_ID may or may
# not have the 0x prefix, so we convert them to an integer value first.
bus = properties.get('ID_BUS', 'unknown').lower()
vendor_id = int(properties.get('ID_VENDOR_ID', '0000'), 16)
model_id = int(properties.get('ID_MODEL_ID', '0000'), 16)
device_id = '%s:%04x:%04x' % (bus, vendor_id, model_id)
if device_id in ['usb:13b1:0041']:
return True
return False
def _get_udev_properties(self, iface):
properties = {}
result = self._run('udevadm info -q property /sys/class/net/%s' % iface,
ignore_status=True)
if result.exit_status == 0:
for line in result.stdout.splitlines():
key, value = line.split('=', 1)
properties[key] = value
return properties
def _reset_usb_ethernet_device(self):
try:
# Use the absolute path to the USB device instead of accessing it
# via the path with the interface name because once we
# deauthorize the USB device, the interface name will be gone.
usb_authorized_path = os.path.realpath(
'/sys/class/net/%s/device/../authorized' % self.interface)
logging.info('Reset ethernet device at %s', usb_authorized_path)
utils.system('echo 0 > %s' % usb_authorized_path)
time.sleep(10)
utils.system('echo 1 > %s' % usb_authorized_path)
except error.CmdError:
pass
def _get_default_route(self):
"""Retrieves default route information."""
# Get default routes and parse out the gateway and interface.
cmd = "ip -4 route show table 0 | awk '/^default via/ { print $3, $5 }'"
return self._run(cmd).stdout.split('\n')[0]
def _is_test_iface_running(self):
"""Checks whether the test interface is running."""
return interface.Interface(BACKCHANNEL_IFACE_NAME).is_link_operational()
def _is_route_ready(self):
"""Checks for a route to the specified destination."""
dest = self.gateway
result = self._run('ping -c 1 %s' % dest, ignore_status=True)
if result.exit_status:
logging.warning('Route to %s is not ready.', dest)
return False
logging.info('Route to %s is ready.', dest)
return True