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