Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(471)

Unified Diff: scripts/run_cmd.py

Issue 196423010: run_cmd: Fix CommandResults for results from multiple sources (Closed) Base URL: https://skia.googlesource.com/buildbot.git@master
Patch Set: Add traceback Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | scripts/run_on_all_slaves_on_all_hosts.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: scripts/run_cmd.py
diff --git a/scripts/run_cmd.py b/scripts/run_cmd.py
index 4a5faa2e72dd309e29fc83163f4b9b194f172f5d..cd749f66482c9d7336b93c0508d1cda7db48c210 100755
--- a/scripts/run_cmd.py
+++ b/scripts/run_cmd.py
@@ -15,6 +15,7 @@ import pprint
import socket
import subprocess
import sys
+import traceback
buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
os.pardir))
@@ -23,8 +24,8 @@ sys.path.append(os.path.join(buildbot_path))
from site_config import slave_hosts_cfg
-class CommandResults(collections.namedtuple('CommandResults',
- 'stdout, stderr, returncode')):
+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.
@@ -36,9 +37,7 @@ class CommandResults(collections.namedtuple('CommandResults',
Returns:
A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
"""
- return (CommandResults.BOOKEND_STR +
- pickle.dumps(self.__dict__).encode('hex') +
- CommandResults.BOOKEND_STR)
+ raise NotImplementedError()
@staticmethod
def decode(results_str):
@@ -49,8 +48,53 @@ class CommandResults(collections.namedtuple('CommandResults',
Returns:
A dictionary of results.
"""
- return CommandResults(**pickle.loads(
- results_str.split(CommandResults.BOOKEND_STR)[1].decode('hex')))
+ 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):
@@ -61,9 +105,9 @@ class CommandResults(collections.namedtuple('CommandResults',
stderr: string; stderr from a command.
returncode: string; return code of a command.
"""
- return CommandResults(stdout=stdout,
- stderr=stderr,
- returncode=returncode)
+ return SingleCommandResults(stdout=stdout,
+ stderr=stderr,
+ returncode=returncode)
@property
def __dict__(self):
@@ -74,16 +118,56 @@ class CommandResults(collections.namedtuple('CommandResults',
"""
return dict(self._asdict())
- def print_results(self, pretty=False):
- """Print the results of a command.
+
+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:
- pretty: bool; whether or not to print in human-readable format.
+ results: dict whose keys are slavenames or slave host names and values
+ are instances of a BaseCommandResults subclass.
"""
- if pretty:
- print pprint.pformat(self.__dict__)
- else:
- print repr(self.encode())
+ 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 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):
@@ -182,12 +266,16 @@ def _get_result(popen):
Args:
popen: subprocess.Popen instance.
Returns:
- A dictionary with stdout, stderr, and returncode as keys.
+ CommandResults instance, decoded from the results of the process.
"""
stdout, stderr = popen.communicate()
- return CommandResults.make(stdout=stdout,
- stderr=stderr,
- returncode=popen.returncode)
+ try:
+ return BaseCommandResults.decode(stdout)
+ except Exception:
+ pass
+ return SingleCommandResults.make(stdout=stdout,
+ stderr=stderr,
+ returncode=popen.returncode)
def run(cmd):
@@ -196,12 +284,12 @@ def run(cmd):
Args:
cmd: string or list of strings; the command to run.
Returns:
- A dictionary with stdout, stderr, and returncode as keys.
+ CommandResults instance, decoded from the results of the command.
"""
try:
proc = _launch_cmd(cmd)
except OSError as e:
- return CommandResults.make(stderr=str(e))
+ return SingleCommandResults.make(stderr=str(e))
return _get_result(proc)
@@ -211,9 +299,8 @@ def run_on_local_slaves(cmd):
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.
+ 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
@@ -221,12 +308,15 @@ def run_on_local_slaves(cmd):
procs = []
for (slave, _) in slaves:
os.chdir(os.path.join(buildbot_path, slave, 'buildbot'))
- procs.append((slave, _launch_cmd(cmd)))
+ try:
+ procs.append((slave, _launch_cmd(cmd)))
+ except OSError as e:
+ results[slave] = SingleCommandResults.make(stderr=str(e))
for slavename, proc in procs:
results[slavename] = _get_result(proc)
- return results
+ return MultiCommandResults(results)
def _launch_on_remote_host(slave_host_name, cmd):
@@ -250,66 +340,83 @@ def _launch_on_remote_host(slave_host_name, cmd):
_fixup_cmd(cmd, slave_host_name))
-def _get_remote_host_results(slave_host_name, popen):
- """Get the results from a running process. Blocks until the process completes.
+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 remote host.
- popen: subprocess.Popen instance.
+ slave_host_name: string; name of the slave host machine.
+ cmd: list of strings or ResolvableCommandElements; the 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.
+ CommandResults instance containing the results of the command.
"""
- result = _get_result(popen)
- if result.returncode:
- return { slave_host_name: result }
- try:
- return { slave_host_name: CommandResults.decode(result.stdout) }
- except (pickle.UnpicklingError, IndexError):
- error_msg = 'Could not decode result: %s' % result.stdout
- return { slave_host_name: CommandResults.make(stderr=error_msg) }
+ proc = _launch_on_remote_host(slave_host_name, cmd)
+ return _get_result(proc)
-def run_on_remote_host(slave_host_name, cmd):
- """Run a command on a remote slave host machine, blocking until completion.
+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.buildbot_path('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; command to run.
+ cmd: list of strings or ResolvableCommandElements; the 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.
+ MultiCommandResults instance with results from each slave on the remote
+ host.
"""
- proc = _launch_on_remote_host(slave_host_name, cmd)
- return _get_remote_host_results(slave_host_name, proc)
+ 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; command to run.
+ cmd: list of strings or ResolvableCommandElements; the 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.
+ 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].login_cmd:
- results.update(
- {hostname: CommandResults.make(stderr='No procedure for login.')})
+ 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.update(_get_remote_host_results(slavename, proc))
+ results[slavename] = _get_result(proc)
+
+ return MultiCommandResults(results)
+
- return 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):
« no previous file with comments | « no previous file | scripts/run_on_all_slaves_on_all_hosts.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698