1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """Simple JSON exporter."""
18
19 import datetime
20 import os
21 import re
22
23 from twisted.internet import defer
24 from twisted.web import html, resource, server
25
26 from buildbot.status.web.base import HtmlResource
27 from buildbot.util import json
28
29
30 _IS_INT = re.compile('^[-+]?\d+$')
31
32
33 FLAGS = """\
34 - as_text
35 - By default, application/json is used. Setting as_text=1 change the type
36 to text/plain and implicitly sets compact=0 and filter=1. Mainly useful to
37 look at the result in a web browser.
38 - compact
39 - By default, the json data is compact and defaults to 1. For easier to read
40 indented output, set compact=0.
41 - select
42 - By default, most children data is listed. You can do a random selection
43 of data by using select=<sub-url> multiple times to coagulate data.
44 "select=" includes the actual url otherwise it is skipped.
45 - filter
46 - Filters out null, false, and empty string, list and dict. This reduce the
47 amount of useless data sent.
48 - callback
49 - Enable uses of JSONP as described in
50 http://en.wikipedia.org/wiki/JSONP. Note that
51 Access-Control-Allow-Origin:* is set in the HTTP response header so you
52 can use this in compatible browsers.
53 """
54
55 EXAMPLES = """\
56 - /json
57 - Root node, that *doesn't* mean all the data. Many things (like logs) must
58 be explicitly queried for performance reasons.
59 - /json/builders/
60 - All builders.
61 - /json/builders/<A_BUILDER>
62 - A specific builder as compact text.
63 - /json/builders/<A_BUILDER>/builds
64 - All *cached* builds.
65 - /json/builders/<A_BUILDER>/builds/_all
66 - All builds. Warning, reads all previous build data.
67 - /json/builders/<A_BUILDER>/builds/<A_BUILD>
68 - Where <A_BUILD> is either positive, a build number, or negative, a past
69 build.
70 - /json/builders/<A_BUILDER>/builds/-1/source_stamp/changes
71 - Build changes
72 - /json/builders/<A_BUILDER>/builds?select=-1&select=-2
73 - Two last builds on '<A_BUILDER>' builder.
74 - /json/builders/<A_BUILDER>/builds?select=-1/source_stamp/changes&select=-2/source_stamp/changes
75 - Changes of the two last builds on '<A_BUILDER>' builder.
76 - /json/builders/<A_BUILDER>/slaves
77 - Slaves associated to this builder.
78 - /json/builders/<A_BUILDER>?select=&select=slaves
79 - Builder information plus details information about its slaves. Neat eh?
80 - /json/slaves/<A_SLAVE>
81 - A specific slave.
82 - /json?select=slaves/<A_SLAVE>/&select=project&select=builders/<A_BUILDER>/builds/<A_BUILD>
83 - A selection of random unrelated stuff as an random example. :)
84 """
88 return request.args.get(arg, [default])[0]
89
92 value = RequestArg(request, arg, default)
93 if value in (False, True):
94 return value
95 value = value.lower()
96 if value in ('1', 'true'):
97 return True
98 if value in ('0', 'false'):
99 return False
100
101 return default
102
105 """Returns a copy with None, False, "", [], () and {} removed.
106 Warning: converts tuple to list."""
107 if isinstance(data, (list, tuple)):
108
109 items = map(FilterOut, data)
110 if not filter(lambda x: not x in ('', False, None, [], {}, ()), items):
111 return None
112 return items
113 elif isinstance(data, dict):
114 return dict(filter(lambda x: not x[1] in ('', False, None, [], {}, ()),
115 [(k, FilterOut(v)) for (k, v) in data.iteritems()]))
116 else:
117 return data
118
121 """Base class for json data."""
122
123 contentType = "application/json"
124 cache_seconds = 60
125 help = None
126 pageTitle = None
127 level = 0
128
140
149
151 """Adds the resource's level for help links generation."""
152
153 def RecurseFix(res, level):
154 res.level = level + 1
155 for c in res.children.itervalues():
156 RecurseFix(c, res.level)
157
158 RecurseFix(res, self.level)
159 resource.Resource.putChild(self, name, res)
160
162 """Renders a HTTP GET at the http request level."""
163 d = defer.maybeDeferred(lambda : self.content(request))
164 def handle(data):
165 if isinstance(data, unicode):
166 data = data.encode("utf-8")
167 request.setHeader("Access-Control-Allow-Origin", "*")
168 if RequestArgToBool(request, 'as_text', False):
169 request.setHeader("content-type", 'text/plain')
170 else:
171 request.setHeader("content-type", self.contentType)
172 request.setHeader("content-disposition",
173 "attachment; filename=\"%s.json\"" % request.path)
174
175 if self.cache_seconds:
176 now = datetime.datetime.utcnow()
177 expires = now + datetime.timedelta(seconds=self.cache_seconds)
178 request.setHeader("Expires",
179 expires.strftime("%a, %d %b %Y %H:%M:%S GMT"))
180 request.setHeader("Pragma", "no-cache")
181 return data
182 d.addCallback(handle)
183 def ok(data):
184 request.write(data)
185 request.finish()
186 def fail(f):
187 request.processingFailed(f)
188 return None
189 d.addCallbacks(ok, fail)
190 return server.NOT_DONE_YET
191
192 @defer.deferredGenerator
193 - def content(self, request):
194 """Renders the json dictionaries."""
195
196 select = request.args.get('select')
197 as_text = RequestArgToBool(request, 'as_text', False)
198 filter_out = RequestArgToBool(request, 'filter', as_text)
199 compact = RequestArgToBool(request, 'compact', not as_text)
200 callback = request.args.get('callback')
201
202
203 if select is not None:
204 del request.args['select']
205
206 data = {}
207
208 select = [s.strip('/') for s in select]
209 select.sort(cmp=lambda x,y: cmp(x.count('/'), y.count('/')),
210 reverse=True)
211 for item in select:
212
213 node = data
214
215
216 child = self
217 prepath = request.prepath[:]
218 postpath = request.postpath[:]
219 request.postpath = filter(None, item.split('/'))
220 while request.postpath and not child.isLeaf:
221 pathElement = request.postpath.pop(0)
222 node[pathElement] = {}
223 node = node[pathElement]
224 request.prepath.append(pathElement)
225 child = child.getChildWithDefault(pathElement, request)
226
227
228
229 if hasattr(child, 'asDict'):
230 wfd = defer.waitForDeferred(
231 defer.maybeDeferred(lambda :
232 child.asDict(request)))
233 yield wfd
234 child_dict = wfd.getResult()
235 else:
236 child_dict = {
237 'error' : 'Not available',
238 }
239 node.update(child_dict)
240
241 request.prepath = prepath
242 request.postpath = postpath
243 else:
244 wfd = defer.waitForDeferred(
245 defer.maybeDeferred(lambda :
246 self.asDict(request)))
247 yield wfd
248 data = wfd.getResult()
249
250 if filter_out:
251 data = FilterOut(data)
252 if compact:
253 data = json.dumps(data, sort_keys=True, separators=(',',':'))
254 else:
255 data = json.dumps(data, sort_keys=True, indent=2)
256 if callback:
257
258 callback = callback[0]
259 if re.match(r'^[a-zA-Z$][a-zA-Z$0-9.]*$', callback):
260 data = '%s(%s);' % (callback, data)
261 yield data
262
263 @defer.deferredGenerator
265 """Generates the json dictionary.
266
267 By default, renders every childs."""
268 if self.children:
269 data = {}
270 for name in self.children:
271 child = self.getChildWithDefault(name, request)
272 if isinstance(child, JsonResource):
273 wfd = defer.waitForDeferred(
274 defer.maybeDeferred(lambda :
275 child.asDict(request)))
276 yield wfd
277 data[name] = wfd.getResult()
278
279 yield data
280 else:
281 raise NotImplementedError()
282
285 """Convert a string in a wiki-style format into HTML."""
286 indent = 0
287 in_item = False
288 output = []
289 for line in text.splitlines(False):
290 match = re.match(r'^( +)\- (.*)$', line)
291 if match:
292 if indent < len(match.group(1)):
293 output.append('<ul>')
294 indent = len(match.group(1))
295 elif indent > len(match.group(1)):
296 while indent > len(match.group(1)):
297 output.append('</ul>')
298 indent -= 2
299 if in_item:
300
301 output.append('</li>')
302 output.append('<li>')
303 in_item = True
304 line = match.group(2)
305 elif indent:
306 if line.startswith((' ' * indent) + ' '):
307
308 line = line.strip()
309 else:
310
311 if in_item:
312 output.append('</li>')
313 in_item = False
314 while indent > 0:
315 output.append('</ul>')
316 indent -= 2
317
318 if line.startswith('/'):
319 if not '?' in line:
320 line_full = line + '?as_text=1'
321 else:
322 line_full = line + '&as_text=1'
323 output.append('<a href="' + html.escape(line_full) + '">' +
324 html.escape(line) + '</a>')
325 else:
326 output.append(html.escape(line).replace(' ', ' '))
327 if not in_item:
328 output.append('<br>')
329
330 if in_item:
331 output.append('</li>')
332 while indent > 0:
333 output.append('</ul>')
334 indent -= 2
335 return '\n'.join(output)
336
339 - def __init__(self, text, pageTitle, parent_node):
344
345 - def content(self, request, cxt):
346 cxt['level'] = self.parent_node.level
347 cxt['text'] = ToHtml(self.text)
348 cxt['children'] = [ n for n in self.parent_node.children.keys() if n != 'help' ]
349 cxt['flags'] = ToHtml(FLAGS)
350 cxt['examples'] = ToHtml(EXAMPLES).replace(
351 'href="/json',
352 'href="%sjson' % (self.level * '../'))
353
354 template = request.site.buildbot_service.templates.get_template("jsonhelp.html")
355 return template.render(**cxt)
356
358 help = """Describe pending builds for a builder.
359 """
360 pageTitle = 'Builder'
361
362 - def __init__(self, status, builder_status):
365
372 d.addCallback(to_dict)
373 return d
374
377 help = """Describe a single builder.
378 """
379 pageTitle = 'Builder'
380
381 - def __init__(self, status, builder_status):
390
394
397 help = """List of all the builders defined on a master.
398 """
399 pageTitle = 'Builders'
400
407
410 help = """Describe the slaves attached to a single builder.
411 """
412 pageTitle = 'BuilderSlaves'
413
414 - def __init__(self, status, builder_status):
421
424 help = """Describe a single build.
425 """
426 pageTitle = 'Build'
427
428 - def __init__(self, status, build_status):
435
438
441 help = """All the builds that were run on a builder.
442 """
443 pageTitle = 'AllBuilds'
444
445 - def __init__(self, status, builder_status):
448
466
478
481 help = """Builds that were run on a builder.
482 """
483 pageTitle = 'Builds'
484
485 - def __init__(self, status, builder_status):
488
492
504
507 help = """A single build step.
508 """
509 pageTitle = 'BuildStep'
510
511 - def __init__(self, status, build_step_status):
515
516
518 return self.build_step_status.asDict()
519
522 help = """A list of build steps that occurred during a build.
523 """
524 pageTitle = 'BuildSteps'
525
526 - def __init__(self, status, build_status):
529
530
531
550
559
562 help = """Describe a single change that originates from a change source.
563 """
564 pageTitle = 'Change'
565
570
572 return self.change.asDict()
573
576 help = """List of changes.
577 """
578 pageTitle = 'Changes'
579
590
592 """Don't throw an exception when there is no child."""
593 if not self.children:
594 return {}
595 return JsonResource.asDict(self, request)
596
599 help = """Describe a change source.
600 """
601 pageTitle = 'ChangeSources'
602
604 result = {}
605 n = 0
606 for c in self.status.getChangeSources():
607
608 change = {}
609 change['description'] = c.describe()
610 result[n] = change
611 n += 1
612 return result
613
616 help = """Project-wide settings.
617 """
618 pageTitle = 'Project'
619
622
625 help = """Describe a slave.
626 """
627 pageTitle = 'Slave'
628
629 - def __init__(self, status, slave_status):
634
636 if self.builders is None:
637
638 self.builders = []
639 for builderName in self.status.getBuilderNames():
640 if self.name in self.status.getBuilder(builderName).slavenames:
641 self.builders.append(builderName)
642 return self.builders
643
660
663 help = """List the registered slaves.
664 """
665 pageTitle = 'Slaves'
666
673
676 help = """Describe the sources for a SourceStamp.
677 """
678 pageTitle = 'SourceStamp'
679
680 - def __init__(self, status, source_stamp):
686
687
688
689
691 return self.source_stamp.asDict()
692
694 help = """Master metrics.
695 """
696 title = "Metrics"
697
705
709 """Retrieves all json data."""
710 help = """JSON status
711
712 Root page to give a fair amount of information in the current buildbot master
713 status. You may want to use a child instead to reduce the load on the server.
714
715 For help on any sub directory, use url /child/help
716 """
717 pageTitle = 'Buildbot JSON'
718
729
730 - def content(self, request):
731 result = JsonResource.content(self, request)
732
733 request.path = 'buildbot'
734 return result
735
752
753
754