| OLD | NEW |
| (Empty) |
| 1 | |
| 2 from twisted.web.error import NoResource | |
| 3 from twisted.web import html, static | |
| 4 from twisted.web.util import Redirect | |
| 5 | |
| 6 import re, urllib, time | |
| 7 from twisted.python import log | |
| 8 from buildbot import interfaces | |
| 9 from buildbot.status.web.base import HtmlResource, make_row, \ | |
| 10 make_force_build_form, OneLineMixin, path_to_build, path_to_slave, \ | |
| 11 path_to_builder, path_to_change, getAndCheckProperties | |
| 12 from buildbot.process.base import BuildRequest | |
| 13 from buildbot.process.properties import Properties | |
| 14 from buildbot.sourcestamp import SourceStamp | |
| 15 | |
| 16 from buildbot.status.web.build import BuildsResource, StatusResourceBuild | |
| 17 from buildbot import util | |
| 18 | |
| 19 # /builders/$builder | |
| 20 class StatusResourceBuilder(HtmlResource, OneLineMixin): | |
| 21 addSlash = True | |
| 22 | |
| 23 def __init__(self, builder_status, builder_control): | |
| 24 HtmlResource.__init__(self) | |
| 25 self.builder_status = builder_status | |
| 26 self.builder_control = builder_control | |
| 27 | |
| 28 def getTitle(self, request): | |
| 29 return "Buildbot: %s" % html.escape(self.builder_status.getName()) | |
| 30 | |
| 31 def build_line(self, build, req): | |
| 32 buildnum = build.getNumber() | |
| 33 buildurl = path_to_build(req, build) | |
| 34 data = '<a href="%s">#%d</a> ' % (buildurl, buildnum) | |
| 35 | |
| 36 when = build.getETA() | |
| 37 if when is not None: | |
| 38 when_time = time.strftime("%H:%M:%S", | |
| 39 time.localtime(time.time() + when)) | |
| 40 data += "ETA %ds (%s) " % (when, when_time) | |
| 41 step = build.getCurrentStep() | |
| 42 if step: | |
| 43 data += "[%s]" % step.getName() | |
| 44 else: | |
| 45 data += "[waiting for Lock]" | |
| 46 # TODO: is this necessarily the case? | |
| 47 | |
| 48 if self.builder_control is not None: | |
| 49 stopURL = path_to_build(req, build) + '/stop' | |
| 50 data += ''' | |
| 51 <form method="post" action="%s" class="command stopbuild" style="display:inline"
> | |
| 52 <input type="submit" value="Stop Build" /> | |
| 53 </form>''' % stopURL | |
| 54 return data | |
| 55 | |
| 56 def request_line(self, build_request, req): | |
| 57 when = time.strftime("%b %d %H:%M:%S", time.localtime(build_request.getS
ubmitTime())) | |
| 58 delay = util.formatInterval(util.now() - build_request.getSubmitTime()) | |
| 59 changes = build_request.source.changes | |
| 60 if changes: | |
| 61 change_strings = [] | |
| 62 for c in changes: | |
| 63 change_strings.append("<a href=\"%s\">%s</a>" % (path_to_change(
req, c), c.who)) | |
| 64 if len(change_strings) == 1: | |
| 65 reason = "change by %s" % change_strings[0] | |
| 66 else: | |
| 67 reason = "changes by %s" % ", ".join(change_strings) | |
| 68 elif build_request.source.revision: | |
| 69 reason = build_request.source.revision | |
| 70 else: | |
| 71 reason = "no changes specified" | |
| 72 | |
| 73 if self.builder_control is not None: | |
| 74 cancelURL = path_to_builder(req, self.builder_status) + '/cancelbuil
d' | |
| 75 cancelButton = ''' | |
| 76 <form action="%s" class="command cancelbuild" style="display:inline" method="pos
t"> | |
| 77 <input type="hidden" name="id" value="%s" /> | |
| 78 <input type="submit" value="Cancel Build" /> | |
| 79 </form>''' % (cancelURL, id(build_request)) | |
| 80 else: | |
| 81 cancelButton = "" | |
| 82 return "<font size=\"-1\">(%s, waiting %s)</font>%s%s" % (when, delay, c
ancelButton, reason) | |
| 83 | |
| 84 def body(self, req): | |
| 85 b = self.builder_status | |
| 86 control = self.builder_control | |
| 87 status = self.getStatus(req) | |
| 88 | |
| 89 slaves = b.getSlaves() | |
| 90 connected_slaves = [s for s in slaves if s.isConnected()] | |
| 91 | |
| 92 projectName = status.getProjectName() | |
| 93 | |
| 94 data = '<a href="%s">%s</a>\n' % (self.path_to_root(req), projectName) | |
| 95 | |
| 96 data += "<h1>Builder: %s</h1>\n" % html.escape(b.getName()) | |
| 97 | |
| 98 # the first section shows builds which are currently running, if any. | |
| 99 | |
| 100 current = b.getCurrentBuilds() | |
| 101 if current: | |
| 102 data += "<h2>Currently Building:</h2>\n" | |
| 103 data += "<ul>\n" | |
| 104 for build in current: | |
| 105 data += " <li>" + self.build_line(build, req) + "</li>\n" | |
| 106 data += "</ul>\n" | |
| 107 else: | |
| 108 data += "<h2>no current builds</h2>\n" | |
| 109 | |
| 110 pending = b.getPendingBuilds() | |
| 111 if pending: | |
| 112 data += "<h2>Pending Builds:</h2>\n" | |
| 113 data += "<ul>\n" | |
| 114 for request in pending: | |
| 115 data += " <li>" + self.request_line(request, req) + "</li>\n" | |
| 116 data += "</ul>\n" | |
| 117 | |
| 118 cancelURL = path_to_builder(req, self.builder_status) + '/cancelbuil
d' | |
| 119 if self.builder_control is not None: | |
| 120 data += ''' | |
| 121 <form action="%s" class="command cancelbuild" style="display:inline" method="pos
t"> | |
| 122 <input type="hidden" name="id" value="all" /> | |
| 123 <input type="submit" value="Cancel All" /> | |
| 124 </form>''' % cancelURL | |
| 125 else: | |
| 126 data += "<h2>no pending builds</h2>\n" | |
| 127 | |
| 128 # Then a section with the last 5 builds, with the most recent build | |
| 129 # distinguished from the rest. | |
| 130 | |
| 131 data += "<h2>Recent Builds:</h2>\n" | |
| 132 data += "(<a href=\"%s\">view in waterfall</a>)\n" % (self.path_to_root(
req)+"waterfall?show="+html.escape(b.getName())) | |
| 133 data += "<ul>\n" | |
| 134 numbuilds = int(req.args.get('numbuilds', ['5'])[0]) | |
| 135 for i,build in enumerate(b.generateFinishedBuilds(num_builds=int(numbuil
ds))): | |
| 136 data += " <li>" + self.make_line(req, build, False) + "</li>\n" | |
| 137 if i == 0: | |
| 138 data += "<br />\n" # separator | |
| 139 # TODO: or empty list? | |
| 140 data += "</ul>\n" | |
| 141 | |
| 142 | |
| 143 data += "<h2>Buildslaves:</h2>\n" | |
| 144 data += "<ol>\n" | |
| 145 for slave in slaves: | |
| 146 slaveurl = path_to_slave(req, slave) | |
| 147 data += "<li><b><a href=\"%s\">%s</a></b>: " % (html.escape(slaveurl
), html.escape(slave.getName())) | |
| 148 if slave.isConnected(): | |
| 149 data += "CONNECTED\n" | |
| 150 if slave.getAdmin(): | |
| 151 data += make_row("Admin:", html.escape(slave.getAdmin())) | |
| 152 if slave.getHost(): | |
| 153 data += "<span class='label'>Host info:</span>\n" | |
| 154 data += html.PRE(html.escape(slave.getHost())) | |
| 155 else: | |
| 156 data += ("NOT CONNECTED\n") | |
| 157 data += "</li>\n" | |
| 158 data += "</ol>\n" | |
| 159 | |
| 160 if control is not None and connected_slaves: | |
| 161 forceURL = path_to_builder(req, b) + '/force' | |
| 162 data += make_force_build_form(forceURL, self.isUsingUserPasswd(req)) | |
| 163 elif control is not None: | |
| 164 data += """ | |
| 165 <p>All buildslaves appear to be offline, so it's not possible | |
| 166 to force this build to execute at this time.</p> | |
| 167 """ | |
| 168 | |
| 169 if control is not None: | |
| 170 pingURL = path_to_builder(req, b) + '/ping' | |
| 171 data += """ | |
| 172 <form method="post" action="%s" class='command pingbuilder'> | |
| 173 <p>To ping the buildslave(s), push the 'Ping' button</p> | |
| 174 | |
| 175 <input type="submit" value="Ping Builder" /> | |
| 176 </form> | |
| 177 """ % pingURL | |
| 178 | |
| 179 data += self.footer(status, req) | |
| 180 | |
| 181 return data | |
| 182 | |
| 183 def force(self, req): | |
| 184 """ | |
| 185 | |
| 186 Custom properties can be passed from the web form. To do | |
| 187 this, subclass this class, overriding the force() method. You | |
| 188 can then determine the properties (usually from form values, | |
| 189 by inspecting req.args), then pass them to this superclass | |
| 190 force method. | |
| 191 | |
| 192 """ | |
| 193 name = req.args.get("username", ["<unknown>"])[0] | |
| 194 reason = req.args.get("comments", ["<no reason specified>"])[0] | |
| 195 branch = req.args.get("branch", [""])[0] | |
| 196 revision = req.args.get("revision", [""])[0] | |
| 197 | |
| 198 r = "The web-page 'force build' button was pressed by '%s': %s\n" \ | |
| 199 % (html.escape(name), html.escape(reason)) | |
| 200 log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'" | |
| 201 " by user '%s'" % (self.builder_status.getName(), branch, | |
| 202 revision, name)) | |
| 203 | |
| 204 if not self.builder_control: | |
| 205 # TODO: tell the web user that their request was denied | |
| 206 log.msg("but builder control is disabled") | |
| 207 return Redirect("..") | |
| 208 | |
| 209 if self.isUsingUserPasswd(req): | |
| 210 if not self.authUser(req): | |
| 211 return Redirect("../../authfail") | |
| 212 | |
| 213 # keep weird stuff out of the branch revision, and property strings. | |
| 214 # TODO: centralize this somewhere. | |
| 215 if not re.match(r'^[\w\.\-\/]*$', branch): | |
| 216 log.msg("bad branch '%s'" % branch) | |
| 217 return Redirect("..") | |
| 218 if not re.match(r'^[\w\.\-\/]*$', revision): | |
| 219 log.msg("bad revision '%s'" % revision) | |
| 220 return Redirect("..") | |
| 221 properties = getAndCheckProperties(req) | |
| 222 if properties is None: | |
| 223 return Redirect("..") | |
| 224 if not branch: | |
| 225 branch = None | |
| 226 if not revision: | |
| 227 revision = None | |
| 228 | |
| 229 # TODO: if we can authenticate that a particular User pushed the | |
| 230 # button, use their name instead of None, so they'll be informed of | |
| 231 # the results. | |
| 232 # TODO2: we can authenticate that a particular User pushed the button | |
| 233 # now, so someone can write this support. but it requires a | |
| 234 # buildbot.changes.changes.Change instance which is tedious at this | |
| 235 # stage to compute | |
| 236 s = SourceStamp(branch=branch, revision=revision) | |
| 237 req = BuildRequest(r, s, builderName=self.builder_status.getName(), | |
| 238 properties=properties) | |
| 239 try: | |
| 240 self.builder_control.requestBuildSoon(req) | |
| 241 except interfaces.NoSlaveError: | |
| 242 # TODO: tell the web user that their request could not be | |
| 243 # honored | |
| 244 pass | |
| 245 # send the user back to the builder page | |
| 246 return Redirect(".") | |
| 247 | |
| 248 def ping(self, req): | |
| 249 log.msg("web ping of builder '%s'" % self.builder_status.getName()) | |
| 250 self.builder_control.ping() # TODO: there ought to be an ISlaveControl | |
| 251 # send the user back to the builder page | |
| 252 return Redirect(".") | |
| 253 | |
| 254 def cancel(self, req): | |
| 255 try: | |
| 256 request_id = req.args.get("id", [None])[0] | |
| 257 if request_id == "all": | |
| 258 cancel_all = True | |
| 259 else: | |
| 260 cancel_all = False | |
| 261 request_id = int(request_id) | |
| 262 except: | |
| 263 request_id = None | |
| 264 if request_id: | |
| 265 for build_req in self.builder_control.getPendingBuilds(): | |
| 266 if cancel_all or id(build_req.original_request.status) == reques
t_id: | |
| 267 log.msg("Cancelling %s" % build_req) | |
| 268 build_req.cancel() | |
| 269 if not cancel_all: | |
| 270 break | |
| 271 return Redirect(".") | |
| 272 | |
| 273 def getChild(self, path, req): | |
| 274 if path == "force": | |
| 275 return self.force(req) | |
| 276 if path == "ping": | |
| 277 return self.ping(req) | |
| 278 if path == "events": | |
| 279 num = req.postpath.pop(0) | |
| 280 req.prepath.append(num) | |
| 281 num = int(num) | |
| 282 # TODO: is this dead code? .statusbag doesn't exist,right? | |
| 283 log.msg("getChild['path']: %s" % req.uri) | |
| 284 return NoResource("events are unavailable until code gets fixed") | |
| 285 filename = req.postpath.pop(0) | |
| 286 req.prepath.append(filename) | |
| 287 e = self.builder_status.getEventNumbered(num) | |
| 288 if not e: | |
| 289 return NoResource("No such event '%d'" % num) | |
| 290 file = e.files.get(filename, None) | |
| 291 if file == None: | |
| 292 return NoResource("No such file '%s'" % filename) | |
| 293 if type(file) == type(""): | |
| 294 if file[:6] in ("<HTML>", "<html>"): | |
| 295 return static.Data(file, "text/html") | |
| 296 return static.Data(file, "text/plain") | |
| 297 return file | |
| 298 if path == "cancelbuild": | |
| 299 return self.cancel(req) | |
| 300 if path == "builds": | |
| 301 return BuildsResource(self.builder_status, self.builder_control) | |
| 302 | |
| 303 return HtmlResource.getChild(self, path, req) | |
| 304 | |
| 305 | |
| 306 # /builders/_all | |
| 307 class StatusResourceAllBuilders(HtmlResource, OneLineMixin): | |
| 308 | |
| 309 def __init__(self, status, control): | |
| 310 HtmlResource.__init__(self) | |
| 311 self.status = status | |
| 312 self.control = control | |
| 313 | |
| 314 def getChild(self, path, req): | |
| 315 if path == "force": | |
| 316 return self.force(req) | |
| 317 if path == "stop": | |
| 318 return self.stop(req) | |
| 319 | |
| 320 return HtmlResource.getChild(self, path, req) | |
| 321 | |
| 322 def force(self, req): | |
| 323 for bname in self.status.getBuilderNames(): | |
| 324 builder_status = self.status.getBuilder(bname) | |
| 325 builder_control = None | |
| 326 c = self.getControl(req) | |
| 327 if c: | |
| 328 builder_control = c.getBuilder(bname) | |
| 329 build = StatusResourceBuilder(builder_status, builder_control) | |
| 330 build.force(req) | |
| 331 # back to the welcome page | |
| 332 return Redirect("../..") | |
| 333 | |
| 334 def stop(self, req): | |
| 335 for bname in self.status.getBuilderNames(): | |
| 336 builder_status = self.status.getBuilder(bname) | |
| 337 builder_control = None | |
| 338 c = self.getControl(req) | |
| 339 if c: | |
| 340 builder_control = c.getBuilder(bname) | |
| 341 (state, current_builds) = builder_status.getState() | |
| 342 if state != "building": | |
| 343 continue | |
| 344 for b in current_builds: | |
| 345 build_status = builder_status.getBuild(b.number) | |
| 346 if not build_status: | |
| 347 continue | |
| 348 if builder_control: | |
| 349 build_control = builder_control.getBuild(b.number) | |
| 350 else: | |
| 351 build_control = None | |
| 352 build = StatusResourceBuild(build_status, build_control, | |
| 353 builder_control) | |
| 354 build.stop(req) | |
| 355 # go back to the welcome page | |
| 356 return Redirect("../..") | |
| 357 | |
| 358 | |
| 359 # /builders | |
| 360 class BuildersResource(HtmlResource): | |
| 361 title = "Builders" | |
| 362 addSlash = True | |
| 363 | |
| 364 def body(self, req): | |
| 365 s = self.getStatus(req) | |
| 366 data = "" | |
| 367 data += "<h1>Builders</h1>\n" | |
| 368 | |
| 369 # TODO: this is really basic. It should be expanded to include a | |
| 370 # brief one-line summary of the builder (perhaps with whatever the | |
| 371 # builder is currently doing) | |
| 372 data += "<ol>\n" | |
| 373 for bname in s.getBuilderNames(): | |
| 374 data += (' <li><a href="%s">%s</a></li>\n' % | |
| 375 (req.childLink(urllib.quote(bname, safe='')), | |
| 376 bname)) | |
| 377 data += "</ol>\n" | |
| 378 | |
| 379 data += self.footer(s, req) | |
| 380 | |
| 381 return data | |
| 382 | |
| 383 def getChild(self, path, req): | |
| 384 s = self.getStatus(req) | |
| 385 if path in s.getBuilderNames(): | |
| 386 builder_status = s.getBuilder(path) | |
| 387 builder_control = None | |
| 388 c = self.getControl(req) | |
| 389 if c: | |
| 390 builder_control = c.getBuilder(path) | |
| 391 return StatusResourceBuilder(builder_status, builder_control) | |
| 392 if path == "_all": | |
| 393 return StatusResourceAllBuilders(self.getStatus(req), | |
| 394 self.getControl(req)) | |
| 395 | |
| 396 return HtmlResource.getChild(self, path, req) | |
| 397 | |
| OLD | NEW |