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 - numbuilds
46 - By default, only in memory cached builds are listed. You can as for more data
47 by using numbuilds=<number>.
48 - filter
49 - Filters out null, false, and empty string, list and dict. This reduce the
50 amount of useless data sent.
51 - callback
52 - Enable uses of JSONP as described in
53 http://en.wikipedia.org/wiki/JSONP. Note that
54 Access-Control-Allow-Origin:* is set in the HTTP response header so you
55 can use this in compatible browsers.
56 """
57
58 EXAMPLES = """\
59 - /json
60 - Root node, that *doesn't* mean all the data. Many things (like logs) must
61 be explicitly queried for performance reasons.
62 - /json/builders/
63 - All builders.
64 - /json/builders/<A_BUILDER>
65 - A specific builder as compact text.
66 - /json/builders/<A_BUILDER>/builds
67 - All *cached* builds.
68 - /json/builders/<A_BUILDER>/builds/_all
69 - All builds. Warning, reads all previous build data.
70 - /json/builders/<A_BUILDER>/builds/<A_BUILD>
71 - Where <A_BUILD> is either positive, a build number, or negative, a past
72 build.
73 - /json/builders/<A_BUILDER>/builds/-1/source_stamp/changes
74 - Build changes
75 - /json/builders/<A_BUILDER>/builds?select=-1&select=-2
76 - Two last builds on '<A_BUILDER>' builder.
77 - /json/builders/<A_BUILDER>/builds?select=-1/source_stamp/changes&select=-2/source_stamp/changes
78 - Changes of the two last builds on '<A_BUILDER>' builder.
79 - /json/builders/<A_BUILDER>/slaves
80 - Slaves associated to this builder.
81 - /json/builders/<A_BUILDER>?select=&select=slaves
82 - Builder information plus details information about its slaves. Neat eh?
83 - /json/slaves/<A_SLAVE>
84 - A specific slave.
85 - /json?select=slaves/<A_SLAVE>/&select=project&select=builders/<A_BUILDER>/builds/<A_BUILD>
86 - A selection of random unrelated stuff as an random example. :)
87 """
92
95 value = RequestArg(request, arg, default)
96 if value in (False, True):
97 return value
98 value = value.lower()
99 if value in ('1', 'true'):
100 return True
101 if value in ('0', 'false'):
102 return False
103
104 return default
105
108 """Returns a copy with None, False, "", [], () and {} removed.
109 Warning: converts tuple to list."""
110 if isinstance(data, (list, tuple)):
111
112 items = map(FilterOut, data)
113 if not filter(lambda x: not x in ('', False, None, [], {}, ()), items):
114 return None
115 return items
116 elif isinstance(data, dict):
117 return dict(filter(lambda x: not x[1] in ('', False, None, [], {}, ()),
118 [(k, FilterOut(v)) for (k, v) in data.iteritems()]))
119 else:
120 return data
121
124 """Base class for json data."""
125
126 contentType = "application/json"
127 cache_seconds = 60
128 help = None
129 pageTitle = None
130 level = 0
131
143
152
154 """Adds the resource's level for help links generation."""
155
156 def RecurseFix(res, level):
157 res.level = level + 1
158 for c in res.children.itervalues():
159 RecurseFix(c, res.level)
160
161 RecurseFix(res, self.level)
162 resource.Resource.putChild(self, name, res)
163
165 """Renders a HTTP GET at the http request level."""
166 d = defer.maybeDeferred(lambda : self.content(request))
167 def handle(data):
168 if isinstance(data, unicode):
169 data = data.encode("utf-8")
170 request.setHeader("Access-Control-Allow-Origin", "*")
171 if RequestArgToBool(request, 'as_text', False):
172 request.setHeader("content-type", 'text/plain')
173 else:
174 request.setHeader("content-type", self.contentType)
175 request.setHeader("content-disposition",
176 "attachment; filename=\"%s.json\"" % request.path)
177
178 if self.cache_seconds:
179 now = datetime.datetime.utcnow()
180 expires = now + datetime.timedelta(seconds=self.cache_seconds)
181 request.setHeader("Expires",
182 expires.strftime("%a, %d %b %Y %H:%M:%S GMT"))
183 request.setHeader("Pragma", "no-cache")
184 return data
185 d.addCallback(handle)
186 def ok(data):
187 request.write(data)
188 request.finish()
189 def fail(f):
190 request.processingFailed(f)
191 return None
192 d.addCallbacks(ok, fail)
193 return server.NOT_DONE_YET
194
195 @defer.deferredGenerator
196 - def content(self, request):
197 """Renders the json dictionaries."""
198
199 select = request.args.get('select')
200 as_text = RequestArgToBool(request, 'as_text', False)
201 filter_out = RequestArgToBool(request, 'filter', as_text)
202 compact = RequestArgToBool(request, 'compact', not as_text)
203 callback = request.args.get('callback')
204
205
206 if select is not None:
207 del request.args['select']
208
209 data = {}
210
211 select = [s.strip('/') for s in select]
212 select.sort(cmp=lambda x,y: cmp(x.count('/'), y.count('/')),
213 reverse=True)
214 for item in select:
215
216 node = data
217
218
219 child = self
220 prepath = request.prepath[:]
221 postpath = request.postpath[:]
222 request.postpath = filter(None, item.split('/'))
223 while request.postpath and not child.isLeaf:
224 pathElement = request.postpath.pop(0)
225 node[pathElement] = {}
226 node = node[pathElement]
227 request.prepath.append(pathElement)
228 child = child.getChildWithDefault(pathElement, request)
229
230
231
232 if hasattr(child, 'asDict'):
233 wfd = defer.waitForDeferred(
234 defer.maybeDeferred(lambda :
235 child.asDict(request)))
236 yield wfd
237 child_dict = wfd.getResult()
238 else:
239 child_dict = {
240 'error' : 'Not available',
241 }
242 node.update(child_dict)
243
244 request.prepath = prepath
245 request.postpath = postpath
246 else:
247 wfd = defer.waitForDeferred(
248 defer.maybeDeferred(lambda :
249 self.asDict(request)))
250 yield wfd
251 data = wfd.getResult()
252
253 if filter_out:
254 data = FilterOut(data)
255 if compact:
256 data = json.dumps(data, sort_keys=True, separators=(',',':'))
257 else:
258 data = json.dumps(data, sort_keys=True, indent=2)
259 if callback:
260
261 callback = callback[0]
262 if re.match(r'^[a-zA-Z$][a-zA-Z$0-9.]*$', callback):
263 data = '%s(%s);' % (callback, data)
264 yield data
265
266 @defer.deferredGenerator
268 """Generates the json dictionary.
269
270 By default, renders every childs."""
271 if self.children:
272 data = {}
273 for name in self.children:
274 child = self.getChildWithDefault(name, request)
275 if isinstance(child, JsonResource):
276 wfd = defer.waitForDeferred(
277 defer.maybeDeferred(lambda :
278 child.asDict(request)))
279 yield wfd
280 data[name] = wfd.getResult()
281
282 yield data
283 else:
284 raise NotImplementedError()
285
288 """Convert a string in a wiki-style format into HTML."""
289 indent = 0
290 in_item = False
291 output = []
292 for line in text.splitlines(False):
293 match = re.match(r'^( +)\- (.*)$', line)
294 if match:
295 if indent < len(match.group(1)):
296 output.append('<ul>')
297 indent = len(match.group(1))
298 elif indent > len(match.group(1)):
299 while indent > len(match.group(1)):
300 output.append('</ul>')
301 indent -= 2
302 if in_item:
303
304 output.append('</li>')
305 output.append('<li>')
306 in_item = True
307 line = match.group(2)
308 elif indent:
309 if line.startswith((' ' * indent) + ' '):
310
311 line = line.strip()
312 else:
313
314 if in_item:
315 output.append('</li>')
316 in_item = False
317 while indent > 0:
318 output.append('</ul>')
319 indent -= 2
320
321 if line.startswith('/'):
322 if not '?' in line:
323 line_full = line + '?as_text=1'
324 else:
325 line_full = line + '&as_text=1'
326 output.append('<a href="' + html.escape(line_full) + '">' +
327 html.escape(line) + '</a>')
328 else:
329 output.append(html.escape(line).replace(' ', ' '))
330 if not in_item:
331 output.append('<br>')
332
333 if in_item:
334 output.append('</li>')
335 while indent > 0:
336 output.append('</ul>')
337 indent -= 2
338 return '\n'.join(output)
339
342 - def __init__(self, text, pageTitle, parent_node):
347
348 - def content(self, request, cxt):
349 cxt['level'] = self.parent_node.level
350 cxt['text'] = ToHtml(self.text)
351 cxt['children'] = [ n for n in self.parent_node.children.keys() if n != 'help' ]
352 cxt['flags'] = ToHtml(FLAGS)
353 cxt['examples'] = ToHtml(EXAMPLES).replace(
354 'href="/json',
355 'href="%sjson' % (self.level * '../'))
356
357 template = request.site.buildbot_service.templates.get_template("jsonhelp.html")
358 return template.render(**cxt)
359
361 help = """Describe pending builds for a builder.
362 """
363 pageTitle = 'Builder'
364
365 - def __init__(self, status, builder_status):
368
375 d.addCallback(to_dict)
376 return d
377
380 help = """Describe a single builder.
381 """
382 pageTitle = 'Builder'
383
384 - def __init__(self, status, builder_status):
393
397
400 help = """List of all the builders defined on a master.
401 """
402 pageTitle = 'Builders'
403
410
413 help = """Describe the slaves attached to a single builder.
414 """
415 pageTitle = 'BuilderSlaves'
416
417 - def __init__(self, status, builder_status):
424
427 help = """Describe a single build.
428 """
429 pageTitle = 'Build'
430
431 - def __init__(self, status, build_status):
438
441
444 help = """All the builds that were run on a builder.
445 """
446 pageTitle = 'AllBuilds'
447
448 - def __init__(self, status, builder_status):
451
469
481
484 help = """Builds that were run on a builder.
485 """
486 pageTitle = 'Builds'
487
488 - def __init__(self, status, builder_status):
491
495
507
510 help = """A single build step.
511 """
512 pageTitle = 'BuildStep'
513
514 - def __init__(self, status, build_step_status):
518
519
521 return self.build_step_status.asDict()
522
525 help = """A list of build steps that occurred during a build.
526 """
527 pageTitle = 'BuildSteps'
528
529 - def __init__(self, status, build_status):
532
533
534
553
562
565 help = """Describe a single change that originates from a change source.
566 """
567 pageTitle = 'Change'
568
573
575 return self.change.asDict()
576
579 help = """List of changes.
580 """
581 pageTitle = 'Changes'
582
593
595 """Don't throw an exception when there is no child."""
596 if not self.children:
597 return {}
598 return JsonResource.asDict(self, request)
599
602 help = """Describe a change source.
603 """
604 pageTitle = 'ChangeSources'
605
607 result = {}
608 n = 0
609 for c in self.status.getChangeSources():
610
611 change = {}
612 change['description'] = c.describe()
613 result[n] = change
614 n += 1
615 return result
616
619 help = """Project-wide settings.
620 """
621 pageTitle = 'Project'
622
625
628 help = """Describe a slave.
629 """
630 pageTitle = 'Slave'
631
632 - def __init__(self, status, slave_status):
637
639 if self.builders is None:
640
641 self.builders = []
642 for builderName in self.status.getBuilderNames():
643 if self.name in self.status.getBuilder(builderName).slavenames:
644 self.builders.append(builderName)
645 return self.builders
646
665
668 help = """List the registered slaves.
669 """
670 pageTitle = 'Slaves'
671
678
681 help = """Describe the sources for a SourceStamp.
682 """
683 pageTitle = 'SourceStamp'
684
685 - def __init__(self, status, source_stamp):
691
692
693
694
696 return self.source_stamp.asDict()
697
699 help = """Master metrics.
700 """
701 title = "Metrics"
702
710
714 """Retrieves all json data."""
715 help = """JSON status
716
717 Root page to give a fair amount of information in the current buildbot master
718 status. You may want to use a child instead to reduce the load on the server.
719
720 For help on any sub directory, use url /child/help
721 """
722 pageTitle = 'Buildbot JSON'
723
734
735 - def content(self, request):
736 result = JsonResource.content(self, request)
737
738 request.path = 'buildbot'
739 return result
740
757
758
759