blob: 9aa1948eaa7542e4b2965585968543c7424f9945 [file]
#!/usr/bin/env python
# Copyright (c) 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Run a command and report its results in machine-readable format."""
import optparse
import os
import pickle
import pprint
import socket
import subprocess
import sys
buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
os.pardir))
sys.path.append(os.path.join(buildbot_path))
from site_config import slave_hosts_cfg
# We print this string before and after the important output from the command.
# This makes it easy to ignore output from SSH, shells, etc.
BOOKEND_STR = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
def encode_results(results):
"""Convert a dictionary of results into a machine-readable string.
Args:
results: dict, the results of a command.
Returns:
A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
"""
return BOOKEND_STR + pickle.dumps(results).encode('hex') + BOOKEND_STR
def decode_results(results_str):
"""Convert a machine-readable string into a dictionary of results.
Args:
results_str: string; output from "run" or one of its siblings.
Returns:
A dictionary of results.
"""
return pickle.loads(results_str.split(BOOKEND_STR)[1].decode('hex'))
def cmd_results(stdout='', stderr='', returncode=1):
"""Create a results dict for a command.
Args:
stdout: string; stdout from a command.
stderr: string; stderr from a command.
returncode: string; return code of a command.
"""
return {'stdout': stdout,
'stderr': stderr,
'returncode': returncode}
def _launch_cmd(cmd):
"""Launch the given command. Non-blocking.
Args:
cmd: list of strings; command to run.
Returns:
subprocess.Popen instance.
"""
return subprocess.Popen(cmd, shell=False, stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
def _get_result(popen):
"""Get the results from a running process. Blocks until the process completes.
Args:
popen: subprocess.Popen instance.
Returns:
A dictionary with stdout, stderr, and returncode as keys.
"""
stdout, stderr = popen.communicate()
return cmd_results(stdout=stdout,
stderr=stderr,
returncode=popen.returncode)
def run(cmd):
"""Run the command, block until it completes, and return a results dictionary.
Args:
cmd: string or list of strings; the command to run.
Returns:
A dictionary with stdout, stderr, and returncode as keys.
"""
try:
proc = _launch_cmd(cmd)
except OSError as e:
return cmd_results(stderr=str(e))
return _get_result(proc)
def run_on_local_slaves(cmd):
"""Run the command on each local buildslave, blocking until completion.
Args:
cmd: list of strings; the command to run.
Returns:
A dictionary of results with buildslave names as keys and individual
result dictionaries (with stdout, stderr, and returncode as keys) as
values.
"""
slave_host = slave_hosts_cfg.GetSlaveHostConfig(socket.gethostname())
slaves = slave_host['slaves']
results = {}
procs = []
for (slave, _) in slaves:
os.chdir(os.path.join(buildbot_path, slave, 'buildbot'))
procs.append((slave, _launch_cmd(cmd)))
for slavename, proc in procs:
results[slavename] = _get_result(proc)
return results
def _launch_on_remote_host(slave_host_name, cmd):
"""Launch the command on a remote slave host machine. Non-blocking.
Args:
slave_host_name: string; name of the slave host machine.
cmd: list of strings; command to run.
Returns:
subprocess.Popen instance.
"""
login_cmd = slave_hosts_cfg.get_login_command(slave_host_name)
if not login_cmd:
raise ValueError('%s does not have a remote login procedure defined in '
'slave_hosts_cfg.py.' % slave_host_name)
host = slave_hosts_cfg.SLAVE_HOSTS[slave_host_name]
path_to_buildbot = host['path_module'].join(*host['path_to_buildbot'])
path_to_run_cmd = host['path_module'].join(path_to_buildbot, 'scripts',
'run_cmd.py')
return _launch_cmd(login_cmd + ['python', path_to_run_cmd] + cmd)
def _get_remote_host_results(slave_host_name, popen):
"""Get the results from a running process. Blocks until the process completes.
Args:
slave_host_name: string; name of the remote host.
popen: subprocess.Popen instance.
Returns:
A dictionary of results with the remote host machine name as its only key
and individual result dictionaries (with stdout, stderr, and returncode as
keys) its value.
"""
result = _get_result(popen)
if result['returncode']:
return { slave_host_name: result }
return { slave_host_name: decode_results(result['stdout']) }
def run_on_remote_host(slave_host_name, cmd):
"""Run a command on a remote slave host machine, blocking until completion.
Args:
slave_host_name: string; name of the slave host machine.
cmd: list of strings; command to run.
Returns:
A dictionary of results with the remote host machine name as its only key
and individual result dictionaries (with stdout, stderr, and returncode as
keys) its value.
"""
proc = _launch_on_remote_host(slave_host_name, cmd)
return _get_remote_host_results(slave_host_name, proc)
def run_on_all_slave_hosts(cmd):
"""Run the given command on all slave hosts, blocking until all complete.
Args:
cmd: list of strings; command to run.
Returns:
A dictionary of results with host machine names as keys and individual
result dictionaries (with stdout, stderr, and returncode as keys) as
values.
"""
results = {}
procs = []
for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys():
if not slave_hosts_cfg.get_login_command(hostname):
results.update({hostname: cmd_results(stderr='No procedure for login.')})
else:
procs.append((hostname, _launch_on_remote_host(hostname, cmd)))
for slavename, proc in procs:
results.update(_get_remote_host_results(slavename, proc))
return results
def print_results(results, pretty=False):
"""Print the results of a command.
Args:
results: dict; the results from a command.
pretty: bool; whether or not to print in human-readable format.
"""
if pretty:
print pprint.pformat(results)
else:
print repr(encode_results(results))
def parse_args(positional_args=None):
"""Common argument parser for scripts using this module.
Args:
positional_args: optional list of strings; extra positional arguments to
the script.
"""
parser = optparse.OptionParser()
parser.disable_interspersed_args()
parser.add_option('-p', '--pretty', action='store_true', dest='pretty',
help='Print output in a human-readable form.')
# Fixup the usage message to include the positional args.
cmd = 'cmd'
all_positional_args = (positional_args or []) + [cmd]
usage = parser.get_usage().rstrip()
for arg in all_positional_args:
usage += ' ' + arg
parser.set_usage(usage)
options, args = parser.parse_args()
# Set positional arguments.
for positional_arg in positional_args or []:
try:
setattr(options, positional_arg, args[0])
except IndexError:
parser.print_usage()
sys.exit(1)
args = args[1:]
# Everything else is part of the command to run.
try:
setattr(options, cmd, args)
except IndexError:
parser.print_usage()
sys.exit(1)
return options
if '__main__' == __name__:
parsed_args = parse_args()
print_results(run(parsed_args.cmd), parsed_args.pretty)