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

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