1#!/usr/bin/env python2 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: 7# 8# * Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# * Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following disclaimer 12# in the documentation and/or other materials provided with the 13# distribution. 14# * Neither the name of Google Inc. nor the names of its 15# contributors may be used to endorse or promote products derived from 16# this software without specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30# NOTE: This file is NOT under GPL. See above. 31"""Queries buildbot through the json interface. 32""" 33 34from __future__ import print_function 35 36__author__ = 'maruel@chromium.org' 37__version__ = '1.2' 38 39import code 40import datetime 41import functools 42import json 43 44# Pylint recommends we use "from chromite.lib import cros_logging as logging". 45# Chromite specific policy message, we want to keep using the standard logging. 46# pylint: disable=cros-logging-import 47import logging 48 49# pylint: disable=deprecated-module 50import optparse 51 52import time 53import urllib 54import urllib2 55import sys 56 57try: 58 from natsort import natsorted 59except ImportError: 60 # natsorted is a simple helper to sort "naturally", e.g. "vm40" is sorted 61 # after "vm7". Defaults to normal sorting. 62 natsorted = sorted 63 64# These values are buildbot constants used for Build and BuildStep. 65# This line was copied from master/buildbot/status/builder.py. 66SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6) 67 68## Generic node caching code. 69 70 71class Node(object): 72 """Root class for all nodes in the graph. 73 74 Provides base functionality for any node in the graph, independent if it has 75 children or not or if its content can be addressed through an url or needs to 76 be fetched as part of another node. 77 78 self.printable_attributes is only used for self documentation and for str() 79 implementation. 80 """ 81 printable_attributes = [] 82 83 def __init__(self, parent, url): 84 self.printable_attributes = self.printable_attributes[:] 85 if url: 86 self.printable_attributes.append('url') 87 url = url.rstrip('/') 88 if parent is not None: 89 self.printable_attributes.append('parent') 90 self.url = url 91 self.parent = parent 92 93 def __str__(self): 94 return self.to_string() 95 96 def __repr__(self): 97 """Embeds key if present.""" 98 key = getattr(self, 'key', None) 99 if key is not None: 100 return '<%s key=%s>' % (self.__class__.__name__, key) 101 cached_keys = getattr(self, 'cached_keys', None) 102 if cached_keys is not None: 103 return '<%s keys=%s>' % (self.__class__.__name__, cached_keys) 104 return super(Node, self).__repr__() 105 106 def to_string(self, maximum=100): 107 out = ['%s:' % self.__class__.__name__] 108 assert not 'printable_attributes' in self.printable_attributes 109 110 def limit(txt): 111 txt = str(txt) 112 if maximum > 0: 113 if len(txt) > maximum + 2: 114 txt = txt[:maximum] + '...' 115 return txt 116 117 for k in sorted(self.printable_attributes): 118 if k == 'parent': 119 # Avoid infinite recursion. 120 continue 121 out.append(limit(' %s: %r' % (k, getattr(self, k)))) 122 return '\n'.join(out) 123 124 def refresh(self): 125 """Refreshes the data.""" 126 self.discard() 127 return self.cache() 128 129 def cache(self): # pragma: no cover 130 """Caches the data.""" 131 raise NotImplementedError() 132 133 def discard(self): # pragma: no cover 134 """Discards cached data. 135 136 Pretty much everything is temporary except completed Build. 137 """ 138 raise NotImplementedError() 139 140 141class AddressableBaseDataNode(Node): # pylint: disable=W0223 142 """A node that contains a dictionary of data that can be fetched with an url. 143 144 The node is directly addressable. It also often can be fetched by the parent. 145 """ 146 printable_attributes = Node.printable_attributes + ['data'] 147 148 def __init__(self, parent, url, data): 149 super(AddressableBaseDataNode, self).__init__(parent, url) 150 self._data = data 151 152 @property 153 def cached_data(self): 154 return self._data 155 156 @property 157 def data(self): 158 self.cache() 159 return self._data 160 161 def cache(self): 162 if self._data is None: 163 self._data = self._readall() 164 return True 165 return False 166 167 def discard(self): 168 self._data = None 169 170 def read(self, suburl): 171 assert self.url, self.__class__.__name__ 172 url = self.url 173 if suburl: 174 url = '%s/%s' % (self.url, suburl) 175 return self.parent.read(url) 176 177 def _readall(self): 178 return self.read('') 179 180 181class AddressableDataNode(AddressableBaseDataNode): # pylint: disable=W0223 182 """Automatically encodes the url.""" 183 184 def __init__(self, parent, url, data): 185 super(AddressableDataNode, self).__init__(parent, urllib.quote(url), data) 186 187 188class NonAddressableDataNode(Node): # pylint: disable=W0223 189 """A node that cannot be addressed by an unique url. 190 191 The data comes directly from the parent. 192 """ 193 194 def __init__(self, parent, subkey): 195 super(NonAddressableDataNode, self).__init__(parent, None) 196 self.subkey = subkey 197 198 @property 199 def cached_data(self): 200 if self.parent.cached_data is None: 201 return None 202 return self.parent.cached_data[self.subkey] 203 204 @property 205 def data(self): 206 return self.parent.data[self.subkey] 207 208 def cache(self): 209 self.parent.cache() 210 211 def discard(self): # pragma: no cover 212 """Avoid invalid state when parent recreate the object.""" 213 raise AttributeError('Call parent discard() instead') 214 215 216class VirtualNodeList(Node): 217 """Base class for every node that has children. 218 219 Adds partial supports for keys and iterator functionality. 'key' can be a 220 string or a int. Not to be used directly. 221 """ 222 printable_attributes = Node.printable_attributes + ['keys'] 223 224 def __init__(self, parent, url): 225 super(VirtualNodeList, self).__init__(parent, url) 226 # Keeps the keys independently when ordering is needed. 227 self._is_cached = False 228 self._has_keys_cached = False 229 230 def __contains__(self, key): 231 """Enables 'if i in obj:'.""" 232 return key in self.keys 233 234 def __iter__(self): 235 """Enables 'for i in obj:'. It returns children.""" 236 self.cache_keys() 237 for key in self.keys: 238 yield self[key] 239 240 def __len__(self): 241 """Enables 'len(obj)' to get the number of childs.""" 242 return len(self.keys) 243 244 def discard(self): 245 """Discards data. 246 247 The default behavior is to not invalidate cached keys. The only place where 248 keys need to be invalidated is with Builds. 249 """ 250 self._is_cached = False 251 self._has_keys_cached = False 252 253 @property 254 def cached_children(self): # pragma: no cover 255 """Returns an iterator over the children that are cached.""" 256 raise NotImplementedError() 257 258 @property 259 def cached_keys(self): # pragma: no cover 260 raise NotImplementedError() 261 262 @property 263 def keys(self): # pragma: no cover 264 """Returns the keys for every children.""" 265 raise NotImplementedError() 266 267 def __getitem__(self, key): # pragma: no cover 268 """Returns a child, without fetching its data. 269 270 The children could be invalid since no verification is done. 271 """ 272 raise NotImplementedError() 273 274 def cache(self): # pragma: no cover 275 """Cache all the children.""" 276 raise NotImplementedError() 277 278 def cache_keys(self): # pragma: no cover 279 """Cache all children's keys.""" 280 raise NotImplementedError() 281 282 283class NodeList(VirtualNodeList): # pylint: disable=W0223 284 """Adds a cache of the keys.""" 285 286 def __init__(self, parent, url): 287 super(NodeList, self).__init__(parent, url) 288 self._keys = [] 289 290 @property 291 def cached_keys(self): 292 return self._keys 293 294 @property 295 def keys(self): 296 self.cache_keys() 297 return self._keys 298 299 300class NonAddressableNodeList(VirtualNodeList): # pylint: disable=W0223 301 """A node that contains children but retrieves all its data from its parent. 302 303 I.e. there's no url to get directly this data. 304 """ 305 # Child class object for children of this instance. For example, BuildSteps 306 # has BuildStep children. 307 _child_cls = None 308 309 def __init__(self, parent, subkey): 310 super(NonAddressableNodeList, self).__init__(parent, None) 311 self.subkey = subkey 312 assert (not isinstance(self._child_cls, NonAddressableDataNode) and 313 issubclass(self._child_cls, NonAddressableDataNode)), ( 314 self._child_cls.__name__) 315 316 @property 317 def cached_children(self): 318 if self.parent.cached_data is not None: 319 for i in xrange(len(self.parent.cached_data[self.subkey])): 320 yield self[i] 321 322 @property 323 def cached_data(self): 324 if self.parent.cached_data is None: 325 return None 326 return self.parent.data.get(self.subkey, None) 327 328 @property 329 def cached_keys(self): 330 if self.parent.cached_data is None: 331 return None 332 return range(len(self.parent.data.get(self.subkey, []))) 333 334 @property 335 def data(self): 336 return self.parent.data[self.subkey] 337 338 def cache(self): 339 self.parent.cache() 340 341 def cache_keys(self): 342 self.parent.cache() 343 344 def discard(self): # pragma: no cover 345 """Do not call. 346 347 Avoid infinite recursion by having the caller calls the parent's 348 discard() explicitely. 349 """ 350 raise AttributeError('Call parent discard() instead') 351 352 def __iter__(self): 353 """Enables 'for i in obj:'. It returns children.""" 354 if self.data: 355 for i in xrange(len(self.data)): 356 yield self[i] 357 358 def __getitem__(self, key): 359 """Doesn't cache the value, it's not needed. 360 361 TODO(maruel): Cache? 362 """ 363 if isinstance(key, int) and key < 0: 364 key = len(self.data) + key 365 # pylint: disable=E1102 366 return self._child_cls(self, key) 367 368 369class AddressableNodeList(NodeList): 370 """A node that has children that can be addressed with an url.""" 371 372 # Child class object for children of this instance. For example, Builders has 373 # Builder children and Builds has Build children. 374 _child_cls = None 375 376 def __init__(self, parent, url): 377 super(AddressableNodeList, self).__init__(parent, url) 378 self._cache = {} 379 assert (not isinstance(self._child_cls, AddressableDataNode) and 380 issubclass(self._child_cls, AddressableDataNode)), ( 381 self._child_cls.__name__) 382 383 @property 384 def cached_children(self): 385 for item in self._cache.itervalues(): 386 if item.cached_data is not None: 387 yield item 388 389 @property 390 def cached_keys(self): 391 return self._cache.keys() 392 393 def __getitem__(self, key): 394 """Enables 'obj[i]'.""" 395 if self._has_keys_cached and not key in self._keys: 396 raise KeyError(key) 397 398 if not key in self._cache: 399 # Create an empty object. 400 self._create_obj(key, None) 401 return self._cache[key] 402 403 def cache(self): 404 if not self._is_cached: 405 data = self._readall() 406 for key in sorted(data): 407 self._create_obj(key, data[key]) 408 self._is_cached = True 409 self._has_keys_cached = True 410 411 def cache_partial(self, children): 412 """Caches a partial number of children. 413 414 This method is more efficient since it does a single request for all the 415 children instead of one request per children. 416 417 It only grab objects not already cached. 418 """ 419 # pylint: disable=W0212 420 if not self._is_cached: 421 to_fetch = [ 422 child 423 for child in children 424 if not (child in self._cache and self._cache[child].cached_data) 425 ] 426 if to_fetch: 427 # Similar to cache(). The only reason to sort is to simplify testing. 428 params = '&'.join('select=%s' % urllib.quote(str(v)) 429 for v in sorted(to_fetch)) 430 data = self.read('?' + params) 431 for key in sorted(data): 432 self._create_obj(key, data[key]) 433 434 def cache_keys(self): 435 """Implement to speed up enumeration. Defaults to call cache().""" 436 if not self._has_keys_cached: 437 self.cache() 438 assert self._has_keys_cached 439 440 def discard(self): 441 """Discards temporary children.""" 442 super(AddressableNodeList, self).discard() 443 for v in self._cache.itervalues(): 444 v.discard() 445 446 def read(self, suburl): 447 assert self.url, self.__class__.__name__ 448 url = self.url 449 if suburl: 450 url = '%s/%s' % (self.url, suburl) 451 return self.parent.read(url) 452 453 def _create_obj(self, key, data): 454 """Creates an object of type self._child_cls.""" 455 # pylint: disable=E1102 456 obj = self._child_cls(self, key, data) 457 # obj.key and key may be different. 458 # No need to overide cached data with None. 459 if data is not None or obj.key not in self._cache: 460 self._cache[obj.key] = obj 461 if obj.key not in self._keys: 462 self._keys.append(obj.key) 463 464 def _readall(self): 465 return self.read('') 466 467 468class SubViewNodeList(VirtualNodeList): # pylint: disable=W0223 469 """A node that shows a subset of children that comes from another structure. 470 471 The node is not addressable. 472 473 E.g. the keys are retrieved from parent but the actual data comes from 474 virtual_parent. 475 """ 476 477 def __init__(self, parent, virtual_parent, subkey): 478 super(SubViewNodeList, self).__init__(parent, None) 479 self.subkey = subkey 480 self.virtual_parent = virtual_parent 481 assert isinstance(self.parent, AddressableDataNode) 482 assert isinstance(self.virtual_parent, NodeList) 483 484 @property 485 def cached_children(self): 486 if self.parent.cached_data is not None: 487 for item in self.keys: 488 if item in self.virtual_parent.keys: 489 child = self[item] 490 if child.cached_data is not None: 491 yield child 492 493 @property 494 def cached_keys(self): 495 return (self.parent.cached_data or {}).get(self.subkey, []) 496 497 @property 498 def keys(self): 499 self.cache_keys() 500 return self.parent.data.get(self.subkey, []) 501 502 def cache(self): 503 """Batch request for each child in a single read request.""" 504 if not self._is_cached: 505 self.virtual_parent.cache_partial(self.keys) 506 self._is_cached = True 507 508 def cache_keys(self): 509 if not self._has_keys_cached: 510 self.parent.cache() 511 self._has_keys_cached = True 512 513 def discard(self): 514 if self.parent.cached_data is not None: 515 for child in self.virtual_parent.cached_children: 516 if child.key in self.keys: 517 child.discard() 518 self.parent.discard() 519 super(SubViewNodeList, self).discard() 520 521 def __getitem__(self, key): 522 """Makes sure the key is in our key but grab it from the virtual parent.""" 523 return self.virtual_parent[key] 524 525 def __iter__(self): 526 self.cache() 527 return super(SubViewNodeList, self).__iter__() 528 529############################################################################### 530## Buildbot-specific code 531 532 533class Slave(AddressableDataNode): 534 """Buildbot slave class.""" 535 printable_attributes = AddressableDataNode.printable_attributes + [ 536 'name', 537 'key', 538 'connected', 539 'version', 540 ] 541 542 def __init__(self, parent, name, data): 543 super(Slave, self).__init__(parent, name, data) 544 self.name = name 545 self.key = self.name 546 # TODO(maruel): Add SlaveBuilders and a 'builders' property. 547 # TODO(maruel): Add a 'running_builds' property. 548 549 @property 550 def connected(self): 551 return self.data.get('connected', False) 552 553 @property 554 def version(self): 555 return self.data.get('version') 556 557 558class Slaves(AddressableNodeList): 559 """Buildbot slaves.""" 560 _child_cls = Slave 561 printable_attributes = AddressableNodeList.printable_attributes + ['names'] 562 563 def __init__(self, parent): 564 super(Slaves, self).__init__(parent, 'slaves') 565 566 @property 567 def names(self): 568 return self.keys 569 570 571class BuilderSlaves(SubViewNodeList): 572 """Similar to Slaves but only list slaves connected to a specific builder.""" 573 printable_attributes = SubViewNodeList.printable_attributes + ['names'] 574 575 def __init__(self, parent): 576 super(BuilderSlaves, self).__init__(parent, parent.parent.parent.slaves, 577 'slaves') 578 579 @property 580 def names(self): 581 return self.keys 582 583 584class BuildStep(NonAddressableDataNode): 585 """Class for a buildbot build step.""" 586 printable_attributes = NonAddressableDataNode.printable_attributes + [ 587 'name', 588 'number', 589 'start_time', 590 'end_time', 591 'duration', 592 'is_started', 593 'is_finished', 594 'is_running', 595 'result', 596 'simplified_result', 597 ] 598 599 def __init__(self, parent, number): 600 """Pre-loaded, since the data is retrieved via the Build object.""" 601 assert isinstance(number, int) 602 super(BuildStep, self).__init__(parent, number) 603 self.number = number 604 605 @property 606 def start_time(self): 607 if self.data.get('times'): 608 return int(round(self.data['times'][0])) 609 610 @property 611 def end_time(self): 612 times = self.data.get('times') 613 if times and len(times) == 2 and times[1]: 614 return int(round(times[1])) 615 616 @property 617 def duration(self): 618 if self.start_time: 619 return (self.end_time or int(round(time.time()))) - self.start_time 620 621 @property 622 def name(self): 623 return self.data['name'] 624 625 @property 626 def is_started(self): 627 return self.data.get('isStarted', False) 628 629 @property 630 def is_finished(self): 631 return self.data.get('isFinished', False) 632 633 @property 634 def is_running(self): 635 return self.is_started and not self.is_finished 636 637 @property 638 def result(self): 639 result = self.data.get('results') 640 if result is None: 641 # results may be 0, in that case with filter=1, the value won't be 642 # present. 643 if self.data.get('isFinished'): 644 result = self.data.get('results', 0) 645 while isinstance(result, list): 646 result = result[0] 647 return result 648 649 @property 650 def simplified_result(self): 651 """Returns a simplified 3 state value, True, False or None.""" 652 result = self.result 653 if result in (SUCCESS, WARNINGS): 654 return True 655 elif result in (FAILURE, EXCEPTION, RETRY): 656 return False 657 assert result in (None, SKIPPED), (result, self.data) 658 return None 659 660 661class BuildSteps(NonAddressableNodeList): 662 """Duplicates keys to support lookup by both step number and step name.""" 663 printable_attributes = NonAddressableNodeList.printable_attributes + [ 664 'failed', 665 ] 666 _child_cls = BuildStep 667 668 def __init__(self, parent): 669 """Pre-loaded, since the data is retrieved via the Build object.""" 670 super(BuildSteps, self).__init__(parent, 'steps') 671 672 @property 673 def keys(self): 674 """Returns the steps name in order.""" 675 return [i['name'] for i in self.data or []] 676 677 @property 678 def failed(self): 679 """Shortcuts that lists the step names of steps that failed.""" 680 return [step.name for step in self if step.simplified_result is False] 681 682 def __getitem__(self, key): 683 """Accept step name in addition to index number.""" 684 if isinstance(key, basestring): 685 # It's a string, try to find the corresponding index. 686 for i, step in enumerate(self.data): 687 if step['name'] == key: 688 key = i 689 break 690 else: 691 raise KeyError(key) 692 return super(BuildSteps, self).__getitem__(key) 693 694 695class Build(AddressableDataNode): 696 """Buildbot build info.""" 697 printable_attributes = AddressableDataNode.printable_attributes + [ 698 'key', 699 'number', 700 'steps', 701 'blame', 702 'reason', 703 'revision', 704 'result', 705 'simplified_result', 706 'start_time', 707 'end_time', 708 'duration', 709 'slave', 710 'properties', 711 'completed', 712 ] 713 714 def __init__(self, parent, key, data): 715 super(Build, self).__init__(parent, str(key), data) 716 self.number = int(key) 717 self.key = self.number 718 self.steps = BuildSteps(self) 719 720 @property 721 def blame(self): 722 return self.data.get('blame', []) 723 724 @property 725 def builder(self): 726 """Returns the Builder object. 727 728 Goes up the hierarchy to find the Buildbot.builders[builder] instance. 729 """ 730 return self.parent.parent.parent.parent.builders[self.data['builderName']] 731 732 @property 733 def start_time(self): 734 if self.data.get('times'): 735 return int(round(self.data['times'][0])) 736 737 @property 738 def end_time(self): 739 times = self.data.get('times') 740 if times and len(times) == 2 and times[1]: 741 return int(round(times[1])) 742 743 @property 744 def duration(self): 745 if self.start_time: 746 return (self.end_time or int(round(time.time()))) - self.start_time 747 748 @property 749 def eta(self): 750 return self.data.get('eta', 0) 751 752 @property 753 def completed(self): 754 return self.data.get('currentStep') is None 755 756 @property 757 def properties(self): 758 return self.data.get('properties', []) 759 760 @property 761 def reason(self): 762 return self.data.get('reason') 763 764 @property 765 def result(self): 766 result = self.data.get('results') 767 while isinstance(result, list): 768 result = result[0] 769 if result is None and self.steps: 770 # results may be 0, in that case with filter=1, the value won't be 771 # present. 772 result = self.steps[-1].result 773 return result 774 775 @property 776 def revision(self): 777 return self.data.get('sourceStamp', {}).get('revision') 778 779 @property 780 def simplified_result(self): 781 """Returns a simplified 3 state value, True, False or None.""" 782 result = self.result 783 if result in (SUCCESS, WARNINGS, SKIPPED): 784 return True 785 elif result in (FAILURE, EXCEPTION, RETRY): 786 return False 787 assert result is None, (result, self.data) 788 return None 789 790 @property 791 def slave(self): 792 """Returns the Slave object. 793 794 Goes up the hierarchy to find the Buildbot.slaves[slave] instance. 795 """ 796 return self.parent.parent.parent.parent.slaves[self.data['slave']] 797 798 def discard(self): 799 """Completed Build isn't discarded.""" 800 if self._data and self.result is None: 801 assert not self.steps or not self.steps[-1].data.get('isFinished') 802 self._data = None 803 804 805class CurrentBuilds(SubViewNodeList): 806 """Lists of the current builds.""" 807 808 def __init__(self, parent): 809 super(CurrentBuilds, self).__init__(parent, parent.builds, 'currentBuilds') 810 811 812class PendingBuilds(AddressableDataNode): 813 """List of the pending builds.""" 814 815 def __init__(self, parent): 816 super(PendingBuilds, self).__init__(parent, 'pendingBuilds', None) 817 818 819class Builds(AddressableNodeList): 820 """Supports iteration. 821 822 Recommends using .cache() to speed up if a significant number of builds are 823 iterated over. 824 """ 825 _child_cls = Build 826 827 def __init__(self, parent): 828 super(Builds, self).__init__(parent, 'builds') 829 830 def __getitem__(self, key): 831 """Support for negative reference and enable retrieving non-cached builds. 832 833 e.g. -1 is the last build, -2 is the previous build before the last one. 834 """ 835 key = int(key) 836 if key < 0: 837 # Convert negative to positive build number. 838 self.cache_keys() 839 # Since the negative value can be outside of the cache keys range, use the 840 # highest key value and calculate from it. 841 key = max(self._keys) + key + 1 842 843 if not key in self._cache: 844 # Create an empty object. 845 self._create_obj(key, None) 846 return self._cache[key] 847 848 def __iter__(self): 849 """Returns cached Build objects in reversed order. 850 851 The most recent build is returned first and then in reverse chronological 852 order, up to the oldest cached build by the server. Older builds can be 853 accessed but will trigger significantly more I/O so they are not included by 854 default in the iteration. 855 856 To access the older builds, use self.iterall() instead. 857 """ 858 self.cache() 859 return reversed(self._cache.values()) 860 861 def iterall(self): 862 """Returns Build objects in decreasing order unbounded up to build 0. 863 864 The most recent build is returned first and then in reverse chronological 865 order. Older builds can be accessed and will trigger significantly more I/O 866 so use this carefully. 867 """ 868 # Only cache keys here. 869 self.cache_keys() 870 if self._keys: 871 for i in xrange(max(self._keys), -1, -1): 872 yield self[i] 873 874 def cache_keys(self): 875 """Grabs the keys (build numbers) from the builder.""" 876 if not self._has_keys_cached: 877 for i in self.parent.data.get('cachedBuilds', []): 878 i = int(i) 879 self._cache.setdefault(i, Build(self, i, None)) 880 if i not in self._keys: 881 self._keys.append(i) 882 self._has_keys_cached = True 883 884 def discard(self): 885 super(Builds, self).discard() 886 # Can't keep keys. 887 self._has_keys_cached = False 888 889 def _readall(self): 890 return self.read('_all') 891 892 893class Builder(AddressableDataNode): 894 """Builder status.""" 895 printable_attributes = AddressableDataNode.printable_attributes + [ 896 'name', 897 'key', 898 'builds', 899 'slaves', 900 'pending_builds', 901 'current_builds', 902 ] 903 904 def __init__(self, parent, name, data): 905 super(Builder, self).__init__(parent, name, data) 906 self.name = name 907 self.key = name 908 self.builds = Builds(self) 909 self.slaves = BuilderSlaves(self) 910 self.current_builds = CurrentBuilds(self) 911 self.pending_builds = PendingBuilds(self) 912 913 def discard(self): 914 super(Builder, self).discard() 915 self.builds.discard() 916 self.slaves.discard() 917 self.current_builds.discard() 918 919 920class Builders(AddressableNodeList): 921 """Root list of builders.""" 922 _child_cls = Builder 923 924 def __init__(self, parent): 925 super(Builders, self).__init__(parent, 'builders') 926 927 928class Buildbot(AddressableBaseDataNode): 929 """This object should be recreated on a master restart as it caches data.""" 930 # Throttle fetches to not kill the server. 931 auto_throttle = None 932 printable_attributes = AddressableDataNode.printable_attributes + [ 933 'slaves', 934 'builders', 935 'last_fetch', 936 ] 937 938 def __init__(self, url): 939 super(Buildbot, self).__init__(None, url.rstrip('/') + '/json', None) 940 self._builders = Builders(self) 941 self._slaves = Slaves(self) 942 self.last_fetch = None 943 944 @property 945 def builders(self): 946 return self._builders 947 948 @property 949 def slaves(self): 950 return self._slaves 951 952 def discard(self): 953 """Discards information about Builders and Slaves.""" 954 super(Buildbot, self).discard() 955 self._builders.discard() 956 self._slaves.discard() 957 958 def read(self, suburl): 959 if self.auto_throttle: 960 if self.last_fetch: 961 delta = datetime.datetime.utcnow() - self.last_fetch 962 remaining = (datetime.timedelta(seconds=self.auto_throttle) - delta) 963 if remaining > datetime.timedelta(seconds=0): 964 logging.debug('Sleeping for %ss', remaining) 965 time.sleep(remaining.seconds) 966 self.last_fetch = datetime.datetime.utcnow() 967 url = '%s/%s' % (self.url, suburl) 968 if '?' in url: 969 url += '&filter=1' 970 else: 971 url += '?filter=1' 972 logging.info('read(%s)', suburl) 973 channel = urllib.urlopen(url) 974 data = channel.read() 975 try: 976 return json.loads(data) 977 except ValueError: 978 if channel.getcode() >= 400: 979 # Convert it into an HTTPError for easier processing. 980 raise urllib2.HTTPError(url, channel.getcode(), '%s:\n%s' % (url, data), 981 channel.headers, None) 982 raise 983 984 def _readall(self): 985 return self.read('project') 986 987############################################################################### 988## Controller code 989 990 991def usage(more): 992 993 def hook(fn): 994 fn.func_usage_more = more 995 return fn 996 997 return hook 998 999 1000def need_buildbot(fn): 1001 """Post-parse args to create a buildbot object.""" 1002 1003 @functools.wraps(fn) 1004 def hook(parser, args, *extra_args, **kwargs): 1005 old_parse_args = parser.parse_args 1006 1007 def new_parse_args(args): 1008 options, args = old_parse_args(args) 1009 if len(args) < 1: 1010 parser.error('Need to pass the root url of the buildbot') 1011 url = args.pop(0) 1012 if not url.startswith('http'): 1013 url = 'http://' + url 1014 buildbot = Buildbot(url) 1015 buildbot.auto_throttle = options.throttle 1016 return options, args, buildbot 1017 1018 parser.parse_args = new_parse_args 1019 # Call the original function with the modified parser. 1020 return fn(parser, args, *extra_args, **kwargs) 1021 1022 hook.func_usage_more = '[options] <url>' 1023 return hook 1024 1025 1026@need_buildbot 1027def CMDpending(parser, args): 1028 """Lists pending jobs.""" 1029 parser.add_option('-b', 1030 '--builder', 1031 dest='builders', 1032 action='append', 1033 default=[], 1034 help='Builders to filter on') 1035 options, args, buildbot = parser.parse_args(args) 1036 if args: 1037 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1038 if not options.builders: 1039 options.builders = buildbot.builders.keys 1040 for builder in options.builders: 1041 builder = buildbot.builders[builder] 1042 pending_builds = builder.data.get('pendingBuilds', 0) 1043 if not pending_builds: 1044 continue 1045 print('Builder %s: %d' % (builder.name, pending_builds)) 1046 if not options.quiet: 1047 for pending in builder.pending_builds.data: 1048 if 'revision' in pending['source']: 1049 print(' revision: %s' % pending['source']['revision']) 1050 for change in pending['source']['changes']: 1051 print(' change:') 1052 print(' comment: %r' % unicode(change['comments'][:50])) 1053 print(' who: %s' % change['who']) 1054 return 0 1055 1056 1057@usage('[options] <url> [commands] ...') 1058@need_buildbot 1059def CMDrun(parser, args): 1060 """Runs commands passed as parameters. 1061 1062 When passing commands on the command line, each command will be run as if it 1063 was on its own line. 1064 """ 1065 parser.add_option('-f', '--file', help='Read script from file') 1066 parser.add_option('-i', 1067 dest='use_stdin', 1068 action='store_true', 1069 help='Read script on stdin') 1070 # Variable 'buildbot' is not used directly. 1071 # pylint: disable=W0612 1072 options, args, buildbot = parser.parse_args(args) 1073 if (bool(args) + bool(options.use_stdin) + bool(options.file)) != 1: 1074 parser.error('Need to pass only one of: <commands>, -f <file> or -i') 1075 if options.use_stdin: 1076 cmds = sys.stdin.read() 1077 elif options.file: 1078 cmds = open(options.file).read() 1079 else: 1080 cmds = '\n'.join(args) 1081 compiled = compile(cmds, '<cmd line>', 'exec') 1082 # pylint: disable=eval-used 1083 eval(compiled, globals(), locals()) 1084 return 0 1085 1086 1087@need_buildbot 1088def CMDinteractive(parser, args): 1089 """Runs an interactive shell to run queries.""" 1090 _, args, buildbot = parser.parse_args(args) 1091 if args: 1092 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1093 prompt = ( 1094 'Buildbot interactive console for "%s".\n' 1095 'Hint: Start with typing: \'buildbot.printable_attributes\' or ' 1096 '\'print str(buildbot)\' to explore.') % buildbot.url[:-len('/json')] 1097 local_vars = {'buildbot': buildbot, 'b': buildbot} 1098 code.interact(prompt, None, local_vars) 1099 1100 1101@need_buildbot 1102def CMDidle(parser, args): 1103 """Lists idle slaves.""" 1104 return find_idle_busy_slaves(parser, args, True) 1105 1106 1107@need_buildbot 1108def CMDbusy(parser, args): 1109 """Lists idle slaves.""" 1110 return find_idle_busy_slaves(parser, args, False) 1111 1112 1113@need_buildbot 1114def CMDdisconnected(parser, args): 1115 """Lists disconnected slaves.""" 1116 _, args, buildbot = parser.parse_args(args) 1117 if args: 1118 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1119 for slave in buildbot.slaves: 1120 if not slave.connected: 1121 print(slave.name) 1122 return 0 1123 1124 1125def find_idle_busy_slaves(parser, args, show_idle): 1126 parser.add_option('-b', 1127 '--builder', 1128 dest='builders', 1129 action='append', 1130 default=[], 1131 help='Builders to filter on') 1132 parser.add_option('-s', 1133 '--slave', 1134 dest='slaves', 1135 action='append', 1136 default=[], 1137 help='Slaves to filter on') 1138 options, args, buildbot = parser.parse_args(args) 1139 if args: 1140 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1141 if not options.builders: 1142 options.builders = buildbot.builders.keys 1143 for builder in options.builders: 1144 builder = buildbot.builders[builder] 1145 if options.slaves: 1146 # Only the subset of slaves connected to the builder. 1147 slaves = list(set(options.slaves).intersection(set(builder.slaves.names))) 1148 if not slaves: 1149 continue 1150 else: 1151 slaves = builder.slaves.names 1152 busy_slaves = [build.slave.name for build in builder.current_builds] 1153 if show_idle: 1154 slaves = natsorted(set(slaves) - set(busy_slaves)) 1155 else: 1156 slaves = natsorted(set(slaves) & set(busy_slaves)) 1157 if options.quiet: 1158 for slave in slaves: 1159 print(slave) 1160 else: 1161 if slaves: 1162 print('Builder %s: %s' % (builder.name, ', '.join(slaves))) 1163 return 0 1164 1165 1166def last_failure(buildbot, 1167 builders=None, 1168 slaves=None, 1169 steps=None, 1170 no_cache=False): 1171 """Returns Build object with last failure with the specific filters.""" 1172 builders = builders or buildbot.builders.keys 1173 for builder in builders: 1174 builder = buildbot.builders[builder] 1175 if slaves: 1176 # Only the subset of slaves connected to the builder. 1177 builder_slaves = list(set(slaves).intersection(set(builder.slaves.names))) 1178 if not builder_slaves: 1179 continue 1180 else: 1181 builder_slaves = builder.slaves.names 1182 1183 if not no_cache and len(builder.slaves) > 2: 1184 # Unless you just want the last few builds, it's often faster to 1185 # fetch the whole thing at once, at the cost of a small hickup on 1186 # the buildbot. 1187 # TODO(maruel): Cache only N last builds or all builds since 1188 # datetime. 1189 builder.builds.cache() 1190 1191 found = [] 1192 for build in builder.builds: 1193 if build.slave.name not in builder_slaves or build.slave.name in found: 1194 continue 1195 # Only add the slave for the first completed build but still look for 1196 # incomplete builds. 1197 if build.completed: 1198 found.append(build.slave.name) 1199 1200 if steps: 1201 if any(build.steps[step].simplified_result is False for step in steps): 1202 yield build 1203 elif build.simplified_result is False: 1204 yield build 1205 1206 if len(found) == len(builder_slaves): 1207 # Found all the slaves, quit. 1208 break 1209 1210 1211@need_buildbot 1212def CMDlast_failure(parser, args): 1213 """Lists all slaves that failed on that step on their last build. 1214 1215 Example: to find all slaves where their last build was a compile failure, 1216 run with --step compile 1217 """ 1218 parser.add_option( 1219 '-S', 1220 '--step', 1221 dest='steps', 1222 action='append', 1223 default=[], 1224 help='List all slaves that failed on that step on their last build') 1225 parser.add_option('-b', 1226 '--builder', 1227 dest='builders', 1228 action='append', 1229 default=[], 1230 help='Builders to filter on') 1231 parser.add_option('-s', 1232 '--slave', 1233 dest='slaves', 1234 action='append', 1235 default=[], 1236 help='Slaves to filter on') 1237 parser.add_option('-n', 1238 '--no_cache', 1239 action='store_true', 1240 help='Don\'t load all builds at once') 1241 options, args, buildbot = parser.parse_args(args) 1242 if args: 1243 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1244 print_builders = not options.quiet and len(options.builders) != 1 1245 last_builder = None 1246 for build in last_failure(buildbot, 1247 builders=options.builders, 1248 slaves=options.slaves, 1249 steps=options.steps, 1250 no_cache=options.no_cache): 1251 1252 if print_builders and last_builder != build.builder: 1253 print(build.builder.name) 1254 last_builder = build.builder 1255 1256 if options.quiet: 1257 if options.slaves: 1258 print('%s: %s' % (build.builder.name, build.slave.name)) 1259 else: 1260 print(build.slave.name) 1261 else: 1262 out = '%d on %s: blame:%s' % (build.number, build.slave.name, 1263 ', '.join(build.blame)) 1264 if print_builders: 1265 out = ' ' + out 1266 print(out) 1267 1268 if len(options.steps) != 1: 1269 for step in build.steps: 1270 if step.simplified_result is False: 1271 # Assume the first line is the text name anyway. 1272 summary = ', '.join(step.data['text'][1:])[:40] 1273 out = ' %s: "%s"' % (step.data['name'], summary) 1274 if print_builders: 1275 out = ' ' + out 1276 print(out) 1277 return 0 1278 1279 1280@need_buildbot 1281def CMDcurrent(parser, args): 1282 """Lists current jobs.""" 1283 parser.add_option('-b', 1284 '--builder', 1285 dest='builders', 1286 action='append', 1287 default=[], 1288 help='Builders to filter on') 1289 parser.add_option('--blame', 1290 action='store_true', 1291 help='Only print the blame list') 1292 options, args, buildbot = parser.parse_args(args) 1293 if args: 1294 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1295 if not options.builders: 1296 options.builders = buildbot.builders.keys 1297 1298 if options.blame: 1299 blame = set() 1300 for builder in options.builders: 1301 for build in buildbot.builders[builder].current_builds: 1302 if build.blame: 1303 for blamed in build.blame: 1304 blame.add(blamed) 1305 print('\n'.join(blame)) 1306 return 0 1307 1308 for builder in options.builders: 1309 builder = buildbot.builders[builder] 1310 if not options.quiet and builder.current_builds: 1311 print(builder.name) 1312 for build in builder.current_builds: 1313 if options.quiet: 1314 print(build.slave.name) 1315 else: 1316 out = '%4d: slave=%10s' % (build.number, build.slave.name) 1317 out += ' duration=%5d' % (build.duration or 0) 1318 if build.eta: 1319 out += ' eta=%5.0f' % build.eta 1320 else: 1321 out += ' ' 1322 if build.blame: 1323 out += ' blame=' + ', '.join(build.blame) 1324 print(out) 1325 1326 return 0 1327 1328 1329@need_buildbot 1330def CMDbuilds(parser, args): 1331 """Lists all builds. 1332 1333 Example: to find all builds on a single slave, run with -b bar -s foo 1334 """ 1335 parser.add_option('-r', 1336 '--result', 1337 type='int', 1338 help='Build result to filter on') 1339 parser.add_option('-b', 1340 '--builder', 1341 dest='builders', 1342 action='append', 1343 default=[], 1344 help='Builders to filter on') 1345 parser.add_option('-s', 1346 '--slave', 1347 dest='slaves', 1348 action='append', 1349 default=[], 1350 help='Slaves to filter on') 1351 parser.add_option('-n', 1352 '--no_cache', 1353 action='store_true', 1354 help='Don\'t load all builds at once') 1355 options, args, buildbot = parser.parse_args(args) 1356 if args: 1357 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1358 builders = options.builders or buildbot.builders.keys 1359 for builder in builders: 1360 builder = buildbot.builders[builder] 1361 for build in builder.builds: 1362 if not options.slaves or build.slave.name in options.slaves: 1363 if options.quiet: 1364 out = '' 1365 if options.builders: 1366 out += '%s/' % builder.name 1367 if len(options.slaves) != 1: 1368 out += '%s/' % build.slave.name 1369 out += '%d revision:%s result:%s blame:%s' % ( 1370 build.number, build.revision, build.result, ','.join(build.blame)) 1371 print(out) 1372 else: 1373 print(build) 1374 return 0 1375 1376 1377@need_buildbot 1378def CMDcount(parser, args): 1379 """Count the number of builds that occured during a specific period.""" 1380 parser.add_option('-o', 1381 '--over', 1382 type='int', 1383 help='Number of seconds to look for') 1384 parser.add_option('-b', 1385 '--builder', 1386 dest='builders', 1387 action='append', 1388 default=[], 1389 help='Builders to filter on') 1390 options, args, buildbot = parser.parse_args(args) 1391 if args: 1392 parser.error('Unrecognized parameters: %s' % ' '.join(args)) 1393 if not options.over: 1394 parser.error( 1395 'Specify the number of seconds, e.g. --over 86400 for the last 24 ' 1396 'hours') 1397 builders = options.builders or buildbot.builders.keys 1398 counts = {} 1399 since = time.time() - options.over 1400 for builder in builders: 1401 builder = buildbot.builders[builder] 1402 counts[builder.name] = 0 1403 if not options.quiet: 1404 print(builder.name) 1405 for build in builder.builds.iterall(): 1406 try: 1407 start_time = build.start_time 1408 except urllib2.HTTPError: 1409 # The build was probably trimmed. 1410 print('Failed to fetch build %s/%d' % (builder.name, build.number), 1411 file=sys.stderr) 1412 continue 1413 if start_time >= since: 1414 counts[builder.name] += 1 1415 else: 1416 break 1417 if not options.quiet: 1418 print('.. %d' % counts[builder.name]) 1419 1420 align_name = max(len(b) for b in counts) 1421 align_number = max(len(str(c)) for c in counts.itervalues()) 1422 for builder in sorted(counts): 1423 print('%*s: %*d' % (align_name, builder, align_number, counts[builder])) 1424 print('Total: %d' % sum(counts.itervalues())) 1425 return 0 1426 1427 1428def gen_parser(): 1429 """Returns an OptionParser instance with default options. 1430 1431 It should be then processed with gen_usage() before being used. 1432 """ 1433 parser = optparse.OptionParser(version=__version__) 1434 # Remove description formatting 1435 parser.format_description = lambda x: parser.description 1436 # Add common parsing. 1437 old_parser_args = parser.parse_args 1438 1439 def Parse(*args, **kwargs): 1440 options, args = old_parser_args(*args, **kwargs) 1441 if options.verbose >= 2: 1442 logging.basicConfig(level=logging.DEBUG) 1443 elif options.verbose: 1444 logging.basicConfig(level=logging.INFO) 1445 else: 1446 logging.basicConfig(level=logging.WARNING) 1447 return options, args 1448 1449 parser.parse_args = Parse 1450 1451 parser.add_option('-v', 1452 '--verbose', 1453 action='count', 1454 help='Use multiple times to increase logging leve') 1455 parser.add_option( 1456 '-q', 1457 '--quiet', 1458 action='store_true', 1459 help='Reduces the output to be parsed by scripts, independent of -v') 1460 parser.add_option('--throttle', 1461 type='float', 1462 help='Minimum delay to sleep between requests') 1463 return parser 1464 1465############################################################################### 1466## Generic subcommand handling code 1467 1468 1469def Command(name): 1470 return getattr(sys.modules[__name__], 'CMD' + name, None) 1471 1472 1473@usage('<command>') 1474def CMDhelp(parser, args): 1475 """Print list of commands or use 'help <command>'.""" 1476 _, args = parser.parse_args(args) 1477 if len(args) == 1: 1478 return main(args + ['--help']) 1479 parser.print_help() 1480 return 0 1481 1482 1483def gen_usage(parser, command): 1484 """Modifies an OptionParser object with the command's documentation. 1485 1486 The documentation is taken from the function's docstring. 1487 """ 1488 obj = Command(command) 1489 more = getattr(obj, 'func_usage_more') 1490 # OptParser.description prefer nicely non-formatted strings. 1491 parser.description = obj.__doc__ + '\n' 1492 parser.set_usage('usage: %%prog %s %s' % (command, more)) 1493 1494 1495def main(args=None): 1496 # Do it late so all commands are listed. 1497 # pylint: disable=E1101 1498 CMDhelp.__doc__ += '\n\nCommands are:\n' + '\n'.join( 1499 ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n', 1)[0]) 1500 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')) 1501 1502 parser = gen_parser() 1503 if args is None: 1504 args = sys.argv[1:] 1505 if args: 1506 command = Command(args[0]) 1507 if command: 1508 # "fix" the usage and the description now that we know the subcommand. 1509 gen_usage(parser, args[0]) 1510 return command(parser, args[1:]) 1511 1512 # Not a known command. Default to help. 1513 gen_usage(parser, 'help') 1514 return CMDhelp(parser, args) 1515 1516 1517if __name__ == '__main__': 1518 sys.exit(main()) 1519