| Index: scripts/run_cmd.py
|
| diff --git a/scripts/run_cmd.py b/scripts/run_cmd.py
|
| deleted file mode 100755
|
| index c0d7d05a249cf843659407479bf025610f5aedcf..0000000000000000000000000000000000000000
|
| --- a/scripts/run_cmd.py
|
| +++ /dev/null
|
| @@ -1,461 +0,0 @@
|
| -#!/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 collections
|
| -import optparse
|
| -import os
|
| -import pickle
|
| -import pprint
|
| -import socket
|
| -import subprocess
|
| -import sys
|
| -import traceback
|
| -
|
| -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
|
| -
|
| -
|
| -class BaseCommandResults(object):
|
| - """Base class for CommandResults classes."""
|
| -
|
| - # 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(self):
|
| - """Convert the results into a machine-readable string.
|
| -
|
| - Returns:
|
| - A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
|
| - """
|
| - raise NotImplementedError()
|
| -
|
| - @staticmethod
|
| - def decode(results_str):
|
| - """Convert a machine-readable string into a CommandResults instance.
|
| -
|
| - Args:
|
| - results_str: string; output from "run" or one of its siblings.
|
| - Returns:
|
| - A dictionary of results.
|
| - """
|
| - decoded_dict = pickle.loads(
|
| - results_str.split(BaseCommandResults.BOOKEND_STR)[1].decode('hex'))
|
| - errors = []
|
| - # First, try to interpret the dict as SingleCommandResults.
|
| - try:
|
| - # This will fail unless decoded_dict has the following set of keys:
|
| - # ('returncode', 'stdout', 'stderr')
|
| - return SingleCommandResults(**decoded_dict)
|
| - except TypeError:
|
| - errors.append(traceback.format_exc())
|
| - # Next, try to interpret the dict as MultiCommandResults.
|
| - try:
|
| - results_dict = {}
|
| - for (slavename, results) in decoded_dict.iteritems():
|
| - results_dict[slavename] = BaseCommandResults.decode(results)
|
| - return MultiCommandResults(results_dict)
|
| - except Exception:
|
| - errors.append(traceback.format_exc())
|
| - raise Exception('Unable to decode CommandResults from dict:\n\n%s\n%s'
|
| - % ('\n'.join(errors), decoded_dict))
|
| -
|
| - def print_results(self, pretty=False):
|
| - """Print the results of a command.
|
| -
|
| - Args:
|
| - pretty: bool; whether or not to print in human-readable format.
|
| - """
|
| - if pretty:
|
| - print pprint.pformat(self.__dict__)
|
| - else:
|
| - print self.encode()
|
| -
|
| -
|
| -class SingleCommandResults(collections.namedtuple('CommandResults_tuple',
|
| - 'stdout, stderr, returncode'),
|
| - BaseCommandResults):
|
| - """Results for a single command. Properties: stdout, stderr, and returncode"""
|
| -
|
| - def encode(self):
|
| - """Convert the results into a machine-readable string.
|
| -
|
| - Returns:
|
| - A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
|
| - """
|
| - return (BaseCommandResults.BOOKEND_STR +
|
| - pickle.dumps(self.__dict__).encode('hex') +
|
| - BaseCommandResults.BOOKEND_STR)
|
| -
|
| - @staticmethod
|
| - def make(stdout='', stderr='', returncode=1):
|
| - """Create CommandResults for a command.
|
| -
|
| - Args:
|
| - stdout: string; stdout from a command.
|
| - stderr: string; stderr from a command.
|
| - returncode: string; return code of a command.
|
| - """
|
| - return SingleCommandResults(stdout=stdout,
|
| - stderr=stderr,
|
| - returncode=returncode)
|
| -
|
| - @property
|
| - def __dict__(self):
|
| - """Return a dictionary representation of this CommandResults instance.
|
| -
|
| - Since collections.NamedTuple.__dict__ returns an OrderedDict, we have to
|
| - create this wrapper to get a normal dict.
|
| - """
|
| - return dict(self._asdict())
|
| -
|
| -
|
| -class MultiCommandResults(BaseCommandResults):
|
| - """Encapsulates CommandResults for multiple buildslaves or hosts.
|
| -
|
| - MultiCommandResults can form tree structures whose leaves are instances of
|
| - SingleComamandResults and interior nodes are instances of MultiCommandResults:
|
| -
|
| - MultiCommandResults({
|
| - 'remote_slave_host_name': MultiCommandResults({
|
| - 'slave_name': SingleCommandResults,
|
| - 'slave_name2': SingleCommandResults,
|
| - }),
|
| - 'local_slave_name': SingleCommandResults,
|
| - })
|
| - """
|
| -
|
| - def __init__(self, results):
|
| - """Instantiate the MultiCommandResults.
|
| -
|
| - Args:
|
| - results: dict whose keys are slavenames or slave host names and values
|
| - are instances of a BaseCommandResults subclass.
|
| - """
|
| - super(MultiCommandResults, self).__init__()
|
| - self._dict = {}
|
| - for (slavename, result) in results.iteritems():
|
| - if not issubclass(result.__class__, BaseCommandResults):
|
| - raise ValueError('%s is not a subclass of BaseCommandResults.'
|
| - % result.__class__)
|
| - self._dict[slavename] = result
|
| -
|
| - def __getitem__(self, key):
|
| - return self._dict[key]
|
| -
|
| - def __iter__(self):
|
| - return self._dict.__iter__()
|
| -
|
| - def __len__(self):
|
| - return self._dict.__len__()
|
| -
|
| - def iteritems(self):
|
| - return self._dict.iteritems()
|
| -
|
| - def iterkeys(self):
|
| - return self._dict.iterkeys()
|
| -
|
| - def encode(self):
|
| - """Convert the results into a machine-readable string.
|
| -
|
| - Returns:
|
| - A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
|
| - """
|
| - encoded_dict = dict([(key, value.encode())
|
| - for (key, value) in self._dict.iteritems()])
|
| - return (BaseCommandResults.BOOKEND_STR +
|
| - pickle.dumps(encoded_dict).encode('hex') +
|
| - BaseCommandResults.BOOKEND_STR)
|
| -
|
| - @property
|
| - def __dict__(self):
|
| - return dict([(key, value.__dict__)
|
| - for (key, value) in self._dict.iteritems()])
|
| -
|
| -
|
| -class ResolvableCommandElement(object):
|
| - """Base class for elements of commands which have different string values
|
| - depending on the properties of the host."""
|
| -
|
| - def resolve(self, slave_host_name):
|
| - """Resolve this ResolvableCommandElement as appropriate.
|
| -
|
| - Args:
|
| - slave_host_name: string; name of the slave host.
|
| - Returns:
|
| - string whose value depends on the given slave_host_name in some way.
|
| - """
|
| - raise NotImplementedError
|
| -
|
| -
|
| -class ResolvablePath(ResolvableCommandElement):
|
| - """Represents a path."""
|
| -
|
| - def __init__(self, *path_elems):
|
| - """Instantiate this ResolvablePath.
|
| -
|
| - Args:
|
| - path_elems: strings or ResolvableCommandElements which will be joined to
|
| - form a path.
|
| - """
|
| - super(ResolvablePath, self).__init__()
|
| - self._path_elems = list(path_elems)
|
| -
|
| - def resolve(self, slave_host_name):
|
| - """Resolve this ResolvablePath as appropriate.
|
| -
|
| - Args:
|
| - slave_host_name: string; name of the slave host.
|
| - Returns:
|
| - string whose value depends on the given slave_host_name in some way.
|
| - """
|
| - host_data = slave_hosts_cfg.get_slave_host_config(slave_host_name)
|
| - fixed_path_elems = _fixup_cmd(self._path_elems, slave_host_name)
|
| - return host_data.path_module.join(*fixed_path_elems)
|
| -
|
| -
|
| -def _fixup_cmd(cmd, slave_host_name):
|
| - """Resolve the command into a list of strings.
|
| -
|
| - Args:
|
| - cmd: list containing strings or ResolvableCommandElements.
|
| - slave_host_name: string; the name of the relevant slave host machine.
|
| - """
|
| - new_cmd = []
|
| - for elem in cmd:
|
| - if isinstance(elem, ResolvableCommandElement):
|
| - resolved_elem = elem.resolve(slave_host_name)
|
| - new_cmd.append(resolved_elem)
|
| - else:
|
| - new_cmd.append(elem)
|
| - return new_cmd
|
| -
|
| -
|
| -def _launch_cmd(cmd, cwd=None):
|
| - """Launch the given command. Non-blocking.
|
| -
|
| - Args:
|
| - cmd: list of strings; command to run.
|
| - cwd: working directory in which to run the process. Defaults to the root
|
| - of the buildbot checkout containing this file.
|
| - Returns:
|
| - subprocess.Popen instance.
|
| - """
|
| - if not cwd:
|
| - cwd = buildbot_path
|
| - return subprocess.Popen(cmd, shell=False, cwd=cwd, 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:
|
| - CommandResults instance, decoded from the results of the process.
|
| - """
|
| - stdout, stderr = popen.communicate()
|
| - try:
|
| - return BaseCommandResults.decode(stdout)
|
| - except Exception:
|
| - pass
|
| - return SingleCommandResults.make(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:
|
| - CommandResults instance, decoded from the results of the command.
|
| - """
|
| - try:
|
| - proc = _launch_cmd(cmd)
|
| - except OSError as e:
|
| - return SingleCommandResults.make(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:
|
| - MultiCommandResults instance containing the results of the command on each
|
| - of the local slaves.
|
| - """
|
| - slave_host = slave_hosts_cfg.get_slave_host_config(socket.gethostname())
|
| - slaves = slave_host.slaves
|
| - results = {}
|
| - procs = []
|
| - for (slave, _) in slaves:
|
| - try:
|
| - proc = _launch_cmd(cmd, cwd=os.path.join(buildbot_path, slave,
|
| - 'buildbot'))
|
| - procs.append((slave, proc))
|
| - except OSError as e:
|
| - results[slave] = SingleCommandResults.make(stderr=str(e))
|
| -
|
| - for slavename, proc in procs:
|
| - results[slavename] = _get_result(proc)
|
| -
|
| - return MultiCommandResults(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.
|
| - """
|
| - host = slave_hosts_cfg.SLAVE_HOSTS[slave_host_name]
|
| - login_cmd = host.login_cmd
|
| - if not login_cmd:
|
| - raise ValueError('%s does not have a remote login procedure defined in '
|
| - 'slave_hosts_cfg.py.' % 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] +
|
| - _fixup_cmd(cmd, slave_host_name))
|
| -
|
| -
|
| -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 or ResolvableCommandElements; the command to run.
|
| - Returns:
|
| - CommandResults instance containing the results of the command.
|
| - """
|
| - proc = _launch_on_remote_host(slave_host_name, cmd)
|
| - return _get_result(proc)
|
| -
|
| -
|
| -def _get_remote_slaves_cmd(cmd):
|
| - """Build a command which runs the command on all slaves on a remote host.
|
| -
|
| - Args:
|
| - cmd: list of strings or ResolvableCommandElements; the command to run.
|
| - Returns:
|
| - list of strings or ResolvableCommandElements; a command which results in
|
| - the given command being run on all of the slaves on the remote host.
|
| - """
|
| - return ['python', ResolvablePath('scripts', 'run_on_local_slaves.py')] + cmd
|
| -
|
| -
|
| -def run_on_remote_slaves(slave_host_name, cmd):
|
| - """Run a command on each buildslave on a remote slave host machine, blocking
|
| - until completion.
|
| -
|
| - Args:
|
| - slave_host_name: string; name of the slave host machine.
|
| - cmd: list of strings or ResolvableCommandElements; the command to run.
|
| - Returns:
|
| - MultiCommandResults instance with results from each slave on the remote
|
| - host.
|
| - """
|
| - proc = _launch_on_remote_host(slave_host_name, _get_remote_slaves_cmd(cmd))
|
| - return _get_result(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 or ResolvableCommandElements; the command to run.
|
| - Returns:
|
| - MultiCommandResults instance with results from each remote slave host.
|
| - """
|
| - results = {}
|
| - procs = []
|
| -
|
| - for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys():
|
| - if not slave_hosts_cfg.SLAVE_HOSTS[hostname].remote_access:
|
| - continue
|
| - if not slave_hosts_cfg.SLAVE_HOSTS[hostname].login_cmd:
|
| - results.update({
|
| - hostname: SingleCommandResults.make(stderr='No procedure for login.'),
|
| - })
|
| - else:
|
| - procs.append((hostname, _launch_on_remote_host(hostname, cmd)))
|
| -
|
| - for slavename, proc in procs:
|
| - results[slavename] = _get_result(proc)
|
| -
|
| - return MultiCommandResults(results)
|
| -
|
| -
|
| -def run_on_all_slaves_on_all_hosts(cmd):
|
| - """Run the given command on all slaves on all hosts. Blocks until completion.
|
| -
|
| - Args:
|
| - cmd: list of strings or ResolvableCommandElements; the command to run.
|
| - Returns:
|
| - MultiCommandResults instance with results from each slave on each remote
|
| - slave host.
|
| - """
|
| - return run_on_all_slave_hosts(_get_remote_slaves_cmd(cmd))
|
| -
|
| -
|
| -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()
|
| - run(parsed_args.cmd).print_results(pretty=parsed_args.pretty)
|
|
|