blob: d482279d99a80d4c67ffefe4b93f1fd5777d2b14 [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 httplib, logging, os, socket, subprocess, sys, time, xmlrpclib
import SocketServer
from autotest_lib.client.common_lib import error
from autotest_lib.server import autotest, test
class ServoTest(test.test):
"""AutoTest test class to serve as a parent class for FAFT tests.
TODO(jrbarnette): This class is a legacy, reflecting
refactoring that has begun but not completed. The long term
plan is to move all function here into FAFT specific classes.
http://crosbug.com/33305.
"""
version = 2
_REMOTE_PORT = 9990
_REMOTE_COMMAND = '/usr/local/autotest/cros/faft_client.py'
_REMOTE_COMMAND_SHORT = 'faft_client'
_REMOTE_LOG_FILE = '/tmp/faft_client.log'
_SSH_CONFIG = ('-o StrictHostKeyChecking=no '
'-o UserKnownHostsFile=/dev/null ')
def initialize(self, host, _, use_pyauto=False, use_faft=False):
"""Create a Servo object and install the dependency.
"""
# TODO(jrbarnette): Part of the incomplete refactoring:
# assert here that there are no legacy callers passing
# parameters for functionality that's been deprecated and
# removed.
assert use_faft and not use_pyauto
self.servo = host.servo
self.faft_client = None
self._client = host
self._ssh_tunnel = None
self._remote_process = None
self._local_port = None
# Initializes dut, may raise AssertionError if pre-defined gpio
# sequence to set GPIO's fail. Autotest does not handle exception
# throwing in initialize and will cause a test to hang.
try:
self.servo.initialize_dut()
except (AssertionError, xmlrpclib.Fault) as e:
raise error.TestFail(e)
# Install faft_client dependency.
self._autotest_client = autotest.Autotest(self._client)
self._autotest_client.install()
self._launch_client()
def _ping_test(self, hostname, timeout=5):
"""Verify whether a host responds to a ping.
Args:
hostname: Hostname to ping.
timeout: Time in seconds to wait for a response.
"""
with open(os.devnull, 'w') as fnull:
return subprocess.call(
['ping', '-c', '1', '-W', str(timeout), hostname],
stdout=fnull, stderr=fnull) == 0
def _sshd_test(self, hostname, timeout=5):
"""Verify whether sshd is running in host.
Args:
hostname: Hostname to verify.
timeout: Time in seconds to wait for a response.
"""
try:
sock = socket.create_connection((hostname, 22), timeout=timeout)
sock.close()
return True
except socket.timeout:
return False
except socket.error:
time.sleep(timeout)
return False
def _launch_client(self):
"""Launch a remote XML RPC connection on client with retrials.
"""
retry = 3
while retry:
try:
self._launch_client_once()
break
except AssertionError:
retry -= 1
if retry:
logging.info('Retry again...')
time.sleep(5)
else:
raise
def _launch_client_once(self):
"""Launch a remote process on client and set up an xmlrpc connection.
"""
if self._ssh_tunnel:
self._ssh_tunnel.terminate()
self._ssh_tunnel = None
# Launch RPC server remotely.
self._kill_remote_process()
self._launch_ssh_tunnel()
logging.info('Client command: %s', self._REMOTE_COMMAND)
logging.info("Logging to %s", self._REMOTE_LOG_FILE)
full_cmd = ['ssh -n -q %s root@%s \'%s &> %s\'' % (
self._SSH_CONFIG, self._client.ip,
self._REMOTE_COMMAND, self._REMOTE_LOG_FILE)]
logging.info('Starting process %s', ' '.join(full_cmd))
self._remote_process = subprocess.Popen(full_cmd, shell=True)
# Connect to RPC object.
logging.info('Connecting to client RPC server...')
remote_url = 'http://localhost:%s' % self._local_port
self.faft_client = xmlrpclib.ServerProxy(remote_url, allow_none=True)
logging.info('Server proxy: %s', remote_url)
# Poll for client RPC server to come online.
timeout = 20
succeed = False
rpc_error = None
while timeout > 0 and not succeed:
time.sleep(1)
try:
self.faft_client.system.is_available()
succeed = True
except (socket.error,
xmlrpclib.ProtocolError,
httplib.BadStatusLine) as e:
logging.info('caught: %s %s, tries left: %s',
repr(e), str(e), timeout)
# The client RPC server may not come online fast enough. Retry.
timeout -= 1
rpc_error = e
except:
logging.error('Unexpected error: %s', sys.exc_info()[0])
raise
if not succeed:
if isinstance(rpc_error, xmlrpclib.ProtocolError):
logging.info("A protocol error occurred")
logging.info("URL: %s", rpc_error.url)
logging.info("HTTP/HTTPS headers: %s", rpc_error.headers)
logging.info("Error code: %d", rpc_error.errcode)
logging.info("Error message: %s", rpc_error.errmsg)
p = subprocess.Popen([
'ssh -n -q %s root@%s \'cat %s\'' % (self._SSH_CONFIG,
self._client.ip, self._REMOTE_LOG_FILE)], shell=True,
stdout=subprocess.PIPE)
logging.info('Log of running remote %s:',
self._REMOTE_COMMAND_SHORT)
logging.info(p.communicate()[0])
assert succeed, 'Timed out connecting to client RPC server.'
def wait_for_client(self, install_deps=False, timeout=100):
"""Wait for the client to come back online.
New remote processes will be launched if their used flags are enabled.
@param install_deps: If True, install Autotest dependency when ready.
@param timeout: Time in seconds to wait for the client SSH daemon to
come up.
"""
# Ensure old ssh connections are terminated.
self._terminate_all_ssh()
# Wait for the client to come up.
while timeout > 0 and not self._sshd_test(self._client.ip, timeout=2):
timeout -= 2
assert (timeout > 0), 'Timed out waiting for client to reboot.'
logging.info('Server: Client machine is up.')
# Relaunch remote clients.
if install_deps:
self._autotest_client.install()
self._launch_client()
logging.info('Server: Relaunched remote %s.', 'faft')
def wait_for_client_offline(self, timeout=60, orig_boot_id=None):
"""Wait for the client to come offline.
@param timeout: Time in seconds to wait the client to come offline.
@param orig_boot_id: A string containing the original boot id.
"""
# Wait for the client to come offline.
while timeout > 0 and self._ping_test(self._client.ip, timeout=1):
time.sleep(1)
timeout -= 1
# As get_boot_id() requires DUT online. So we move the comparison here.
if timeout == 0 and orig_boot_id:
if self._client.get_boot_id() != orig_boot_id:
logging.warn('Reboot done very quickly.')
return
assert timeout, 'Timed out waiting for client offline.'
logging.info('Server: Client machine is offline.')
def kill_remote(self):
"""Call remote cleanup and kill ssh."""
if self._remote_process and self._remote_process.poll() is None:
try:
self.faft_client.cleanup()
logging.info('Cleanup succeeded.')
except xmlrpclib.ProtocolError, e:
logging.info('Cleanup returned protocol error: ' + str(e))
self._terminate_all_ssh()
def cleanup(self):
"""Delete the Servo object, call remote cleanup, and kill ssh."""
self.kill_remote()
def _find_unused_port(self):
"""Returns an unused TCP port."""
server = SocketServer.TCPServer(('localhost', 0),
SocketServer.BaseRequestHandler)
_, port = server.server_address
return port
def _launch_ssh_tunnel(self):
"""Establish an ssh tunnel for connecting to the remote RPC server.
"""
if self._local_port is None:
self._local_port = self._find_unused_port()
if not self._ssh_tunnel or self._ssh_tunnel.poll() is not None:
self._ssh_tunnel = subprocess.Popen([
'ssh -N -n -q %s -L %s:localhost:%s root@%s' %
(self._SSH_CONFIG, self._local_port, self._REMOTE_PORT,
self._client.ip)], shell=True)
assert self._ssh_tunnel.poll() is None, \
'The SSH tunnel on port %d is not up.' % self._local_port
def _kill_remote_process(self):
"""Ensure the remote process and local ssh process are terminated.
"""
kill_cmd = 'pkill -f %s' % self._REMOTE_COMMAND_SHORT
subprocess.call(['ssh -n -q %s root@%s \'%s\'' %
(self._SSH_CONFIG, self._client.ip, kill_cmd)],
shell=True)
if self._remote_process and self._remote_process.poll() is None:
self._remote_process.terminate()
def _terminate_all_ssh(self):
"""Terminate all ssh connections associated with remote processes."""
if self._ssh_tunnel and self._ssh_tunnel.poll() is None:
self._ssh_tunnel.terminate()
self._kill_remote_process()
self._ssh_tunnel = None