1
2
3
4 """Simple JSON exporter."""
5
6 import datetime
7 import os
8 import re
9
10 from twisted.web import error, html, resource
11
12 from buildbot.status.web.base import HtmlResource
13 from buildbot.util import json
14
15
16 _IS_INT = re.compile('^[-+]?\d+$')
17
18
19 FLAGS = """\
20 - as_text
21 - By default, application/json is used. Setting as_text=1 change the type
22 to text/plain and implicitly sets compact=0 and filter=1. Mainly useful to
23 look at the result in a web browser.
24 - compact
25 - By default, the json data is compact and defaults to 1. For easier to read
26 indented output, set compact=0.
27 - select
28 - By default, most children data is listed. You can do a random selection
29 of data by using select=<sub-url> multiple times to coagulate data.
30 "select=" includes the actual url otherwise it is skipped.
31 - filter
32 - Filters out null, false, and empty string, list and dict. This reduce the
33 amount of useless data sent.
34 - callback
35 - Enable uses of JSONP as described in
36 http://en.wikipedia.org/wiki/JSON#JSONP. Note that
37 Access-Control-Allow-Origin:* is set in the HTTP response header so you
38 can use this in compatible browsers.
39 """
40
41 EXAMPLES = """\
42 - /json
43 - Root node, that *doesn't* mean all the data. Many things (like logs) must
44 be explicitly queried for performance reasons.
45 - /json/builders/
46 - All builders.
47 - /json/builders/<A_BUILDER>
48 - A specific builder as compact text.
49 - /json/builders/<A_BUILDER>/builds
50 - All *cached* builds.
51 - /json/builders/<A_BUILDER>/builds/_all
52 - All builds. Warning, reads all previous build data.
53 - /json/builders/<A_BUILDER>/builds/<A_BUILD>
54 - Where <A_BUILD> is either positive, a build number, or negative, a past
55 build.
56 - /json/builders/<A_BUILDER>/builds/-1/source_stamp/changes
57 - Build changes
58 - /json/builders/<A_BUILDER>/builds?select=-1&select=-2
59 - Two last builds on '<A_BUILDER>' builder.
60 - /json/builders/<A_BUILDER>/builds?select=-1/source_stamp/changes&select=-2/source_stamp/changes
61 - Changes of the two last builds on '<A_BUILDER>' builder.
62 - /json/builders/<A_BUILDER>/slaves
63 - Slaves associated to this builder.
64 - /json/builders/<A_BUILDER>?select=&select=slaves
65 - Builder information plus details information about its slaves. Neat eh?
66 - /json/slaves/<A_SLAVE>
67 - A specific slave.
68 - /json?select=slaves/<A_SLAVE>/&select=project&select=builders/<A_BUILDER>/builds/<A_BUILD>
69 - A selection of random unrelated stuff as an random example. :)
70 """
71
72
74 return request.args.get(arg, [default])[0]
75
76
78 value = RequestArg(request, arg, default)
79 if value in (False, True):
80 return value
81 value = value.lower()
82 if value in ('1', 'true'):
83 return True
84 if value in ('0', 'false'):
85 return False
86
87 return default
88
89
91 """Additional method for twisted.web.error.Error."""
92 result = {}
93 result['http_error'] = self.status
94 result['response'] = self.response
95 return result
96
97
99 """Additional method for twisted.web.error.Error."""
100 result = {}
101 result['http_error'] = self.code
102 result['response'] = self.brief
103 result['detail'] = self.detail
104 return result
105
106
107
108 error.Error.asDict = TwistedWebErrorAsDict
109 error.PageRedirect.asDict = TwistedWebErrorAsDict
110 error.ErrorPage.asDict = TwistedWebErrorPageAsDict
111 error.NoResource.asDict = TwistedWebErrorPageAsDict
112 error.ForbiddenResource.asDict = TwistedWebErrorPageAsDict
113
114
116 """Returns a copy with None, False, "", [], () and {} removed.
117 Warning: converts tuple to list."""
118 if isinstance(data, (list, tuple)):
119
120 items = map(FilterOut, data)
121 if not filter(lambda x: not x in ('', False, None, [], {}, ()), items):
122 return None
123 return items
124 elif isinstance(data, dict):
125 return dict(filter(lambda x: not x[1] in ('', False, None, [], {}, ()),
126 [(k, FilterOut(v)) for (k, v) in data.iteritems()]))
127 else:
128 return data
129
130
132 """Base class for json data."""
133
134 contentType = "application/json"
135 cache_seconds = 60
136 help = None
137 title = None
138 level = 0
139
151
160
162 """Adds the resource's level for help links generation."""
163
164 def RecurseFix(res, level):
165 res.level = level + 1
166 for c in res.children.itervalues():
167 RecurseFix(c, res.level)
168
169 RecurseFix(res, self.level)
170 resource.Resource.putChild(self, name, res)
171
173 """Renders a HTTP GET at the http request level."""
174 data = self.content(request)
175 if isinstance(data, unicode):
176 data = data.encode("utf-8")
177 request.setHeader("Access-Control-Allow-Origin", "*")
178 if RequestArgToBool(request, 'as_text', False):
179 request.setHeader("content-type", 'text/plain')
180 else:
181 request.setHeader("content-type", self.contentType)
182 request.setHeader("content-disposition",
183 "attachment; filename=\"%s.json\"" % request.path)
184
185 if self.cache_seconds:
186 now = datetime.datetime.utcnow()
187 expires = now + datetime.timedelta(seconds=self.cache_seconds)
188 request.setHeader("Expires",
189 expires.strftime("%a, %d %b %Y %H:%M:%S GMT"))
190 request.setHeader("Pragma", "no-cache")
191 return data
192
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 node.update(child.asDict(request))
227 request.prepath = prepath
228 request.postpath = postpath
229 else:
230 data = self.asDict(request)
231 if filter_out:
232 data = FilterOut(data)
233 if compact:
234 data = json.dumps(data, sort_keys=True, separators=(',',':'))
235 else:
236 data = json.dumps(data, sort_keys=True, indent=2)
237 if callback:
238
239 callback = callback[0]
240 if re.match(r'^[a-zA-Z$][a-zA-Z$0-9.]*$', callback):
241 data = '%s(%s);' % (callback, data)
242 return data
243
245 """Generates the json dictionary.
246
247 By default, renders every childs."""
248 if self.children:
249 data = {}
250 for name in self.children:
251 child = self.getChildWithDefault(name, request)
252 if isinstance(child, JsonResource):
253 data[name] = child.asDict(request)
254
255 return data
256 else:
257 raise NotImplementedError()
258
259
261 """Convert a string in a wiki-style format into HTML."""
262 indent = 0
263 in_item = False
264 output = []
265 for line in text.splitlines(False):
266 match = re.match(r'^( +)\- (.*)$', line)
267 if match:
268 if indent < len(match.group(1)):
269 output.append('<ul>')
270 indent = len(match.group(1))
271 elif indent > len(match.group(1)):
272 while indent > len(match.group(1)):
273 output.append('</ul>')
274 indent -= 2
275 if in_item:
276
277 output.append('</li>')
278 output.append('<li>')
279 in_item = True
280 line = match.group(2)
281 elif indent:
282 if line.startswith((' ' * indent) + ' '):
283
284 line = line.strip()
285 else:
286
287 if in_item:
288 output.append('</li>')
289 in_item = False
290 while indent > 0:
291 output.append('</ul>')
292 indent -= 2
293
294 if line.startswith('/'):
295 if not '?' in line:
296 line_full = line + '?as_text=1'
297 else:
298 line_full = line + '&as_text=1'
299 output.append('<a href="' + html.escape(line_full) + '">' +
300 html.escape(line) + '</a>')
301 else:
302 output.append(html.escape(line).replace(' ', ' '))
303 if not in_item:
304 output.append('<br>')
305
306 if in_item:
307 output.append('</li>')
308 while indent > 0:
309 output.append('</ul>')
310 indent -= 2
311 return '\n'.join(output)
312
313
315 - def __init__(self, text, title, parent_node):
320
321 - def content(self, request, cxt):
322 cxt['level'] = self.parent_node.level
323 cxt['text'] = ToHtml(self.text)
324 cxt['children'] = [ n for n in self.parent_node.children.keys() if n != 'help' ]
325 cxt['flags'] = ToHtml(FLAGS)
326 cxt['examples'] = ToHtml(EXAMPLES).replace(
327 'href="/json',
328 'href="%sjson' % (self.level * '../'))
329
330 template = request.site.buildbot_service.templates.get_template("jsonhelp.html")
331 return template.render(**cxt)
332
334 help = """Describe a single builder.
335 """
336 title = 'Builder'
337
338 - def __init__(self, status, builder_status):
344
346
347 return self.builder_status.asDict()
348
349
351 help = """List of all the builders defined on a master.
352 """
353 title = 'Builders'
354
361
362
364 help = """Describe the slaves attached to a single builder.
365 """
366 title = 'BuilderSlaves'
367
368 - def __init__(self, status, builder_status):
375
376
378 help = """Describe a single build.
379 """
380 title = 'Build'
381
382 - def __init__(self, status, build_status):
389
392
393
395 help = """All the builds that were run on a builder.
396 """
397 title = 'AllBuilds'
398
399 - def __init__(self, status, builder_status):
402
420
432
433
435 help = """Builds that were run on a builder.
436 """
437 title = 'Builds'
438
439 - def __init__(self, status, builder_status):
442
446
448
449
450
451
452 builds = dict([
453 (int(file), None)
454 for file in os.listdir(self.builder_status.basedir)
455 if _IS_INT.match(file)
456 ])
457 return builds
458
459
461 help = """A single build step.
462 """
463 title = 'BuildStep'
464
465 - def __init__(self, status, build_step_status):
469
470
472 return self.build_step_status.asDict()
473
474
476 help = """A list of build steps that occurred during a build.
477 """
478 title = 'BuildSteps'
479
480 - def __init__(self, status, build_status):
483
484
485
504
513
514
516 help = """Describe a single change that originates from a change source.
517 """
518 title = 'Change'
519
524
526 return self.change.asDict()
527
528
530 help = """List of changes.
531 """
532 title = 'Changes'
533
546
548 """Don't throw an exception when there is no child."""
549 if not self.children:
550 return {}
551 return JsonResource.asDict(self, request)
552
553
555 help = """Describe a change source.
556 """
557 title = 'ChangeSources'
558
560 result = {}
561 n = 0
562 for c in self.status.getChangeSources():
563
564 change = {}
565 change['description'] = c.describe()
566 result[n] = change
567 n += 1
568 return result
569
570
572 help = """Project-wide settings.
573 """
574 title = 'Project'
575
578
579
581 help = """Describe a slave.
582 """
583 title = 'Slave'
584
585 - def __init__(self, status, slave_status):
590
592 if self.builders is None:
593
594 self.builders = []
595 for builderName in self.status.getBuilderNames():
596 if self.name in self.status.getBuilder(builderName).slavenames:
597 self.builders.append(builderName)
598 return self.builders
599
616
617
619 help = """List the registered slaves.
620 """
621 title = 'Slaves'
622
629
630
632 help = """Describe the sources for a BuildRequest.
633 """
634 title = 'SourceStamp'
635
636 - def __init__(self, status, source_stamp):
642
643
644
645
647 return self.source_stamp.asDict()
648
649
651 """Retrieves all json data."""
652 help = """JSON status
653
654 Root page to give a fair amount of information in the current buildbot master
655 status. You may want to use a child instead to reduce the load on the server.
656
657 For help on any sub directory, use url /child/help
658 """
659 title = 'Buildbot JSON'
660
670
671 - def content(self, request):
672 result = JsonResource.content(self, request)
673
674 request.path = 'buildbot'
675 return result
676
693
694
695