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