| 1 |
""" |
|---|
| 2 |
AMF server for buildbot that can be used by other programs to query build status. |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
import pyamf |
|---|
| 6 |
from pyamf.remoting.gateway.twisted import TwistedGateway |
|---|
| 7 |
|
|---|
| 8 |
from twisted.python import log |
|---|
| 9 |
from buildbot.status.builder import Results |
|---|
| 10 |
from itertools import count |
|---|
| 11 |
from datetime import datetime |
|---|
| 12 |
|
|---|
| 13 |
class AMFServer(TwistedGateway): |
|---|
| 14 |
def __init__(self): |
|---|
| 15 |
self.services = { |
|---|
| 16 |
"buildbot.getAllBuilders": self.getAllBuilders, |
|---|
| 17 |
"buildbot.getStatus": self.getStatus, |
|---|
| 18 |
"buildbot.getLastBuilds": self.getLastBuilds, |
|---|
| 19 |
"buildbot.getAllBuildsInInterval": self.getAllBuildsInInterval, |
|---|
| 20 |
"buildbot.getBuild": self.getBuild |
|---|
| 21 |
} |
|---|
| 22 |
TwistedGateway.__init__(self, self.services, expose_request=False) |
|---|
| 23 |
|
|---|
| 24 |
def render(self, req): |
|---|
| 25 |
# extract the IStatus and IControl objects for later use, since they |
|---|
| 26 |
# come from the request object. They'll be the same each time, but |
|---|
| 27 |
# they aren't available until the first request arrives. |
|---|
| 28 |
self.status = req.site.buildbot_service.getStatus() |
|---|
| 29 |
self.control = req.site.buildbot_service.getControl() |
|---|
| 30 |
|
|---|
| 31 |
return TwistedGateway.render(self, req) |
|---|
| 32 |
|
|---|
| 33 |
def getAllBuilders(self): |
|---|
| 34 |
""" |
|---|
| 35 |
Return a list of all builder names. |
|---|
| 36 |
""" |
|---|
| 37 |
log.msg("getAllBuilders") |
|---|
| 38 |
|
|---|
| 39 |
return self.status.getBuilderNames() |
|---|
| 40 |
|
|---|
| 41 |
def getStatus(self, builder_name): |
|---|
| 42 |
""" |
|---|
| 43 |
Return the result of the last build for the given builder. |
|---|
| 44 |
""" |
|---|
| 45 |
builder = self.status.getBuilder(builder_name) |
|---|
| 46 |
lastbuild = builder.getBuild(-1) |
|---|
| 47 |
|
|---|
| 48 |
if lastbuild == None: |
|---|
| 49 |
return lastbuild |
|---|
| 50 |
|
|---|
| 51 |
return Results[lastbuild.getResults()] |
|---|
| 52 |
|
|---|
| 53 |
def getLastBuilds(self, builder_name, num_builds): |
|---|
| 54 |
""" |
|---|
| 55 |
Return the last N completed builds for the given builder. |
|---|
| 56 |
'builder_name' is the name of the builder to query |
|---|
| 57 |
'num_builds' is the number of builds to return |
|---|
| 58 |
|
|---|
| 59 |
Each build is returned in the same form as C{getAllBuildsInInterval}. |
|---|
| 60 |
""" |
|---|
| 61 |
log.msg("getLastBuilds: %s - %d" % (builder_name, num_builds)) |
|---|
| 62 |
builder = self.status.getBuilder(builder_name) |
|---|
| 63 |
all_builds = [] |
|---|
| 64 |
for build_number in range(1, num_builds+1): |
|---|
| 65 |
build = builder.getBuild(-build_number) |
|---|
| 66 |
if not build: |
|---|
| 67 |
break |
|---|
| 68 |
if not build.isFinished(): |
|---|
| 69 |
continue |
|---|
| 70 |
(build_start, build_end) = build.getTimes() |
|---|
| 71 |
|
|---|
| 72 |
ss = build.getSourceStamp() |
|---|
| 73 |
branch = ss.branch |
|---|
| 74 |
if branch is None: |
|---|
| 75 |
branch = "" |
|---|
| 76 |
try: |
|---|
| 77 |
revision = build.getProperty("got_revision") |
|---|
| 78 |
except KeyError: |
|---|
| 79 |
revision = "" |
|---|
| 80 |
revision = str(revision) |
|---|
| 81 |
|
|---|
| 82 |
answer = BuildInfo() |
|---|
| 83 |
answer.builder_name = builder_name |
|---|
| 84 |
answer.number = build.getNumber() |
|---|
| 85 |
answer.end = datetime.fromtimestamp(build_end) |
|---|
| 86 |
answer.branchname = branch |
|---|
| 87 |
answer.revision = revision |
|---|
| 88 |
answer.results = Results[build.getResults()] |
|---|
| 89 |
answer.text = build.getText() |
|---|
| 90 |
|
|---|
| 91 |
all_builds.append((build_end, answer)) |
|---|
| 92 |
|
|---|
| 93 |
# now we've gotten all the builds we're interested in. Sort them by |
|---|
| 94 |
# end time. |
|---|
| 95 |
all_builds.sort(lambda a,b: cmp(a[0], b[0])) |
|---|
| 96 |
# and remove the timestamps |
|---|
| 97 |
all_builds = [t[1] for t in all_builds] |
|---|
| 98 |
|
|---|
| 99 |
log.msg("ready to go: %s" % (all_builds,)) |
|---|
| 100 |
|
|---|
| 101 |
return all_builds |
|---|
| 102 |
|
|---|
| 103 |
def getAllBuildsInInterval(self, start, stop): |
|---|
| 104 |
""" |
|---|
| 105 |
Return a list of builds that have completed after the 'start' |
|---|
| 106 |
timestamp and before the 'stop' timestamp. This looks at all |
|---|
| 107 |
Builders. |
|---|
| 108 |
|
|---|
| 109 |
The timestamps are integers, interpreted as standard unix timestamps |
|---|
| 110 |
(seconds since epoch). |
|---|
| 111 |
|
|---|
| 112 |
Each Build is returned as a tuple in the form:: |
|---|
| 113 |
(buildername, buildnumber, build_end, branchname, revision, |
|---|
| 114 |
results, text) |
|---|
| 115 |
|
|---|
| 116 |
The buildnumber is an integer. 'build_end' is an integer (seconds |
|---|
| 117 |
since epoch) specifying when the build finished. |
|---|
| 118 |
|
|---|
| 119 |
The branchname is a string, which may be an empty string to indicate |
|---|
| 120 |
None (i.e. the default branch). The revision is a string whose |
|---|
| 121 |
meaning is specific to the VC system in use, and comes from the |
|---|
| 122 |
'got_revision' build property. The results are expressed as a string, |
|---|
| 123 |
one of ('success', 'warnings', 'failure', 'exception'). The text is a |
|---|
| 124 |
list of short strings that ought to be joined by spaces and include |
|---|
| 125 |
slightly more data about the results of the build. |
|---|
| 126 |
""" |
|---|
| 127 |
#log.msg("start: %s %s %s" % (start, type(start), start.__class__)) |
|---|
| 128 |
log.msg("getAllBuildsInInterval: %d - %d" % (start, stop)) |
|---|
| 129 |
all_builds = [] |
|---|
| 130 |
|
|---|
| 131 |
for builder_name in self.status.getBuilderNames(): |
|---|
| 132 |
builder = self.status.getBuilder(builder_name) |
|---|
| 133 |
for build_number in count(1): |
|---|
| 134 |
build = builder.getBuild(-build_number) |
|---|
| 135 |
if not build: |
|---|
| 136 |
break |
|---|
| 137 |
if not build.isFinished(): |
|---|
| 138 |
continue |
|---|
| 139 |
(build_start, build_end) = build.getTimes() |
|---|
| 140 |
# in reality, builds are mostly ordered by start time. For |
|---|
| 141 |
# the purposes of this method, we pretend that they are |
|---|
| 142 |
# strictly ordered by end time, so that we can stop searching |
|---|
| 143 |
# when we start seeing builds that are outside the window. |
|---|
| 144 |
if build_end > stop: |
|---|
| 145 |
continue # keep looking |
|---|
| 146 |
if build_end < start: |
|---|
| 147 |
break # stop looking |
|---|
| 148 |
|
|---|
| 149 |
ss = build.getSourceStamp() |
|---|
| 150 |
branch = ss.branch |
|---|
| 151 |
if branch is None: |
|---|
| 152 |
branch = "" |
|---|
| 153 |
try: |
|---|
| 154 |
revision = build.getProperty("got_revision") |
|---|
| 155 |
except KeyError: |
|---|
| 156 |
revision = "" |
|---|
| 157 |
revision = str(revision) |
|---|
| 158 |
|
|---|
| 159 |
answer = (builder_name, |
|---|
| 160 |
build.getNumber(), |
|---|
| 161 |
datetime.fromtimestamp(build_end), |
|---|
| 162 |
branch, |
|---|
| 163 |
revision, |
|---|
| 164 |
Results[build.getResults()], |
|---|
| 165 |
build.getText(), |
|---|
| 166 |
) |
|---|
| 167 |
all_builds.append((build_end, answer)) |
|---|
| 168 |
# we've gotten all the builds that we care about from this |
|---|
| 169 |
# particular builder, so now we can continue on the next builder |
|---|
| 170 |
|
|---|
| 171 |
# now we've gotten all the builds we're interested in. Sort them by |
|---|
| 172 |
# end time. |
|---|
| 173 |
all_builds.sort(lambda a,b: cmp(a[0], b[0])) |
|---|
| 174 |
# and remove the timestamps |
|---|
| 175 |
all_builds = [t[1] for t in all_builds] |
|---|
| 176 |
|
|---|
| 177 |
log.msg("ready to go: %s" % (all_builds,)) |
|---|
| 178 |
|
|---|
| 179 |
return all_builds |
|---|
| 180 |
|
|---|
| 181 |
def getBuild(self, builder_name, build_number, details=False): |
|---|
| 182 |
""" |
|---|
| 183 |
Return information about a specific build. |
|---|
| 184 |
""" |
|---|
| 185 |
builder = self.status.getBuilder(builder_name) |
|---|
| 186 |
build = builder.getBuild(build_number) |
|---|
| 187 |
info = BuildDetails() |
|---|
| 188 |
info.builder_name = builder.getName() |
|---|
| 189 |
info.url = self.status.getURLForThing(build) |
|---|
| 190 |
info.reason = build.getReason() |
|---|
| 191 |
info.slavename = build.getSlavename() |
|---|
| 192 |
info.results = build.getResults() |
|---|
| 193 |
info.text = build.getText() |
|---|
| 194 |
ss = build.getSourceStamp() |
|---|
| 195 |
build_start, build_end = build.getTimes() |
|---|
| 196 |
info.start = datetime.fromtimestamp(build_start) |
|---|
| 197 |
info.end = datetime.fromtimestamp(build_end) |
|---|
| 198 |
|
|---|
| 199 |
info_steps = [] |
|---|
| 200 |
for s in build.getSteps(): |
|---|
| 201 |
stepinfo = StepInfo() |
|---|
| 202 |
stepinfo.name = s.getName() |
|---|
| 203 |
step_start, step_end = s.getTimes() |
|---|
| 204 |
stepinfo.start = datetime.fromtimestamp(step_start) |
|---|
| 205 |
stepinfo.end = datetime.fromtimestamp(step_end) |
|---|
| 206 |
stepinfo.results = s.getResults() |
|---|
| 207 |
info_steps.append(stepinfo) |
|---|
| 208 |
info.steps = info_steps |
|---|
| 209 |
|
|---|
| 210 |
info_logs = [] |
|---|
| 211 |
for l in build.getLogs(): |
|---|
| 212 |
loginfo = LogInfo() |
|---|
| 213 |
loginfo.name = l.getStep().getName() + "/" + l.getName() |
|---|
| 214 |
loginfo.text = l.getText() |
|---|
| 215 |
info_logs.append(loginfo) |
|---|
| 216 |
info.logs = info_logs |
|---|
| 217 |
|
|---|
| 218 |
return info |
|---|
| 219 |
|
|---|
| 220 |
class BuildInfo(object): |
|---|
| 221 |
def __init__(self): |
|---|
| 222 |
self.builder_name = None |
|---|
| 223 |
self.number = None |
|---|
| 224 |
self.end = None |
|---|
| 225 |
self.branchname = None |
|---|
| 226 |
self.revision = None |
|---|
| 227 |
self.results = None |
|---|
| 228 |
self.text = [] |
|---|
| 229 |
|
|---|
| 230 |
def __repr__(self): |
|---|
| 231 |
return '<%s builder=%s number=%s>' % (BuildInfo.__name__, self.builder_name, self.number) |
|---|
| 232 |
|
|---|
| 233 |
pyamf.register_class(BuildInfo, 'net.buildbot.build.BuildInfo') |
|---|
| 234 |
|
|---|
| 235 |
class BuildDetails(object): |
|---|
| 236 |
def __init__(self): |
|---|
| 237 |
self.builder_name = None |
|---|
| 238 |
self.url = None |
|---|
| 239 |
self.reason = None |
|---|
| 240 |
self.slavename = None |
|---|
| 241 |
self.results = None |
|---|
| 242 |
self.text = [] |
|---|
| 243 |
self.start = None |
|---|
| 244 |
self.end = None |
|---|
| 245 |
self.steps = [] |
|---|
| 246 |
self.logs = [] |
|---|
| 247 |
|
|---|
| 248 |
def __repr__(self): |
|---|
| 249 |
return '<%s builder=%s slave=%s>' % (BuildDetails.__name__, self.builder_name, self.slavename) |
|---|
| 250 |
|
|---|
| 251 |
pyamf.register_class(BuildDetails, 'net.buildbot.build.BuildDetails') |
|---|
| 252 |
|
|---|
| 253 |
class StepInfo(object): |
|---|
| 254 |
def __init__(self): |
|---|
| 255 |
self.name = None |
|---|
| 256 |
self.start = None |
|---|
| 257 |
self.end = None |
|---|
| 258 |
self.results = [] |
|---|
| 259 |
|
|---|
| 260 |
def __repr__(self): |
|---|
| 261 |
return '<%s name=%s>' % (StepInfo.__name__, self.name) |
|---|
| 262 |
|
|---|
| 263 |
pyamf.register_class(StepInfo, 'net.buildbot.build.StepInfo') |
|---|
| 264 |
|
|---|
| 265 |
class LogInfo(object): |
|---|
| 266 |
def __init__(self): |
|---|
| 267 |
self.name = None |
|---|
| 268 |
self.text = None |
|---|
| 269 |
|
|---|
| 270 |
def __repr__(self): |
|---|
| 271 |
return '<%s name=%s>' % (LogInfo.__name__, self.name) |
|---|
| 272 |
|
|---|
| 273 |
pyamf.register_class(LogInfo, 'net.buildbot.build.LogInfo') |
|---|