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

Side by Side 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 unified diff | 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2014 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 6
7 """Run a command and report its results in machine-readable format.""" 7 """Run a command and report its results in machine-readable format."""
8 8
9 9
10 import collections 10 import collections
11 import optparse 11 import optparse
12 import os 12 import os
13 import pickle 13 import pickle
14 import pprint 14 import pprint
15 import socket 15 import socket
16 import subprocess 16 import subprocess
17 import sys 17 import sys
18 import traceback
18 19
19 buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 20 buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
20 os.pardir)) 21 os.pardir))
21 sys.path.append(os.path.join(buildbot_path)) 22 sys.path.append(os.path.join(buildbot_path))
22 23
23 from site_config import slave_hosts_cfg 24 from site_config import slave_hosts_cfg
24 25
25 26
26 class CommandResults(collections.namedtuple('CommandResults', 27 class BaseCommandResults(object):
27 'stdout, stderr, returncode')): 28 """Base class for CommandResults classes."""
28 29
29 # We print this string before and after the important output from the command. 30 # We print this string before and after the important output from the command.
30 # This makes it easy to ignore output from SSH, shells, etc. 31 # This makes it easy to ignore output from SSH, shells, etc.
31 BOOKEND_STR = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@' 32 BOOKEND_STR = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
32 33
33 def encode(self): 34 def encode(self):
34 """Convert the results into a machine-readable string. 35 """Convert the results into a machine-readable string.
35 36
36 Returns: 37 Returns:
37 A hex-encoded string, bookended by BOOKEND_STR for easy parsing. 38 A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
38 """ 39 """
39 return (CommandResults.BOOKEND_STR + 40 raise NotImplementedError()
40 pickle.dumps(self.__dict__).encode('hex') +
41 CommandResults.BOOKEND_STR)
42 41
43 @staticmethod 42 @staticmethod
44 def decode(results_str): 43 def decode(results_str):
45 """Convert a machine-readable string into a CommandResults instance. 44 """Convert a machine-readable string into a CommandResults instance.
46 45
47 Args: 46 Args:
48 results_str: string; output from "run" or one of its siblings. 47 results_str: string; output from "run" or one of its siblings.
49 Returns: 48 Returns:
50 A dictionary of results. 49 A dictionary of results.
51 """ 50 """
52 return CommandResults(**pickle.loads( 51 decoded_dict = pickle.loads(
53 results_str.split(CommandResults.BOOKEND_STR)[1].decode('hex'))) 52 results_str.split(BaseCommandResults.BOOKEND_STR)[1].decode('hex'))
53 errors = []
54 # First, try to interpret the dict as SingleCommandResults.
55 try:
56 # This will fail unless decoded_dict has the following set of keys:
57 # ('returncode', 'stdout', 'stderr')
58 return SingleCommandResults(**decoded_dict)
59 except TypeError:
60 errors.append(traceback.format_exc())
61 # Next, try to interpret the dict as MultiCommandResults.
62 try:
63 results_dict = {}
64 for (slavename, results) in decoded_dict.iteritems():
65 results_dict[slavename] = BaseCommandResults.decode(results)
66 return MultiCommandResults(results_dict)
67 except Exception:
68 errors.append(traceback.format_exc())
69 raise Exception('Unable to decode CommandResults from dict:\n\n%s\n%s'
70 % ('\n'.join(errors), decoded_dict))
71
72 def print_results(self, pretty=False):
73 """Print the results of a command.
74
75 Args:
76 pretty: bool; whether or not to print in human-readable format.
77 """
78 if pretty:
79 print pprint.pformat(self.__dict__)
80 else:
81 print self.encode()
82
83
84 class SingleCommandResults(collections.namedtuple('CommandResults_tuple',
85 'stdout, stderr, returncode'),
86 BaseCommandResults):
87 """Results for a single command. Properties: stdout, stderr, and returncode"""
88
89 def encode(self):
90 """Convert the results into a machine-readable string.
91
92 Returns:
93 A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
94 """
95 return (BaseCommandResults.BOOKEND_STR +
96 pickle.dumps(self.__dict__).encode('hex') +
97 BaseCommandResults.BOOKEND_STR)
54 98
55 @staticmethod 99 @staticmethod
56 def make(stdout='', stderr='', returncode=1): 100 def make(stdout='', stderr='', returncode=1):
57 """Create CommandResults for a command. 101 """Create CommandResults for a command.
58 102
59 Args: 103 Args:
60 stdout: string; stdout from a command. 104 stdout: string; stdout from a command.
61 stderr: string; stderr from a command. 105 stderr: string; stderr from a command.
62 returncode: string; return code of a command. 106 returncode: string; return code of a command.
63 """ 107 """
64 return CommandResults(stdout=stdout, 108 return SingleCommandResults(stdout=stdout,
65 stderr=stderr, 109 stderr=stderr,
66 returncode=returncode) 110 returncode=returncode)
67 111
68 @property 112 @property
69 def __dict__(self): 113 def __dict__(self):
70 """Return a dictionary representation of this CommandResults instance. 114 """Return a dictionary representation of this CommandResults instance.
71 115
72 Since collections.NamedTuple.__dict__ returns an OrderedDict, we have to 116 Since collections.NamedTuple.__dict__ returns an OrderedDict, we have to
73 create this wrapper to get a normal dict. 117 create this wrapper to get a normal dict.
74 """ 118 """
75 return dict(self._asdict()) 119 return dict(self._asdict())
76 120
77 def print_results(self, pretty=False): 121
78 """Print the results of a command. 122 class MultiCommandResults(BaseCommandResults):
123 """Encapsulates CommandResults for multiple buildslaves or hosts.
124
125 MultiCommandResults can form tree structures whose leaves are instances of
126 SingleComamandResults and interior nodes are instances of MultiCommandResults:
127
128 MultiCommandResults({
129 'remote_slave_host_name': MultiCommandResults({
130 'slave_name': SingleCommandResults,
131 'slave_name2': SingleCommandResults,
132 }),
133 'local_slave_name': SingleCommandResults,
134 })
135 """
136
137 def __init__(self, results):
138 """Instantiate the MultiCommandResults.
79 139
80 Args: 140 Args:
81 pretty: bool; whether or not to print in human-readable format. 141 results: dict whose keys are slavenames or slave host names and values
142 are instances of a BaseCommandResults subclass.
82 """ 143 """
83 if pretty: 144 super(MultiCommandResults, self).__init__()
84 print pprint.pformat(self.__dict__) 145 self._dict = {}
85 else: 146 for (slavename, result) in results.iteritems():
86 print repr(self.encode()) 147 if not issubclass(result.__class__, BaseCommandResults):
148 raise ValueError('%s is not a subclass of BaseCommandResults.'
149 % result.__class__)
150 self._dict[slavename] = result
151
152 def __getitem__(self, key):
153 return self._dict[key]
154
155 def encode(self):
156 """Convert the results into a machine-readable string.
157
158 Returns:
159 A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
160 """
161 encoded_dict = dict([(key, value.encode())
162 for (key, value) in self._dict.iteritems()])
163 return (BaseCommandResults.BOOKEND_STR +
164 pickle.dumps(encoded_dict).encode('hex') +
165 BaseCommandResults.BOOKEND_STR)
166
167 @property
168 def __dict__(self):
169 return dict([(key, value.__dict__)
170 for (key, value) in self._dict.iteritems()])
87 171
88 172
89 class ResolvableCommandElement(object): 173 class ResolvableCommandElement(object):
90 """Base class for elements of commands which have different string values 174 """Base class for elements of commands which have different string values
91 depending on the properties of the host.""" 175 depending on the properties of the host."""
92 176
93 def resolve(self, slave_host_name): 177 def resolve(self, slave_host_name):
94 """Resolve this ResolvableCommandElement as appropriate. 178 """Resolve this ResolvableCommandElement as appropriate.
95 179
96 Args: 180 Args:
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
175 return subprocess.Popen(cmd, shell=False, stderr=subprocess.PIPE, 259 return subprocess.Popen(cmd, shell=False, stderr=subprocess.PIPE,
176 stdout=subprocess.PIPE) 260 stdout=subprocess.PIPE)
177 261
178 262
179 def _get_result(popen): 263 def _get_result(popen):
180 """Get the results from a running process. Blocks until the process completes. 264 """Get the results from a running process. Blocks until the process completes.
181 265
182 Args: 266 Args:
183 popen: subprocess.Popen instance. 267 popen: subprocess.Popen instance.
184 Returns: 268 Returns:
185 A dictionary with stdout, stderr, and returncode as keys. 269 CommandResults instance, decoded from the results of the process.
186 """ 270 """
187 stdout, stderr = popen.communicate() 271 stdout, stderr = popen.communicate()
188 return CommandResults.make(stdout=stdout, 272 try:
189 stderr=stderr, 273 return BaseCommandResults.decode(stdout)
190 returncode=popen.returncode) 274 except Exception:
275 pass
276 return SingleCommandResults.make(stdout=stdout,
277 stderr=stderr,
278 returncode=popen.returncode)
191 279
192 280
193 def run(cmd): 281 def run(cmd):
194 """Run the command, block until it completes, and return a results dictionary. 282 """Run the command, block until it completes, and return a results dictionary.
195 283
196 Args: 284 Args:
197 cmd: string or list of strings; the command to run. 285 cmd: string or list of strings; the command to run.
198 Returns: 286 Returns:
199 A dictionary with stdout, stderr, and returncode as keys. 287 CommandResults instance, decoded from the results of the command.
200 """ 288 """
201 try: 289 try:
202 proc = _launch_cmd(cmd) 290 proc = _launch_cmd(cmd)
203 except OSError as e: 291 except OSError as e:
204 return CommandResults.make(stderr=str(e)) 292 return SingleCommandResults.make(stderr=str(e))
205 return _get_result(proc) 293 return _get_result(proc)
206 294
207 295
208 def run_on_local_slaves(cmd): 296 def run_on_local_slaves(cmd):
209 """Run the command on each local buildslave, blocking until completion. 297 """Run the command on each local buildslave, blocking until completion.
210 298
211 Args: 299 Args:
212 cmd: list of strings; the command to run. 300 cmd: list of strings; the command to run.
213 Returns: 301 Returns:
214 A dictionary of results with buildslave names as keys and individual 302 MultiCommandResults instance containing the results of the command on each
215 result dictionaries (with stdout, stderr, and returncode as keys) as 303 of the local slaves.
216 values.
217 """ 304 """
218 slave_host = slave_hosts_cfg.get_slave_host_config(socket.gethostname()) 305 slave_host = slave_hosts_cfg.get_slave_host_config(socket.gethostname())
219 slaves = slave_host.slaves 306 slaves = slave_host.slaves
220 results = {} 307 results = {}
221 procs = [] 308 procs = []
222 for (slave, _) in slaves: 309 for (slave, _) in slaves:
223 os.chdir(os.path.join(buildbot_path, slave, 'buildbot')) 310 os.chdir(os.path.join(buildbot_path, slave, 'buildbot'))
224 procs.append((slave, _launch_cmd(cmd))) 311 try:
312 procs.append((slave, _launch_cmd(cmd)))
313 except OSError as e:
314 results[slave] = SingleCommandResults.make(stderr=str(e))
225 315
226 for slavename, proc in procs: 316 for slavename, proc in procs:
227 results[slavename] = _get_result(proc) 317 results[slavename] = _get_result(proc)
228 318
229 return results 319 return MultiCommandResults(results)
230 320
231 321
232 def _launch_on_remote_host(slave_host_name, cmd): 322 def _launch_on_remote_host(slave_host_name, cmd):
233 """Launch the command on a remote slave host machine. Non-blocking. 323 """Launch the command on a remote slave host machine. Non-blocking.
234 324
235 Args: 325 Args:
236 slave_host_name: string; name of the slave host machine. 326 slave_host_name: string; name of the slave host machine.
237 cmd: list of strings; command to run. 327 cmd: list of strings; command to run.
238 Returns: 328 Returns:
239 subprocess.Popen instance. 329 subprocess.Popen instance.
240 """ 330 """
241 host = slave_hosts_cfg.SLAVE_HOSTS[slave_host_name] 331 host = slave_hosts_cfg.SLAVE_HOSTS[slave_host_name]
242 login_cmd = host.login_cmd 332 login_cmd = host.login_cmd
243 if not login_cmd: 333 if not login_cmd:
244 raise ValueError('%s does not have a remote login procedure defined in ' 334 raise ValueError('%s does not have a remote login procedure defined in '
245 'slave_hosts_cfg.py.' % slave_host_name) 335 'slave_hosts_cfg.py.' % slave_host_name)
246 path_to_buildbot = host.path_module.join(*host.path_to_buildbot) 336 path_to_buildbot = host.path_module.join(*host.path_to_buildbot)
247 path_to_run_cmd = host.path_module.join(path_to_buildbot, 'scripts', 337 path_to_run_cmd = host.path_module.join(path_to_buildbot, 'scripts',
248 'run_cmd.py') 338 'run_cmd.py')
249 return _launch_cmd(login_cmd + ['python', path_to_run_cmd] + 339 return _launch_cmd(login_cmd + ['python', path_to_run_cmd] +
250 _fixup_cmd(cmd, slave_host_name)) 340 _fixup_cmd(cmd, slave_host_name))
251 341
252 342
253 def _get_remote_host_results(slave_host_name, popen):
254 """Get the results from a running process. Blocks until the process completes.
255
256 Args:
257 slave_host_name: string; name of the remote host.
258 popen: subprocess.Popen instance.
259 Returns:
260 A dictionary of results with the remote host machine name as its only key
261 and individual result dictionaries (with stdout, stderr, and returncode as
262 keys) its value.
263 """
264 result = _get_result(popen)
265 if result.returncode:
266 return { slave_host_name: result }
267 try:
268 return { slave_host_name: CommandResults.decode(result.stdout) }
269 except (pickle.UnpicklingError, IndexError):
270 error_msg = 'Could not decode result: %s' % result.stdout
271 return { slave_host_name: CommandResults.make(stderr=error_msg) }
272
273
274 def run_on_remote_host(slave_host_name, cmd): 343 def run_on_remote_host(slave_host_name, cmd):
275 """Run a command on a remote slave host machine, blocking until completion. 344 """Run a command on a remote slave host machine, blocking until completion.
276 345
277 Args: 346 Args:
278 slave_host_name: string; name of the slave host machine. 347 slave_host_name: string; name of the slave host machine.
279 cmd: list of strings; command to run. 348 cmd: list of strings or ResolvableCommandElements; the command to run.
280 Returns: 349 Returns:
281 A dictionary of results with the remote host machine name as its only key 350 CommandResults instance containing the results of the command.
282 and individual result dictionaries (with stdout, stderr, and returncode as
283 keys) its value.
284 """ 351 """
285 proc = _launch_on_remote_host(slave_host_name, cmd) 352 proc = _launch_on_remote_host(slave_host_name, cmd)
286 return _get_remote_host_results(slave_host_name, proc) 353 return _get_result(proc)
354
355
356 def _get_remote_slaves_cmd(cmd):
357 """Build a command which runs the command on all slaves on a remote host.
358
359 Args:
360 cmd: list of strings or ResolvableCommandElements; the command to run.
361 Returns:
362 list of strings or ResolvableCommandElements; a command which results in
363 the given command being run on all of the slaves on the remote host.
364 """
365 return ['python',
366 ResolvablePath.buildbot_path('scripts',
367 'run_on_local_slaves.py')] + cmd
368
369
370 def run_on_remote_slaves(slave_host_name, cmd):
371 """Run a command on each buildslave on a remote slave host machine, blocking
372 until completion.
373
374 Args:
375 slave_host_name: string; name of the slave host machine.
376 cmd: list of strings or ResolvableCommandElements; the command to run.
377 Returns:
378 MultiCommandResults instance with results from each slave on the remote
379 host.
380 """
381 proc = _launch_on_remote_host(slave_host_name, _get_remote_slaves_cmd(cmd))
382 return _get_result(proc)
287 383
288 384
289 def run_on_all_slave_hosts(cmd): 385 def run_on_all_slave_hosts(cmd):
290 """Run the given command on all slave hosts, blocking until all complete. 386 """Run the given command on all slave hosts, blocking until all complete.
291 387
292 Args: 388 Args:
293 cmd: list of strings; command to run. 389 cmd: list of strings or ResolvableCommandElements; the command to run.
294 Returns: 390 Returns:
295 A dictionary of results with host machine names as keys and individual 391 MultiCommandResults instance with results from each remote slave host.
296 result dictionaries (with stdout, stderr, and returncode as keys) as
297 values.
298 """ 392 """
299 results = {} 393 results = {}
300 procs = [] 394 procs = []
301 395
302 for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys(): 396 for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys():
303 if not slave_hosts_cfg.SLAVE_HOSTS[hostname].login_cmd: 397 if not slave_hosts_cfg.SLAVE_HOSTS[hostname].login_cmd:
304 results.update( 398 results.update({
305 {hostname: CommandResults.make(stderr='No procedure for login.')}) 399 hostname: SingleCommandResults.make(stderr='No procedure for login.'),
400 })
306 else: 401 else:
307 procs.append((hostname, _launch_on_remote_host(hostname, cmd))) 402 procs.append((hostname, _launch_on_remote_host(hostname, cmd)))
308 403
309 for slavename, proc in procs: 404 for slavename, proc in procs:
310 results.update(_get_remote_host_results(slavename, proc)) 405 results[slavename] = _get_result(proc)
311 406
312 return results 407 return MultiCommandResults(results)
408
409
410 def run_on_all_slaves_on_all_hosts(cmd):
411 """Run the given command on all slaves on all hosts. Blocks until completion.
412
413 Args:
414 cmd: list of strings or ResolvableCommandElements; the command to run.
415 Returns:
416 MultiCommandResults instance with results from each slave on each remote
417 slave host.
418 """
419 return run_on_all_slave_hosts(_get_remote_slaves_cmd(cmd))
313 420
314 421
315 def parse_args(positional_args=None): 422 def parse_args(positional_args=None):
316 """Common argument parser for scripts using this module. 423 """Common argument parser for scripts using this module.
317 424
318 Args: 425 Args:
319 positional_args: optional list of strings; extra positional arguments to 426 positional_args: optional list of strings; extra positional arguments to
320 the script. 427 the script.
321 """ 428 """
322 parser = optparse.OptionParser() 429 parser = optparse.OptionParser()
(...skipping 25 matching lines...) Expand all
348 setattr(options, cmd, args) 455 setattr(options, cmd, args)
349 except IndexError: 456 except IndexError:
350 parser.print_usage() 457 parser.print_usage()
351 sys.exit(1) 458 sys.exit(1)
352 return options 459 return options
353 460
354 461
355 if '__main__' == __name__: 462 if '__main__' == __name__:
356 parsed_args = parse_args() 463 parsed_args = parse_args()
357 run(parsed_args.cmd).print_results(pretty=parsed_args.pretty) 464 run(parsed_args.cmd).print_results(pretty=parsed_args.pretty)
OLDNEW
« 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