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