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

Side by Side Diff: commit-queue/buildbot_json.py

Issue 135363007: Delete public commit queue to avoid confusion after move to internal repo (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/
Patch Set: Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « commit-queue/async_push.py ('k') | commit-queue/codereview.settings » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 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())
OLDNEW
« no previous file with comments | « commit-queue/async_push.py ('k') | commit-queue/codereview.settings » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698