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
35 """
36
37 EXAMPLES = """\
38 - /json
39 - Root node, that *doesn't* mean all the data. Many things (like logs) must
40 be explicitly queried for performance reasons.
41 - /json/builders/
42 - All builders.
43 - /json/builders/<A_BUILDER>
44 - A specific builder as compact text.
45 - /json/builders/<A_BUILDER>/builds
46 - All *cached* builds.
47 - /json/builders/<A_BUILDER>/builds/_all
48 - All builds. Warning, reads all previous build data.
49 - /json/builders/<A_BUILDER>/builds/<A_BUILD>
50 - Where <A_BUILD> is either positive, a build number, or negative, a past
51 build.
52 - /json/builders/<A_BUILDER>/builds/-1/source_stamp/changes
53 - Build changes
54 - /json/builders/<A_BUILDER>/builds?select=-1&select=-2
55 - Two last builds on '<A_BUILDER>' builder.
56 - /json/builders/<A_BUILDER>/builds?select=-1/source_stamp/changes&select=-2/source_stamp/changes
57 - Changes of the two last builds on '<A_BUILDER>' builder.
58 - /json/builders/<A_BUILDER>/slaves
59 - Slaves associated to this builder.
60 - /json/builders/<A_BUILDER>?select=&select=slaves
61 - Builder information plus details information about its slaves. Neat eh?
62 - /json/slaves/<A_SLAVE>
63 - A specific slave.
64 - /json?select=slaves/<A_SLAVE>/&select=project&select=builders/<A_BUILDER>/builds/<A_BUILD>
65 - A selection of random unrelated stuff as an random example. :)
66 """
67
68
70 return request.args.get(arg, [default])[0]
71
72
74 value = RequestArg(request, arg, default)
75 if value in (False, True):
76 return value
77 value = value.lower()
78 if value in ('1', 'true'):
79 return True
80 if value in ('0', 'false'):
81 return False
82
83 return default
84
85
87 """Additional method for twisted.web.error.Error."""
88 result = {}
89 result['http_error'] = self.status
90 result['response'] = self.response
91 return result
92
93
95 """Additional method for twisted.web.error.Error."""
96 result = {}
97 result['http_error'] = self.code
98 result['response'] = self.brief
99 result['detail'] = self.detail
100 return result
101
102
103
104 error.Error.asDict = TwistedWebErrorAsDict
105 error.PageRedirect.asDict = TwistedWebErrorAsDict
106 error.ErrorPage.asDict = TwistedWebErrorPageAsDict
107 error.NoResource.asDict = TwistedWebErrorPageAsDict
108 error.ForbiddenResource.asDict = TwistedWebErrorPageAsDict
109
110
112 """Returns a copy with None, False, "", [], () and {} removed.
113 Warning: converts tuple to list."""
114 if isinstance(data, (list, tuple)):
115
116 items = map(FilterOut, data)
117 if not filter(lambda x: not x in ('', False, None, [], {}, ()), items):
118 return None
119 return items
120 elif isinstance(data, dict):
121 return dict(filter(lambda x: not x[1] in ('', False, None, [], {}, ()),
122 [(k, FilterOut(v)) for (k, v) in data.iteritems()]))
123 else:
124 return data
125
126
128 """Base class for json data."""
129
130 contentType = "application/json"
131 cache_seconds = 60
132 help = None
133 title = None
134 level = 0
135
147
149 """Adds transparent support for url ending with /"""
150 if path == "" and len(request.postpath) == 0:
151 return self
152
153 if self.children.has_key(path):
154 return self.children[path]
155 return self.getChild(path, request)
156
158 """Adds the resource's level for help links generation."""
159
160 def RecurseFix(res, level):
161 res.level = level + 1
162 for c in res.children.itervalues():
163 RecurseFix(c, res.level)
164
165 RecurseFix(res, self.level)
166 resource.Resource.putChild(self, name, res)
167
169 """Renders a HTTP GET at the http request level."""
170 data = self.content(request)
171 if isinstance(data, unicode):
172 data = data.encode("utf-8")
173 if RequestArgToBool(request, 'as_text', False):
174 request.setHeader("content-type", 'text/plain')
175 else:
176 request.setHeader("content-type", self.contentType)
177 request.setHeader("content-disposition",
178 "attachment; filename=\"%s.json\"" % request.path)
179
180 if self.cache_seconds:
181 now = datetime.datetime.utcnow()
182 expires = now + datetime.timedelta(seconds=self.cache_seconds)
183 request.setHeader("Expires",
184 expires.strftime("%a, %d %b %Y %H:%M:%S GMT"))
185 request.setHeader("Pragma", "no-cache")
186 return data
187
188 - def content(self, request):
189 """Renders the json dictionaries."""
190
191 select = request.args.get('select')
192 if select is not None:
193 del request.args['select']
194
195 data = {}
196
197 select = [s.strip('/') for s in select]
198 select.sort(cmp=lambda x,y: cmp(x.count('/'), y.count('/')),
199 reverse=True)
200 for item in select:
201
202 node = data
203
204
205 child = self
206 prepath = request.prepath[:]
207 postpath = request.postpath[:]
208 request.postpath = filter(None, item.split('/'))
209 while request.postpath and not child.isLeaf:
210 pathElement = request.postpath.pop(0)
211 node[pathElement] = {}
212 node = node[pathElement]
213 request.prepath.append(pathElement)
214 child = child.getChildWithDefault(pathElement, request)
215 node.update(child.asDict(request))
216 request.prepath = prepath
217 request.postpath = postpath
218 else:
219 data = self.asDict(request)
220 as_text = RequestArgToBool(request, 'as_text', False)
221 filter_out = RequestArgToBool(request, 'filter', as_text)
222 if filter_out:
223 data = FilterOut(data)
224 if RequestArgToBool(request, 'compact', not as_text):
225 return json.dumps(data, sort_keys=True, separators=(',',':'))
226 else:
227 return json.dumps(data, sort_keys=True, indent=2)
228
230 """Generates the json dictionary.
231
232 By default, renders every childs."""
233 if self.children:
234 data = {}
235 for name in self.children:
236 child = self.getChildWithDefault(name, request)
237 if isinstance(child, JsonResource):
238 data[name] = child.asDict(request)
239
240 return data
241 else:
242 raise NotImplementedError()
243
244
246 """Convert a string in a wiki-style format into HTML."""
247 indent = 0
248 in_item = False
249 output = []
250 for line in text.splitlines(False):
251 match = re.match(r'^( +)\- (.*)$', line)
252 if match:
253 if indent < len(match.group(1)):
254 output.append('<ul>')
255 indent = len(match.group(1))
256 elif indent > len(match.group(1)):
257 while indent > len(match.group(1)):
258 output.append('</ul>')
259 indent -= 2
260 if in_item:
261
262 output.append('</li>')
263 output.append('<li>')
264 in_item = True
265 line = match.group(2)
266 elif indent:
267 if line.startswith((' ' * indent) + ' '):
268
269 line = line.strip()
270 else:
271
272 if in_item:
273 output.append('</li>')
274 in_item = False
275 while indent > 0:
276 output.append('</ul>')
277 indent -= 2
278
279 if line.startswith('/'):
280 if not '?' in line:
281 line_full = line + '?as_text=1'
282 else:
283 line_full = line + '&as_text=1'
284 output.append('<a href="' + html.escape(line_full) + '">' +
285 html.escape(line) + '</a>')
286 else:
287 output.append(html.escape(line).replace(' ', ' '))
288 if not in_item:
289 output.append('<br>')
290
291 if in_item:
292 output.append('</li>')
293 while indent > 0:
294 output.append('</ul>')
295 indent -= 2
296 return '\n'.join(output)
297
298
300 - def __init__(self, text, title, parent_node):
305
306 - def content(self, request, cxt):
307 cxt['level'] = self.parent_node.level
308 cxt['text'] = ToHtml(self.text)
309 cxt['children'] = [ n for n in self.parent_node.children.keys() if n != 'help' ]
310 cxt['flags'] = ToHtml(FLAGS)
311 cxt['examples'] = ToHtml(EXAMPLES).replace(
312 'href="/json',
313 'href="%sjson' % (self.level * '../'))
314
315 template = request.site.buildbot_service.templates.get_template("jsonhelp.html")
316 return template.render(**cxt)
317
319 help = """Describe a single builder.
320 """
321 title = 'Builder'
322
323 - def __init__(self, status, builder_status):
329
331
332 return self.builder_status.asDict()
333
334
336 help = """List of all the builders defined on a master.
337 """
338 title = 'Builders'
339
346
347
349 help = """Describe the slaves attached to a single builder.
350 """
351 title = 'BuilderSlaves'
352
353 - def __init__(self, status, builder_status):
360
361
363 help = """Describe a single build.
364 """
365 title = 'Build'
366
367 - def __init__(self, status, build_status):
374
377
378
380 help = """All the builds that were run on a builder.
381 """
382 title = 'AllBuilds'
383
384 - def __init__(self, status, builder_status):
387
405
417
418
420 help = """Builds that were run on a builder.
421 """
422 title = 'Builds'
423
424 - def __init__(self, status, builder_status):
427
431
433
434
435
436
437 builds = dict([
438 (int(file), None)
439 for file in os.listdir(self.builder_status.basedir)
440 if _IS_INT.match(file)
441 ])
442 return builds
443
444
446 help = """A single build step.
447 """
448 title = 'BuildStep'
449
450 - def __init__(self, status, build_step_status):
454
455
457 return self.build_step_status.asDict()
458
459
461 help = """A list of build steps that occurred during a build.
462 """
463 title = 'BuildSteps'
464
465 - def __init__(self, status, build_status):
468
469
470
489
498
499
501 help = """Describe a single change that originates from a change source.
502 """
503 title = 'Change'
504
509
511 return self.change.asDict()
512
513
515 help = """List of changes.
516 """
517 title = 'Changes'
518
531
533 """Don't throw an exception when there is no child."""
534 if not self.children:
535 return {}
536 return JsonResource.asDict(self, request)
537
538
540 help = """Describe a change source.
541 """
542 title = 'ChangeSources'
543
545 result = {}
546 n = 0
547 for c in self.status.getChangeSources():
548
549 change = {}
550 change['description'] = c.describe()
551 result[n] = change
552 n += 1
553 return result
554
555
557 help = """Project-wide settings.
558 """
559 title = 'Project'
560
563
564
566 help = """Describe a slave.
567 """
568 title = 'Slave'
569
570 - def __init__(self, status, slave_status):
575
577 if self.builders is None:
578
579 self.builders = []
580 for builderName in self.status.getBuilderNames():
581 if self.name in self.status.getBuilder(builderName).slavenames:
582 self.builders.append(builderName)
583 return self.builders
584
601
602
604 help = """List the registered slaves.
605 """
606 title = 'Slaves'
607
614
615
617 help = """Describe the sources for a BuildRequest.
618 """
619 title = 'SourceStamp'
620
621 - def __init__(self, status, source_stamp):
627
628
629
630
632 return self.source_stamp.asDict()
633
634
636 """Retrieves all json data."""
637 help = """JSON status
638
639 Root page to give a fair amount of information in the current buildbot master
640 status. You may want to use a child instead to reduce the load on the server.
641
642 For help on any sub directory, use url /child/help
643 """
644 title = 'Buildbot JSON'
645
655
656 - def content(self, request):
657 result = JsonResource.content(self, request)
658
659 request.path = 'buildbot'
660 return result
661
678
679
680