Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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 Loading... | |
| 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) |
| OLD | NEW |