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

Side by Side Diff: scripts/run_cmd.py

Issue 648353002: Remove Skia's forked buildbot code (Closed) Base URL: https://skia.googlesource.com/buildbot.git@master
Patch Set: Address comment Created 6 years, 2 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 | « scripts/restart_masters.py ('k') | scripts/run_on_all_slave_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
(Empty)
1 #!/usr/bin/env python
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
4 # found in the LICENSE file.
5
6
7 """Run a command and report its results in machine-readable format."""
8
9
10 import collections
11 import optparse
12 import os
13 import pickle
14 import pprint
15 import socket
16 import subprocess
17 import sys
18 import traceback
19
20 buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
21 os.pardir))
22 sys.path.append(os.path.join(buildbot_path))
23
24 from site_config import slave_hosts_cfg
25
26
27 class BaseCommandResults(object):
28 """Base class for CommandResults classes."""
29
30 # We print this string before and after the important output from the command.
31 # This makes it easy to ignore output from SSH, shells, etc.
32 BOOKEND_STR = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
33
34 def encode(self):
35 """Convert the results into a machine-readable string.
36
37 Returns:
38 A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
39 """
40 raise NotImplementedError()
41
42 @staticmethod
43 def decode(results_str):
44 """Convert a machine-readable string into a CommandResults instance.
45
46 Args:
47 results_str: string; output from "run" or one of its siblings.
48 Returns:
49 A dictionary of results.
50 """
51 decoded_dict = pickle.loads(
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)
98
99 @staticmethod
100 def make(stdout='', stderr='', returncode=1):
101 """Create CommandResults for a command.
102
103 Args:
104 stdout: string; stdout from a command.
105 stderr: string; stderr from a command.
106 returncode: string; return code of a command.
107 """
108 return SingleCommandResults(stdout=stdout,
109 stderr=stderr,
110 returncode=returncode)
111
112 @property
113 def __dict__(self):
114 """Return a dictionary representation of this CommandResults instance.
115
116 Since collections.NamedTuple.__dict__ returns an OrderedDict, we have to
117 create this wrapper to get a normal dict.
118 """
119 return dict(self._asdict())
120
121
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.
139
140 Args:
141 results: dict whose keys are slavenames or slave host names and values
142 are instances of a BaseCommandResults subclass.
143 """
144 super(MultiCommandResults, self).__init__()
145 self._dict = {}
146 for (slavename, result) in results.iteritems():
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 __iter__(self):
156 return self._dict.__iter__()
157
158 def __len__(self):
159 return self._dict.__len__()
160
161 def iteritems(self):
162 return self._dict.iteritems()
163
164 def iterkeys(self):
165 return self._dict.iterkeys()
166
167 def encode(self):
168 """Convert the results into a machine-readable string.
169
170 Returns:
171 A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
172 """
173 encoded_dict = dict([(key, value.encode())
174 for (key, value) in self._dict.iteritems()])
175 return (BaseCommandResults.BOOKEND_STR +
176 pickle.dumps(encoded_dict).encode('hex') +
177 BaseCommandResults.BOOKEND_STR)
178
179 @property
180 def __dict__(self):
181 return dict([(key, value.__dict__)
182 for (key, value) in self._dict.iteritems()])
183
184
185 class ResolvableCommandElement(object):
186 """Base class for elements of commands which have different string values
187 depending on the properties of the host."""
188
189 def resolve(self, slave_host_name):
190 """Resolve this ResolvableCommandElement as appropriate.
191
192 Args:
193 slave_host_name: string; name of the slave host.
194 Returns:
195 string whose value depends on the given slave_host_name in some way.
196 """
197 raise NotImplementedError
198
199
200 class ResolvablePath(ResolvableCommandElement):
201 """Represents a path."""
202
203 def __init__(self, *path_elems):
204 """Instantiate this ResolvablePath.
205
206 Args:
207 path_elems: strings or ResolvableCommandElements which will be joined to
208 form a path.
209 """
210 super(ResolvablePath, self).__init__()
211 self._path_elems = list(path_elems)
212
213 def resolve(self, slave_host_name):
214 """Resolve this ResolvablePath as appropriate.
215
216 Args:
217 slave_host_name: string; name of the slave host.
218 Returns:
219 string whose value depends on the given slave_host_name in some way.
220 """
221 host_data = slave_hosts_cfg.get_slave_host_config(slave_host_name)
222 fixed_path_elems = _fixup_cmd(self._path_elems, slave_host_name)
223 return host_data.path_module.join(*fixed_path_elems)
224
225
226 def _fixup_cmd(cmd, slave_host_name):
227 """Resolve the command into a list of strings.
228
229 Args:
230 cmd: list containing strings or ResolvableCommandElements.
231 slave_host_name: string; the name of the relevant slave host machine.
232 """
233 new_cmd = []
234 for elem in cmd:
235 if isinstance(elem, ResolvableCommandElement):
236 resolved_elem = elem.resolve(slave_host_name)
237 new_cmd.append(resolved_elem)
238 else:
239 new_cmd.append(elem)
240 return new_cmd
241
242
243 def _launch_cmd(cmd, cwd=None):
244 """Launch the given command. Non-blocking.
245
246 Args:
247 cmd: list of strings; command to run.
248 cwd: working directory in which to run the process. Defaults to the root
249 of the buildbot checkout containing this file.
250 Returns:
251 subprocess.Popen instance.
252 """
253 if not cwd:
254 cwd = buildbot_path
255 return subprocess.Popen(cmd, shell=False, cwd=cwd, stderr=subprocess.PIPE,
256 stdout=subprocess.PIPE)
257
258
259 def _get_result(popen):
260 """Get the results from a running process. Blocks until the process completes.
261
262 Args:
263 popen: subprocess.Popen instance.
264 Returns:
265 CommandResults instance, decoded from the results of the process.
266 """
267 stdout, stderr = popen.communicate()
268 try:
269 return BaseCommandResults.decode(stdout)
270 except Exception:
271 pass
272 return SingleCommandResults.make(stdout=stdout,
273 stderr=stderr,
274 returncode=popen.returncode)
275
276
277 def run(cmd):
278 """Run the command, block until it completes, and return a results dictionary.
279
280 Args:
281 cmd: string or list of strings; the command to run.
282 Returns:
283 CommandResults instance, decoded from the results of the command.
284 """
285 try:
286 proc = _launch_cmd(cmd)
287 except OSError as e:
288 return SingleCommandResults.make(stderr=str(e))
289 return _get_result(proc)
290
291
292 def run_on_local_slaves(cmd):
293 """Run the command on each local buildslave, blocking until completion.
294
295 Args:
296 cmd: list of strings; the command to run.
297 Returns:
298 MultiCommandResults instance containing the results of the command on each
299 of the local slaves.
300 """
301 slave_host = slave_hosts_cfg.get_slave_host_config(socket.gethostname())
302 slaves = slave_host.slaves
303 results = {}
304 procs = []
305 for (slave, _) in slaves:
306 try:
307 proc = _launch_cmd(cmd, cwd=os.path.join(buildbot_path, slave,
308 'buildbot'))
309 procs.append((slave, proc))
310 except OSError as e:
311 results[slave] = SingleCommandResults.make(stderr=str(e))
312
313 for slavename, proc in procs:
314 results[slavename] = _get_result(proc)
315
316 return MultiCommandResults(results)
317
318
319 def _launch_on_remote_host(slave_host_name, cmd):
320 """Launch the command on a remote slave host machine. Non-blocking.
321
322 Args:
323 slave_host_name: string; name of the slave host machine.
324 cmd: list of strings; command to run.
325 Returns:
326 subprocess.Popen instance.
327 """
328 host = slave_hosts_cfg.SLAVE_HOSTS[slave_host_name]
329 login_cmd = host.login_cmd
330 if not login_cmd:
331 raise ValueError('%s does not have a remote login procedure defined in '
332 'slave_hosts_cfg.py.' % slave_host_name)
333 path_to_buildbot = host.path_module.join(*host.path_to_buildbot)
334 path_to_run_cmd = host.path_module.join(path_to_buildbot, 'scripts',
335 'run_cmd.py')
336 return _launch_cmd(login_cmd + ['python', path_to_run_cmd] +
337 _fixup_cmd(cmd, slave_host_name))
338
339
340 def run_on_remote_host(slave_host_name, cmd):
341 """Run a command on a remote slave host machine, blocking until completion.
342
343 Args:
344 slave_host_name: string; name of the slave host machine.
345 cmd: list of strings or ResolvableCommandElements; the command to run.
346 Returns:
347 CommandResults instance containing the results of the command.
348 """
349 proc = _launch_on_remote_host(slave_host_name, cmd)
350 return _get_result(proc)
351
352
353 def _get_remote_slaves_cmd(cmd):
354 """Build a command which runs the command on all slaves on a remote host.
355
356 Args:
357 cmd: list of strings or ResolvableCommandElements; the command to run.
358 Returns:
359 list of strings or ResolvableCommandElements; a command which results in
360 the given command being run on all of the slaves on the remote host.
361 """
362 return ['python', ResolvablePath('scripts', 'run_on_local_slaves.py')] + cmd
363
364
365 def run_on_remote_slaves(slave_host_name, cmd):
366 """Run a command on each buildslave on a remote slave host machine, blocking
367 until completion.
368
369 Args:
370 slave_host_name: string; name of the slave host machine.
371 cmd: list of strings or ResolvableCommandElements; the command to run.
372 Returns:
373 MultiCommandResults instance with results from each slave on the remote
374 host.
375 """
376 proc = _launch_on_remote_host(slave_host_name, _get_remote_slaves_cmd(cmd))
377 return _get_result(proc)
378
379
380 def run_on_all_slave_hosts(cmd):
381 """Run the given command on all slave hosts, blocking until all complete.
382
383 Args:
384 cmd: list of strings or ResolvableCommandElements; the command to run.
385 Returns:
386 MultiCommandResults instance with results from each remote slave host.
387 """
388 results = {}
389 procs = []
390
391 for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys():
392 if not slave_hosts_cfg.SLAVE_HOSTS[hostname].remote_access:
393 continue
394 if not slave_hosts_cfg.SLAVE_HOSTS[hostname].login_cmd:
395 results.update({
396 hostname: SingleCommandResults.make(stderr='No procedure for login.'),
397 })
398 else:
399 procs.append((hostname, _launch_on_remote_host(hostname, cmd)))
400
401 for slavename, proc in procs:
402 results[slavename] = _get_result(proc)
403
404 return MultiCommandResults(results)
405
406
407 def run_on_all_slaves_on_all_hosts(cmd):
408 """Run the given command on all slaves on all hosts. Blocks until completion.
409
410 Args:
411 cmd: list of strings or ResolvableCommandElements; the command to run.
412 Returns:
413 MultiCommandResults instance with results from each slave on each remote
414 slave host.
415 """
416 return run_on_all_slave_hosts(_get_remote_slaves_cmd(cmd))
417
418
419 def parse_args(positional_args=None):
420 """Common argument parser for scripts using this module.
421
422 Args:
423 positional_args: optional list of strings; extra positional arguments to
424 the script.
425 """
426 parser = optparse.OptionParser()
427 parser.disable_interspersed_args()
428 parser.add_option('-p', '--pretty', action='store_true', dest='pretty',
429 help='Print output in a human-readable form.')
430
431 # Fixup the usage message to include the positional args.
432 cmd = 'cmd'
433 all_positional_args = (positional_args or []) + [cmd]
434 usage = parser.get_usage().rstrip()
435 for arg in all_positional_args:
436 usage += ' ' + arg
437 parser.set_usage(usage)
438
439 options, args = parser.parse_args()
440
441 # Set positional arguments.
442 for positional_arg in positional_args or []:
443 try:
444 setattr(options, positional_arg, args[0])
445 except IndexError:
446 parser.print_usage()
447 sys.exit(1)
448 args = args[1:]
449
450 # Everything else is part of the command to run.
451 try:
452 setattr(options, cmd, args)
453 except IndexError:
454 parser.print_usage()
455 sys.exit(1)
456 return options
457
458
459 if '__main__' == __name__:
460 parsed_args = parse_args()
461 run(parsed_args.cmd).print_results(pretty=parsed_args.pretty)
OLDNEW
« no previous file with comments | « scripts/restart_masters.py ('k') | scripts/run_on_all_slave_hosts.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698