| OLD | NEW |
| (Empty) |
| 1 | |
| 2 import urlparse, urllib, time, re | |
| 3 from zope.interface import Interface | |
| 4 from twisted.python import log | |
| 5 from twisted.web import html, resource | |
| 6 from buildbot.status import builder | |
| 7 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTI
ON | |
| 8 from buildbot import version, util | |
| 9 from buildbot.process.properties import Properties | |
| 10 | |
| 11 import datetime | |
| 12 | |
| 13 class ITopBox(Interface): | |
| 14 """I represent a box in the top row of the waterfall display: the one | |
| 15 which shows the status of the last build for each builder.""" | |
| 16 def getBox(self, request): | |
| 17 """Return a Box instance, which can produce a <td> cell. | |
| 18 """ | |
| 19 | |
| 20 class ICurrentBox(Interface): | |
| 21 """I represent the 'current activity' box, just above the builder name.""" | |
| 22 def getBox(self, status): | |
| 23 """Return a Box instance, which can produce a <td> cell. | |
| 24 """ | |
| 25 | |
| 26 class IBox(Interface): | |
| 27 """I represent a box in the waterfall display.""" | |
| 28 def getBox(self, request): | |
| 29 """Return a Box instance, which wraps an Event and can produce a <td> | |
| 30 cell. | |
| 31 """ | |
| 32 | |
| 33 class IHTMLLog(Interface): | |
| 34 pass | |
| 35 | |
| 36 css_classes = {SUCCESS: "success", | |
| 37 WARNINGS: "warnings", | |
| 38 FAILURE: "failure", | |
| 39 SKIPPED: "skipped", | |
| 40 EXCEPTION: "exception", | |
| 41 None: "", | |
| 42 } | |
| 43 | |
| 44 ROW_TEMPLATE = ''' | |
| 45 <div class="row"> | |
| 46 <span class="label">%(label)s</span> | |
| 47 <span class="field">%(field)s</span> | |
| 48 </div> | |
| 49 ''' | |
| 50 | |
| 51 def make_row(label, field): | |
| 52 """Create a name/value row for the HTML. | |
| 53 | |
| 54 `label` is plain text; it will be HTML-encoded. | |
| 55 | |
| 56 `field` is a bit of HTML structure; it will not be encoded in | |
| 57 any way. | |
| 58 """ | |
| 59 label = html.escape(label) | |
| 60 return ROW_TEMPLATE % {"label": label, "field": field} | |
| 61 | |
| 62 def make_name_user_passwd_form(useUserPasswd): | |
| 63 """helper function to create HTML prompt for 'name' when | |
| 64 C{useUserPasswd} is C{False} or 'username' / 'password' prompt | |
| 65 when C{True}.""" | |
| 66 | |
| 67 if useUserPasswd: | |
| 68 label = "Your username:" | |
| 69 else: | |
| 70 label = "Your name:" | |
| 71 data = make_row(label, '<input type="text" name="username" />') | |
| 72 if useUserPasswd: | |
| 73 data += make_row("Your password:", | |
| 74 '<input type="password" name="passwd" />') | |
| 75 return data | |
| 76 | |
| 77 def make_stop_form(stopURL, useUserPasswd, on_all=False, label="Build"): | |
| 78 if on_all: | |
| 79 data = """<form method="post" action="%s" class='command stopbuild'> | |
| 80 <p>To stop all builds, fill out the following fields and | |
| 81 push the 'Stop' button</p>\n""" % stopURL | |
| 82 else: | |
| 83 data = """<form method="post" action="%s" class='command stopbuild'> | |
| 84 <p>To stop this build, fill out the following fields and | |
| 85 push the 'Stop' button</p>\n""" % stopURL | |
| 86 data += make_name_user_passwd_form(useUserPasswd) | |
| 87 data += make_row("Reason for stopping build:", | |
| 88 "<input type='text' name='comments' />") | |
| 89 data += '<input type="submit" value="Stop %s" /></form>\n' % label | |
| 90 return data | |
| 91 | |
| 92 def make_extra_property_row(N): | |
| 93 """helper function to create the html for adding extra build | |
| 94 properties to a forced (or resubmitted) build. "N" is an integer | |
| 95 inserted into the form names so that more than one property can be | |
| 96 used in the form. | |
| 97 """ | |
| 98 prop_html = ''' | |
| 99 <div class="row">Property %(N)i | |
| 100 <span class="label">Name:</span> | |
| 101 <span class="field"><input type="text" name="property%(N)iname" /></span> | |
| 102 <span class="label">Value:</span> | |
| 103 <span class="field"><input type="text" name="property%(N)ivalue" /></span> | |
| 104 </div> | |
| 105 ''' % {"N": N} | |
| 106 return prop_html | |
| 107 | |
| 108 def make_force_build_form(forceURL, useUserPasswd, on_all=False): | |
| 109 if on_all: | |
| 110 data = """<form method="post" action="%s" class="command forcebuild"> | |
| 111 <p>To force a build on all Builders, fill out the following fields | |
| 112 and push the 'Force Build' button</p>""" % forceURL | |
| 113 else: | |
| 114 data = """<form method="post" action="%s" class="command forcebuild"> | |
| 115 <p>To force a build, fill out the following fields and | |
| 116 push the 'Force Build' button</p>""" % forceURL | |
| 117 return (data | |
| 118 + make_name_user_passwd_form(useUserPasswd) | |
| 119 + make_row("Reason for build:", | |
| 120 "<input type='text' name='comments' />") | |
| 121 + make_row("Branch to build:", | |
| 122 "<input type='text' name='branch' />") | |
| 123 + make_row("Revision to build:", | |
| 124 "<input type='text' name='revision' />") | |
| 125 + make_extra_property_row(1) | |
| 126 + make_extra_property_row(2) | |
| 127 + make_extra_property_row(3) | |
| 128 + '<input type="submit" value="Force Build" /></form>\n') | |
| 129 | |
| 130 def getAndCheckProperties(req): | |
| 131 """ | |
| 132 Fetch custom build properties from the HTTP request of a "Force build" or | |
| 133 "Resubmit build" HTML form. | |
| 134 Check the names for valid strings, and return None if a problem is found. | |
| 135 Return a new Properties object containing each property found in req. | |
| 136 """ | |
| 137 properties = Properties() | |
| 138 for i in (1,2,3): | |
| 139 pname = req.args.get("property%dname" % i, [""])[0] | |
| 140 pvalue = req.args.get("property%dvalue" % i, [""])[0] | |
| 141 if pname and pvalue: | |
| 142 if not re.match(r'^[\w\.\-\/\~:]*$', pname) \ | |
| 143 or not re.match(r'^[\w\.\-\/\~:]*$', pvalue): | |
| 144 log.msg("bad property name='%s', value='%s'" % (pname, pvalue)) | |
| 145 return None | |
| 146 properties.setProperty(pname, pvalue, "Force Build Form") | |
| 147 return properties | |
| 148 | |
| 149 def td(text="", parms={}, **props): | |
| 150 data = "" | |
| 151 data += " " | |
| 152 #if not props.has_key("border"): | |
| 153 # props["border"] = 1 | |
| 154 props.update(parms) | |
| 155 comment = props.get("comment", None) | |
| 156 if comment: | |
| 157 data += "<!-- %s -->" % comment | |
| 158 data += "<td" | |
| 159 class_ = props.get('class_', None) | |
| 160 if class_: | |
| 161 props["class"] = class_ | |
| 162 for prop in ("align", "colspan", "rowspan", "border", | |
| 163 "valign", "halign", "class"): | |
| 164 p = props.get(prop, None) | |
| 165 if p != None: | |
| 166 data += " %s=\"%s\"" % (prop, p) | |
| 167 data += ">" | |
| 168 if not text: | |
| 169 text = " " | |
| 170 if isinstance(text, list): | |
| 171 data += "<br />".join(text) | |
| 172 else: | |
| 173 data += text | |
| 174 data += "</td>\n" | |
| 175 return data | |
| 176 | |
| 177 def build_get_class(b): | |
| 178 """ | |
| 179 Return the class to use for a finished build or buildstep, | |
| 180 based on the result. | |
| 181 """ | |
| 182 # FIXME: this getResults duplicity might need to be fixed | |
| 183 result = b.getResults() | |
| 184 #print "THOMAS: result for b %r: %r" % (b, result) | |
| 185 if isinstance(b, builder.BuildStatus): | |
| 186 result = b.getResults() | |
| 187 elif isinstance(b, builder.BuildStepStatus): | |
| 188 result = b.getResults()[0] | |
| 189 # after forcing a build, b.getResults() returns ((None, []), []), ugh | |
| 190 if isinstance(result, tuple): | |
| 191 result = result[0] | |
| 192 else: | |
| 193 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b | |
| 194 | |
| 195 if result == None: | |
| 196 # FIXME: this happens when a buildstep is running ? | |
| 197 return "running" | |
| 198 return builder.Results[result] | |
| 199 | |
| 200 def path_to_root(request): | |
| 201 # /waterfall : ['waterfall'] -> '' | |
| 202 # /somewhere/lower : ['somewhere', 'lower'] -> '../' | |
| 203 # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../' | |
| 204 # / : [] -> '' | |
| 205 if request.prepath: | |
| 206 segs = len(request.prepath) - 1 | |
| 207 else: | |
| 208 segs = 0 | |
| 209 root = "../" * segs | |
| 210 return root | |
| 211 | |
| 212 def path_to_builder(request, builderstatus): | |
| 213 return (path_to_root(request) + | |
| 214 "builders/" + | |
| 215 urllib.quote(builderstatus.getName(), safe='')) | |
| 216 | |
| 217 def path_to_build(request, buildstatus): | |
| 218 return (path_to_builder(request, buildstatus.getBuilder()) + | |
| 219 "/builds/%d" % buildstatus.getNumber()) | |
| 220 | |
| 221 def path_to_step(request, stepstatus): | |
| 222 return (path_to_build(request, stepstatus.getBuild()) + | |
| 223 "/steps/%s" % urllib.quote(stepstatus.getName(), safe='')) | |
| 224 | |
| 225 def path_to_slave(request, slave): | |
| 226 return (path_to_root(request) + | |
| 227 "buildslaves/" + | |
| 228 urllib.quote(slave.getName(), safe='')) | |
| 229 | |
| 230 def path_to_change(request, change): | |
| 231 return (path_to_root(request) + | |
| 232 "changes/%s" % change.number) | |
| 233 | |
| 234 class Box: | |
| 235 # a Box wraps an Event. The Box has HTML <td> parameters that Events | |
| 236 # lack, and it has a base URL to which each File's name is relative. | |
| 237 # Events don't know about HTML. | |
| 238 spacer = False | |
| 239 def __init__(self, text=[], class_=None, urlbase=None, | |
| 240 **parms): | |
| 241 self.text = text | |
| 242 self.class_ = class_ | |
| 243 self.urlbase = urlbase | |
| 244 self.show_idle = 0 | |
| 245 if parms.has_key('show_idle'): | |
| 246 del parms['show_idle'] | |
| 247 self.show_idle = 1 | |
| 248 | |
| 249 self.parms = parms | |
| 250 # parms is a dict of HTML parameters for the <td> element that will | |
| 251 # represent this Event in the waterfall display. | |
| 252 | |
| 253 def td(self, **props): | |
| 254 props.update(self.parms) | |
| 255 text = self.text | |
| 256 if not text and self.show_idle: | |
| 257 text = ["[idle]"] | |
| 258 return td(text, props, class_=self.class_) | |
| 259 | |
| 260 | |
| 261 class HtmlResource(resource.Resource): | |
| 262 # this is a cheap sort of template thingy | |
| 263 contentType = "text/html; charset=UTF-8" | |
| 264 title = "Buildbot" | |
| 265 addSlash = False # adapted from Nevow | |
| 266 | |
| 267 def getChild(self, path, request): | |
| 268 if self.addSlash and path == "" and len(request.postpath) == 0: | |
| 269 return self | |
| 270 return resource.Resource.getChild(self, path, request) | |
| 271 | |
| 272 def render(self, request): | |
| 273 # tell the WebStatus about the HTTPChannel that got opened, so they | |
| 274 # can close it if we get reconfigured and the WebStatus goes away. | |
| 275 # They keep a weakref to this, since chances are good that it will be | |
| 276 # closed by the browser or by us before we get reconfigured. See | |
| 277 # ticket #102 for details. | |
| 278 if hasattr(request, "channel"): | |
| 279 # web.distrib.Request has no .channel | |
| 280 request.site.buildbot_service.registerChannel(request.channel) | |
| 281 | |
| 282 # Our pages no longer require that their URL end in a slash. Instead, | |
| 283 # they all use request.childLink() or some equivalent which takes the | |
| 284 # last path component into account. This clause is left here for | |
| 285 # historical and educational purposes. | |
| 286 if False and self.addSlash and request.prepath[-1] != '': | |
| 287 # this is intended to behave like request.URLPath().child('') | |
| 288 # but we need a relative URL, since we might be living behind a | |
| 289 # reverse proxy | |
| 290 # | |
| 291 # note that the Location: header (as used in redirects) are | |
| 292 # required to have absolute URIs, and my attempt to handle | |
| 293 # reverse-proxies gracefully violates rfc2616. This frequently | |
| 294 # works, but single-component paths sometimes break. The best | |
| 295 # strategy is to avoid these redirects whenever possible by using | |
| 296 # HREFs with trailing slashes, and only use the redirects for | |
| 297 # manually entered URLs. | |
| 298 url = request.prePathURL() | |
| 299 scheme, netloc, path, query, fragment = urlparse.urlsplit(url) | |
| 300 new_url = request.prepath[-1] + "/" | |
| 301 if query: | |
| 302 new_url += "?" + query | |
| 303 request.redirect(new_url) | |
| 304 return '' | |
| 305 | |
| 306 data = self.content(request) | |
| 307 if isinstance(data, unicode): | |
| 308 data = data.encode("utf-8") | |
| 309 request.setHeader("content-type", self.contentType) | |
| 310 if request.method == "HEAD": | |
| 311 request.setHeader("content-length", len(data)) | |
| 312 return '' | |
| 313 | |
| 314 # Make sure we get fresh pages. | |
| 315 now = datetime.datetime.utcnow() | |
| 316 expires = now + datetime.timedelta(seconds=60) | |
| 317 request.setHeader("Expires", expires.strftime("%a, %d %b %Y %H:%M:%S GMT
")) | |
| 318 request.setHeader("Pragma", "no-cache") | |
| 319 | |
| 320 return data | |
| 321 | |
| 322 def getStatus(self, request): | |
| 323 return request.site.buildbot_service.getStatus() | |
| 324 | |
| 325 def getControl(self, request): | |
| 326 return request.site.buildbot_service.getControl() | |
| 327 | |
| 328 def isUsingUserPasswd(self, request): | |
| 329 return request.site.buildbot_service.isUsingUserPasswd() | |
| 330 | |
| 331 def authUser(self, request): | |
| 332 user = request.args.get("username", ["<unknown>"])[0] | |
| 333 passwd = request.args.get("passwd", ["<no-password>"])[0] | |
| 334 if user == "<unknown>" or passwd == "<no-password>": | |
| 335 return False | |
| 336 return request.site.buildbot_service.authUser(user, passwd) | |
| 337 | |
| 338 def getChangemaster(self, request): | |
| 339 return request.site.buildbot_service.getChangeSvc() | |
| 340 | |
| 341 def path_to_root(self, request): | |
| 342 return path_to_root(request) | |
| 343 | |
| 344 def footer(self, status, req): | |
| 345 # TODO: this stuff should be generated by a template of some sort | |
| 346 projectURL = status.getProjectURL() | |
| 347 projectName = status.getProjectName() | |
| 348 data = '<hr /><div class="footer">\n' | |
| 349 | |
| 350 welcomeurl = self.path_to_root(req) + "index.html" | |
| 351 data += '[<a href="%s">welcome</a>]\n' % welcomeurl | |
| 352 data += "<br />\n" | |
| 353 | |
| 354 data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>' | |
| 355 data += "-%s " % version | |
| 356 if projectName: | |
| 357 data += "working for the " | |
| 358 if projectURL: | |
| 359 data += "<a href=\"%s\">%s</a> project." % (projectURL, | |
| 360 projectName) | |
| 361 else: | |
| 362 data += "%s project." % projectName | |
| 363 data += "<br />\n" | |
| 364 data += ("Page built: " + | |
| 365 time.strftime("%a %d %b %Y %H:%M:%S", | |
| 366 time.localtime(util.now())) | |
| 367 + "\n") | |
| 368 data += '</div>\n' | |
| 369 | |
| 370 return data | |
| 371 | |
| 372 def getTitle(self, request): | |
| 373 return self.title | |
| 374 | |
| 375 def fillTemplate(self, template, request): | |
| 376 s = request.site.buildbot_service | |
| 377 values = s.template_values.copy() | |
| 378 values['root'] = self.path_to_root(request) | |
| 379 # e.g. to reference the top-level 'buildbot.css' page, use | |
| 380 # "%(root)sbuildbot.css" | |
| 381 values['title'] = self.getTitle(request) | |
| 382 return template % values | |
| 383 | |
| 384 def content(self, request): | |
| 385 s = request.site.buildbot_service | |
| 386 data = "" | |
| 387 data += self.fillTemplate(s.header, request) | |
| 388 data += "<head>\n" | |
| 389 for he in s.head_elements: | |
| 390 data += " " + self.fillTemplate(he, request) + "\n" | |
| 391 data += self.head(request) | |
| 392 data += "</head>\n\n" | |
| 393 | |
| 394 data += '<body %s>\n' % " ".join(['%s="%s"' % (k,v) | |
| 395 for (k,v) in s.body_attrs.items()]) | |
| 396 data += self.body(request) | |
| 397 data += "</body>\n" | |
| 398 data += self.fillTemplate(s.footer, request) | |
| 399 return data | |
| 400 | |
| 401 def head(self, request): | |
| 402 return "" | |
| 403 | |
| 404 def body(self, request): | |
| 405 return "Dummy\n" | |
| 406 | |
| 407 class StaticHTML(HtmlResource): | |
| 408 def __init__(self, body, title): | |
| 409 HtmlResource.__init__(self) | |
| 410 self.bodyHTML = body | |
| 411 self.title = title | |
| 412 def body(self, request): | |
| 413 return self.bodyHTML | |
| 414 | |
| 415 MINUTE = 60 | |
| 416 HOUR = 60*MINUTE | |
| 417 DAY = 24*HOUR | |
| 418 WEEK = 7*DAY | |
| 419 MONTH = 30*DAY | |
| 420 | |
| 421 def plural(word, words, num): | |
| 422 if int(num) == 1: | |
| 423 return "%d %s" % (num, word) | |
| 424 else: | |
| 425 return "%d %s" % (num, words) | |
| 426 | |
| 427 def abbreviate_age(age): | |
| 428 if age <= 90: | |
| 429 return "%s ago" % plural("second", "seconds", age) | |
| 430 if age < 90*MINUTE: | |
| 431 return "about %s ago" % plural("minute", "minutes", age / MINUTE) | |
| 432 if age < DAY: | |
| 433 return "about %s ago" % plural("hour", "hours", age / HOUR) | |
| 434 if age < 2*WEEK: | |
| 435 return "about %s ago" % plural("day", "days", age / DAY) | |
| 436 if age < 2*MONTH: | |
| 437 return "about %s ago" % plural("week", "weeks", age / WEEK) | |
| 438 return "a long time ago" | |
| 439 | |
| 440 | |
| 441 class OneLineMixin: | |
| 442 LINE_TIME_FORMAT = "%b %d %H:%M" | |
| 443 | |
| 444 def get_line_values(self, req, build): | |
| 445 ''' | |
| 446 Collect the data needed for each line display | |
| 447 ''' | |
| 448 builder_name = build.getBuilder().getName() | |
| 449 results = build.getResults() | |
| 450 text = build.getText() | |
| 451 try: | |
| 452 rev = build.getProperty("got_revision") | |
| 453 if rev is None: | |
| 454 rev = "??" | |
| 455 except KeyError: | |
| 456 rev = "??" | |
| 457 rev = str(rev) | |
| 458 if len(rev) > 40: | |
| 459 rev = "version is too-long" | |
| 460 root = self.path_to_root(req) | |
| 461 css_class = css_classes.get(results, "") | |
| 462 values = {'class': css_class, | |
| 463 'builder_name': builder_name, | |
| 464 'buildnum': build.getNumber(), | |
| 465 'results': css_class, | |
| 466 'text': " ".join(build.getText()), | |
| 467 'buildurl': path_to_build(req, build), | |
| 468 'builderurl': path_to_builder(req, build.getBuilder()), | |
| 469 'rev': rev, | |
| 470 'time': time.strftime(self.LINE_TIME_FORMAT, | |
| 471 time.localtime(build.getTimes()[0])), | |
| 472 } | |
| 473 return values | |
| 474 | |
| 475 def make_line(self, req, build, include_builder=True): | |
| 476 ''' | |
| 477 Format and render a single line into HTML | |
| 478 ''' | |
| 479 values = self.get_line_values(req, build) | |
| 480 fmt_pieces = ['<font size="-1">(%(time)s)</font>', | |
| 481 'rev=[%(rev)s]', | |
| 482 '<span class="%(class)s">%(results)s</span>', | |
| 483 ] | |
| 484 if include_builder: | |
| 485 fmt_pieces.append('<a href="%(builderurl)s">%(builder_name)s</a>') | |
| 486 fmt_pieces.append('<a href="%(buildurl)s">#%(buildnum)d</a>:') | |
| 487 fmt_pieces.append('%(text)s') | |
| 488 data = " ".join(fmt_pieces) % values | |
| 489 return data | |
| 490 | |
| 491 def map_branches(branches): | |
| 492 # when the query args say "trunk", present that to things like | |
| 493 # IBuilderStatus.generateFinishedBuilds as None, since that's the | |
| 494 # convention in use. But also include 'trunk', because some VC systems | |
| 495 # refer to it that way. In the long run we should clean this up better, | |
| 496 # maybe with Branch objects or something. | |
| 497 if "trunk" in branches: | |
| 498 return branches + [None] | |
| 499 return branches | |
| OLD | NEW |