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