| 1 | # -*- coding: utf-8 -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (C) 2007-2010 Edgewall Software |
---|
| 4 | # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> |
---|
| 5 | # All rights reserved. |
---|
| 6 | # |
---|
| 7 | # This software is licensed as described in the file COPYING, which |
---|
| 8 | # you should have received as part of this distribution. The terms |
---|
| 9 | # are also available at http://bitten.edgewall.org/wiki/License. |
---|
| 10 | |
---|
1 | 11 | """Build master implementation.""" |
---|
| 12 | |
---|
1 | 13 | import calendar |
---|
1 | 14 | import re |
---|
1 | 15 | import time |
---|
1 | 16 | from StringIO import StringIO |
---|
| 17 | |
---|
1 | 18 | from trac.attachment import Attachment |
---|
1 | 19 | from trac.config import BoolOption, IntOption, Option |
---|
1 | 20 | from trac.core import * |
---|
1 | 21 | from trac.resource import ResourceNotFound |
---|
1 | 22 | from trac.web import IRequestHandler, RequestDone |
---|
| 23 | |
---|
1 | 24 | from bitten import PROTOCOL_VERSION |
---|
1 | 25 | from bitten.model import BuildConfig, Build, BuildStep, BuildLog, Report, \ |
---|
1 | 26 | TargetPlatform |
---|
| 27 | |
---|
1 | 28 | from bitten.main import BuildSystem |
---|
1 | 29 | from bitten.queue import BuildQueue |
---|
1 | 30 | from bitten.recipe import Recipe |
---|
1 | 31 | from bitten.util import xmlio |
---|
| 32 | |
---|
1 | 33 | __all__ = ['BuildMaster'] |
---|
1 | 34 | __docformat__ = 'restructuredtext en' |
---|
| 35 | |
---|
| 36 | |
---|
1 | 37 | HTTP_BAD_REQUEST = 400 |
---|
1 | 38 | HTTP_FORBIDDEN = 403 |
---|
1 | 39 | HTTP_NOT_FOUND = 404 |
---|
1 | 40 | HTTP_METHOD_NOT_ALLOWED = 405 |
---|
1 | 41 | HTTP_CONFLICT = 409 |
---|
| 42 | |
---|
| 43 | |
---|
2 | 44 | class BuildMaster(Component): |
---|
1 | 45 | """Trac request handler implementation for the build master.""" |
---|
| 46 | |
---|
1 | 47 | implements(IRequestHandler) |
---|
| 48 | |
---|
| 49 | # Configuration options |
---|
| 50 | |
---|
1 | 51 | adjust_timestamps = BoolOption('bitten', 'adjust_timestamps', False, doc= |
---|
| 52 | """Whether the timestamps of builds should be adjusted to be close |
---|
1 | 53 | to the timestamps of the corresponding changesets.""") |
---|
| 54 | |
---|
1 | 55 | build_all = BoolOption('bitten', 'build_all', False, doc= |
---|
| 56 | """Whether to request builds of older revisions even if a younger |
---|
1 | 57 | revision has already been built.""") |
---|
| 58 | |
---|
1 | 59 | stabilize_wait = IntOption('bitten', 'stabilize_wait', 0, doc= |
---|
| 60 | """The time in seconds to wait for the repository to stabilize before |
---|
| 61 | queuing up a new build. This allows time for developers to check in |
---|
| 62 | a group of related changes back to back without spawning multiple |
---|
1 | 63 | builds.""") |
---|
| 64 | |
---|
1 | 65 | slave_timeout = IntOption('bitten', 'slave_timeout', 3600, doc= |
---|
| 66 | """The time in seconds after which a build is cancelled if the slave |
---|
1 | 67 | does not report progress.""") |
---|
| 68 | |
---|
1 | 69 | logs_dir = Option('bitten', 'logs_dir', "log/bitten", doc= |
---|
1 | 70 | """The directory on the server in which client log files will be stored.""") |
---|
| 71 | |
---|
1 | 72 | quick_status = BoolOption('bitten', 'quick_status', False, doc= |
---|
| 73 | """Whether to show the current build status within the Trac main |
---|
| 74 | navigation bar. '''Note:''' The feature requires expensive database and |
---|
| 75 | repository checks for every page request, and should not be enabled |
---|
| 76 | if the project has a large repository or uses a non-Subversion |
---|
1 | 77 | repository such as Mercurial or Git.""") |
---|
| 78 | |
---|
1 | 79 | def __init__(self): |
---|
26 | 80 | self.env.systeminfo.append(('Bitten', |
---|
26 | 81 | __import__('bitten', ['__version__']).__version__)) |
---|
| 82 | |
---|
| 83 | # IRequestHandler methods |
---|
| 84 | |
---|
1 | 85 | def match_request(self, req): |
---|
23 | 86 | match = re.match(r'/builds(?:/(\d+)(?:/(\w+)/([^/]+)?)?)?$', |
---|
23 | 87 | req.path_info) |
---|
23 | 88 | if match: |
---|
23 | 89 | if match.group(1): |
---|
17 | 90 | req.args['id'] = match.group(1) |
---|
17 | 91 | req.args['collection'] = match.group(2) |
---|
17 | 92 | req.args['member'] = match.group(3) |
---|
23 | 93 | return True |
---|
| 94 | |
---|
1 | 95 | def process_request(self, req): |
---|
21 | 96 | req.perm.assert_permission('BUILD_EXEC') |
---|
| 97 | |
---|
21 | 98 | if 'trac_auth' in req.incookie: |
---|
21 | 99 | slave_token = req.incookie['trac_auth'].value |
---|
21 | 100 | else: |
---|
0 | 101 | slave_token = req.session.sid |
---|
| 102 | |
---|
21 | 103 | if 'id' not in req.args: |
---|
6 | 104 | if req.method != 'POST': |
---|
1 | 105 | self._send_response(req, |
---|
1 | 106 | body='Only POST allowed for build creation.') |
---|
5 | 107 | return self._process_build_creation(req, slave_token) |
---|
| 108 | |
---|
15 | 109 | build = Build.fetch(self.env, req.args['id']) |
---|
15 | 110 | if not build: |
---|
1 | 111 | self._send_error(req, HTTP_NOT_FOUND, |
---|
1 | 112 | 'No such build (%s)' % req.args['id']) |
---|
| 113 | |
---|
14 | 114 | build_token = build.slave_info.get('token', '') |
---|
14 | 115 | if build_token != slave_token: |
---|
2 | 116 | self._send_error(req, HTTP_CONFLICT, |
---|
2 | 117 | 'Token mismatch (wrong slave): slave=%s, build=%s' \ |
---|
2 | 118 | % (slave_token, build_token)) |
---|
| 119 | |
---|
12 | 120 | config = BuildConfig.fetch(self.env, build.config) |
---|
| 121 | |
---|
12 | 122 | if not req.args['collection']: |
---|
2 | 123 | if req.method == 'DELETE': |
---|
1 | 124 | return self._process_build_cancellation(req, config, build) |
---|
1 | 125 | else: |
---|
1 | 126 | return self._process_build_initiation(req, config, build) |
---|
| 127 | |
---|
10 | 128 | if req.method != 'POST': |
---|
1 | 129 | self._send_error(req, HTTP_METHOD_NOT_ALLOWED, |
---|
1 | 130 | 'Method %s not allowed' % req.method) |
---|
| 131 | |
---|
9 | 132 | if req.args['collection'] == 'steps': |
---|
8 | 133 | return self._process_build_step(req, config, build) |
---|
1 | 134 | elif req.args['collection'] == 'attach': |
---|
0 | 135 | return self._process_attachment(req, config, build) |
---|
1 | 136 | elif req.args['collection'] == 'keepalive': |
---|
0 | 137 | return self._process_keepalive(req, config, build) |
---|
0 | 138 | else: |
---|
1 | 139 | self._send_error(req, HTTP_NOT_FOUND, |
---|
1 | 140 | "No such collection '%s'" % req.args['collection']) |
---|
| 141 | |
---|
| 142 | # Internal methods |
---|
| 143 | |
---|
1 | 144 | def _send_response(self, req, code=200, body='', headers=None): |
---|
| 145 | """ Formats and sends the response, raising ``RequestDone``. """ |
---|
23 | 146 | if isinstance(body, unicode): |
---|
1 | 147 | body = body.encode('utf-8') |
---|
23 | 148 | req.send_response(code) |
---|
23 | 149 | headers = headers or {} |
---|
23 | 150 | headers.setdefault('Content-Length', len(body)) |
---|
75 | 151 | for header in headers: |
---|
52 | 152 | req.send_header(header, headers[header]) |
---|
23 | 153 | req.write(body) |
---|
23 | 154 | raise RequestDone |
---|
| 155 | |
---|
1 | 156 | def _send_error(self, req, code=500, message=''): |
---|
| 157 | """ Formats and sends the error, raising ``RequestDone``. """ |
---|
9 | 158 | headers = {'Content-Type': 'text/plain', |
---|
9 | 159 | 'Content-Length': str(len(message))} |
---|
9 | 160 | self._send_response(req, code, body=message, headers=headers) |
---|
| 161 | |
---|
1 | 162 | def _process_build_creation(self, req, slave_token): |
---|
5 | 163 | queue = BuildQueue(self.env, build_all=self.build_all, |
---|
5 | 164 | stabilize_wait=self.stabilize_wait, |
---|
5 | 165 | timeout=self.slave_timeout) |
---|
5 | 166 | try: |
---|
5 | 167 | queue.populate() |
---|
0 | 168 | except AssertionError, e: |
---|
0 | 169 | self.log.error(e.message, exc_info=True) |
---|
0 | 170 | self._send_error(req, HTTP_BAD_REQUEST, e.message) |
---|
| 171 | |
---|
5 | 172 | try: |
---|
5 | 173 | elem = xmlio.parse(req.read()) |
---|
1 | 174 | except xmlio.ParseError, e: |
---|
1 | 175 | self.log.error('Error parsing build initialization request: %s', e, |
---|
1 | 176 | exc_info=True) |
---|
1 | 177 | self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error') |
---|
| 178 | |
---|
4 | 179 | slave_version = int(elem.attr.get('version', 1)) |
---|
| 180 | |
---|
| 181 | # FIXME: Remove version compatibility code. |
---|
| 182 | # The initial difference between protocol version 3 and 4 is that |
---|
| 183 | # the master allows keepalive requests-- the master must be |
---|
| 184 | # at least 4 before slaves supporting version 4 are allowed. When |
---|
| 185 | # the first force master/slave upgrade requirement comes in |
---|
| 186 | # (or we bump the) version number again, remove this code. |
---|
4 | 187 | if slave_version == 3 and PROTOCOL_VERSION == 4: |
---|
0 | 188 | self.log.info('Allowing slave version %d to process build for ' |
---|
0 | 189 | 'compatibility. Upgrade slave to support build ' |
---|
0 | 190 | 'keepalives.', slave_version) |
---|
4 | 191 | elif slave_version != PROTOCOL_VERSION: |
---|
2 | 192 | self._send_error(req, HTTP_BAD_REQUEST, |
---|
2 | 193 | "Master-Slave version mismatch: master=%d, slave=%d" % \ |
---|
2 | 194 | (PROTOCOL_VERSION, slave_version)) |
---|
| 195 | |
---|
2 | 196 | slavename = elem.attr['name'] |
---|
2 | 197 | properties = {'name': slavename, Build.IP_ADDRESS: req.remote_addr, |
---|
2 | 198 | Build.TOKEN: slave_token} |
---|
2 | 199 | self.log.info('Build slave %r connected from %s with token %s', |
---|
2 | 200 | slavename, req.remote_addr, slave_token) |
---|
| 201 | |
---|
7 | 202 | for child in elem.children(): |
---|
5 | 203 | if child.name == 'platform': |
---|
2 | 204 | properties[Build.MACHINE] = child.gettext() |
---|
2 | 205 | properties[Build.PROCESSOR] = child.attr.get('processor') |
---|
3 | 206 | elif child.name == 'os': |
---|
2 | 207 | properties[Build.OS_NAME] = child.gettext() |
---|
2 | 208 | properties[Build.OS_FAMILY] = child.attr.get('family') |
---|
2 | 209 | properties[Build.OS_VERSION] = child.attr.get('version') |
---|
1 | 210 | elif child.name == 'package': |
---|
3 | 211 | for name, value in child.attr.items(): |
---|
2 | 212 | if name == 'name': |
---|
1 | 213 | continue |
---|
1 | 214 | properties[child.attr['name'] + '.' + name] = value |
---|
| 215 | |
---|
2 | 216 | self.log.debug('Build slave configuration: %r', properties) |
---|
| 217 | |
---|
2 | 218 | build = queue.get_build_for_slave(slavename, properties) |
---|
2 | 219 | if not build: |
---|
1 | 220 | self._send_response(req, 204, '', {}) |
---|
| 221 | |
---|
1 | 222 | self._send_response(req, 201, 'Build pending', headers={ |
---|
1 | 223 | 'Content-Type': 'text/plain', |
---|
1 | 224 | 'Location': req.abs_href.builds(build.id)}) |
---|
| 225 | |
---|
1 | 226 | def _process_build_cancellation(self, req, config, build): |
---|
1 | 227 | self.log.info('Build slave %r cancelled build %d', build.slave, |
---|
1 | 228 | build.id) |
---|
1 | 229 | build.status = Build.PENDING |
---|
1 | 230 | build.slave = None |
---|
1 | 231 | build.slave_info = {} |
---|
1 | 232 | build.started = 0 |
---|
1 | 233 | db = self.env.get_db_cnx() |
---|
1 | 234 | for step in list(BuildStep.select(self.env, build=build.id, db=db)): |
---|
0 | 235 | step.delete(db=db) |
---|
1 | 236 | build.update(db=db) |
---|
| 237 | |
---|
1 | 238 | Attachment.delete_all(self.env, 'build', build.resource.id, db) |
---|
| 239 | |
---|
1 | 240 | db.commit() |
---|
| 241 | |
---|
2 | 242 | for listener in BuildSystem(self.env).listeners: |
---|
1 | 243 | listener.build_aborted(build) |
---|
| 244 | |
---|
1 | 245 | self._send_response(req, 204, '', {}) |
---|
| 246 | |
---|
1 | 247 | def _process_build_initiation(self, req, config, build): |
---|
1 | 248 | self.log.info('Build slave %r initiated build %d', build.slave, |
---|
1 | 249 | build.id) |
---|
1 | 250 | build.started = int(time.time()) |
---|
1 | 251 | build.last_activity = build.started |
---|
1 | 252 | build.update() |
---|
| 253 | |
---|
2 | 254 | for listener in BuildSystem(self.env).listeners: |
---|
1 | 255 | listener.build_started(build) |
---|
| 256 | |
---|
1 | 257 | xml = xmlio.parse(config.recipe) |
---|
1 | 258 | xml.attr['path'] = config.path |
---|
1 | 259 | xml.attr['revision'] = build.rev |
---|
1 | 260 | xml.attr['config'] = config.name |
---|
1 | 261 | xml.attr['build'] = str(build.id) |
---|
1 | 262 | target_platform = TargetPlatform.fetch(self.env, build.platform) |
---|
1 | 263 | xml.attr['platform'] = target_platform.name |
---|
1 | 264 | xml.attr['name'] = build.slave |
---|
1 | 265 | xml.attr['form_token'] = req.form_token # For posting attachments |
---|
1 | 266 | body = str(xml) |
---|
| 267 | |
---|
1 | 268 | self.log.info('Build slave %r initiated build %d', build.slave, |
---|
1 | 269 | build.id) |
---|
| 270 | |
---|
| 271 | # create the first step, mark it as in-progress. |
---|
| 272 | |
---|
1 | 273 | recipe = Recipe(xmlio.parse(config.recipe)) |
---|
1 | 274 | stepname = recipe.__iter__().next().id |
---|
| 275 | |
---|
1 | 276 | step = self._start_new_step(build, stepname) |
---|
1 | 277 | step.insert() |
---|
| 278 | |
---|
1 | 279 | self._send_response(req, 200, body, headers={ |
---|
1 | 280 | 'Content-Type': 'application/x-bitten+xml', |
---|
1 | 281 | 'Content-Length': str(len(body)), |
---|
1 | 282 | 'Content-Disposition': |
---|
1 | 283 | 'attachment; filename=recipe_%s_r%s.xml' % |
---|
1 | 284 | (config.name, build.rev)}) |
---|
| 285 | |
---|
1 | 286 | def _process_build_step(self, req, config, build): |
---|
8 | 287 | try: |
---|
8 | 288 | elem = xmlio.parse(req.read()) |
---|
1 | 289 | except xmlio.ParseError, e: |
---|
1 | 290 | self.log.error('Error parsing build step result: %s', e, |
---|
1 | 291 | exc_info=True) |
---|
1 | 292 | self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error') |
---|
7 | 293 | stepname = elem.attr['step'] |
---|
| 294 | |
---|
| 295 | # we should have created this step previously; if it hasn't, |
---|
| 296 | # the master and slave are processing steps out of order. |
---|
7 | 297 | step = BuildStep.fetch(self.env, build=build.id, name=stepname) |
---|
7 | 298 | if not step: |
---|
0 | 299 | self._send_error(req, HTTP_CONFLICT, 'Build step has not been created.') |
---|
| 300 | |
---|
7 | 301 | recipe = Recipe(xmlio.parse(config.recipe)) |
---|
7 | 302 | index = None |
---|
7 | 303 | current_step = None |
---|
15 | 304 | for num, recipe_step in enumerate(recipe): |
---|
8 | 305 | if recipe_step.id == stepname: |
---|
7 | 306 | index = num |
---|
7 | 307 | current_step = recipe_step |
---|
7 | 308 | if index is None: |
---|
0 | 309 | self._send_error(req, HTTP_FORBIDDEN, |
---|
0 | 310 | 'No such build step' % stepname) |
---|
7 | 311 | last_step = index == num |
---|
| 312 | |
---|
7 | 313 | self.log.debug('Slave %s (build %d) completed step %d (%s) with ' |
---|
7 | 314 | 'status %s', build.slave, build.id, index, stepname, |
---|
7 | 315 | elem.attr['status']) |
---|
| 316 | |
---|
7 | 317 | db = self.env.get_db_cnx() |
---|
| 318 | |
---|
7 | 319 | step.stopped = int(time.time()) |
---|
| 320 | |
---|
7 | 321 | if elem.attr['status'] == 'failure': |
---|
3 | 322 | self.log.warning('Build %s step %s failed', build.id, stepname) |
---|
3 | 323 | step.status = BuildStep.FAILURE |
---|
3 | 324 | if current_step.onerror == 'fail': |
---|
1 | 325 | last_step = True |
---|
1 | 326 | else: |
---|
4 | 327 | step.status = BuildStep.SUCCESS |
---|
7 | 328 | step.errors += [error.gettext() for error in elem.children('error')] |
---|
| 329 | |
---|
| 330 | # TODO: step.update(db=db) |
---|
7 | 331 | step.delete(db=db) |
---|
7 | 332 | step.insert(db=db) |
---|
| 333 | |
---|
| 334 | # Collect log messages from the request body |
---|
9 | 335 | for idx, log_elem in enumerate(elem.children('log')): |
---|
2 | 336 | build_log = BuildLog(self.env, build=build.id, step=stepname, |
---|
2 | 337 | generator=log_elem.attr.get('generator'), |
---|
2 | 338 | orderno=idx) |
---|
6 | 339 | for message_elem in log_elem.children('message'): |
---|
4 | 340 | build_log.messages.append((message_elem.attr['level'], |
---|
4 | 341 | message_elem.gettext())) |
---|
2 | 342 | build_log.insert(db=db) |
---|
| 343 | |
---|
| 344 | # Collect report data from the request body |
---|
8 | 345 | for report_elem in elem.children('report'): |
---|
1 | 346 | report = Report(self.env, build=build.id, step=stepname, |
---|
1 | 347 | category=report_elem.attr.get('category'), |
---|
1 | 348 | generator=report_elem.attr.get('generator')) |
---|
2 | 349 | for item_elem in report_elem.children(): |
---|
1 | 350 | item = {'type': item_elem.name} |
---|
1 | 351 | item.update(item_elem.attr) |
---|
2 | 352 | for child_elem in item_elem.children(): |
---|
1 | 353 | item[child_elem.name] = child_elem.gettext() |
---|
1 | 354 | report.items.append(item) |
---|
1 | 355 | report.insert(db=db) |
---|
| 356 | |
---|
| 357 | # If this was the last step in the recipe we mark the build as |
---|
| 358 | # completed otherwise just update last_activity |
---|
7 | 359 | if last_step: |
---|
6 | 360 | self.log.info('Slave %s completed build %d ("%s" as of [%s])', |
---|
6 | 361 | build.slave, build.id, build.config, build.rev) |
---|
6 | 362 | build.stopped = step.stopped |
---|
6 | 363 | build.last_activity = build.stopped |
---|
| 364 | |
---|
| 365 | # Determine overall outcome of the build by checking the outcome |
---|
| 366 | # of the individual steps against the "onerror" specification of |
---|
| 367 | # each step in the recipe |
---|
10 | 368 | for num, recipe_step in enumerate(recipe): |
---|
6 | 369 | step = BuildStep.fetch(self.env, build.id, recipe_step.id) |
---|
6 | 370 | if step.status == BuildStep.FAILURE: |
---|
3 | 371 | if recipe_step.onerror == 'fail' or \ |
---|
2 | 372 | recipe_step.onerror == 'continue': |
---|
2 | 373 | build.status = Build.FAILURE |
---|
2 | 374 | break |
---|
2 | 375 | else: |
---|
4 | 376 | build.status = Build.SUCCESS |
---|
| 377 | |
---|
6 | 378 | build.update(db=db) |
---|
6 | 379 | else: |
---|
1 | 380 | build.last_activity = step.stopped |
---|
1 | 381 | build.update(db=db) |
---|
| 382 | |
---|
| 383 | # start the next step. |
---|
3 | 384 | for num, recipe_step in enumerate(recipe): |
---|
2 | 385 | if num == index + 1: |
---|
1 | 386 | next_step = recipe_step |
---|
1 | 387 | if next_step is None: |
---|
0 | 388 | self._send_error(req, HTTP_FORBIDDEN, |
---|
0 | 389 | 'Unable to find step after ' % stepname) |
---|
| 390 | |
---|
1 | 391 | step = self._start_new_step(build, next_step.id) |
---|
1 | 392 | step.insert(db=db) |
---|
| 393 | |
---|
7 | 394 | db.commit() |
---|
| 395 | |
---|
7 | 396 | if last_step: |
---|
12 | 397 | for listener in BuildSystem(self.env).listeners: |
---|
6 | 398 | listener.build_completed(build) |
---|
| 399 | |
---|
7 | 400 | body = 'Build step processed' |
---|
7 | 401 | self._send_response(req, 201, body, { |
---|
7 | 402 | 'Content-Type': 'text/plain', |
---|
7 | 403 | 'Content-Length': str(len(body)), |
---|
7 | 404 | 'Location': req.abs_href.builds( |
---|
7 | 405 | build.id, 'steps', stepname)}) |
---|
| 406 | |
---|
1 | 407 | def _process_attachment(self, req, config, build): |
---|
2 | 408 | resource_id = req.args['member'] == 'config' \ |
---|
1 | 409 | and build.config or build.resource.id |
---|
2 | 410 | upload = req.args['file'] |
---|
2 | 411 | if not upload.file: |
---|
0 | 412 | send_error(req, message="Attachment not received.") |
---|
2 | 413 | self.log.debug('Received attachment %s for attaching to build:%s', |
---|
2 | 414 | upload.filename, resource_id) |
---|
| 415 | |
---|
| 416 | # Determine size of file |
---|
2 | 417 | upload.file.seek(0, 2) # to the end |
---|
2 | 418 | size = upload.file.tell() |
---|
2 | 419 | upload.file.seek(0) # beginning again |
---|
| 420 | |
---|
| 421 | # Delete attachment if it already exists |
---|
2 | 422 | try: |
---|
2 | 423 | old_attach = Attachment(self.env, 'build', |
---|
2 | 424 | parent_id=resource_id, filename=upload.filename) |
---|
0 | 425 | old_attach.delete() |
---|
2 | 426 | except ResourceNotFound: |
---|
2 | 427 | pass |
---|
| 428 | |
---|
| 429 | # Save new attachment |
---|
2 | 430 | attachment = Attachment(self.env, 'build', parent_id=resource_id) |
---|
2 | 431 | attachment.description = req.args.get('description', '') |
---|
2 | 432 | attachment.author = req.authname |
---|
2 | 433 | attachment.insert(upload.filename, upload.file, size) |
---|
| 434 | |
---|
2 | 435 | self._send_response(req, 201, 'Attachment created', headers={ |
---|
2 | 436 | 'Content-Type': 'text/plain', |
---|
2 | 437 | 'Content-Length': str(len('Attachment created'))}) |
---|
| 438 | |
---|
1 | 439 | def _process_keepalive(self, req, config, build): |
---|
0 | 440 | build.last_activity = int(time.time()) |
---|
0 | 441 | build.update() |
---|
| 442 | |
---|
0 | 443 | self.log.info('Slave %s build %d keepalive ("%s" as of [%s])', |
---|
0 | 444 | build.slave, build.id, build.config, build.rev) |
---|
| 445 | |
---|
0 | 446 | body = 'Keepalive processed' |
---|
0 | 447 | self._send_response(req, 200, body, { |
---|
0 | 448 | 'Content-Type': 'text/plain', |
---|
0 | 449 | 'Content-Length': str(len(body))}) |
---|
| 450 | |
---|
1 | 451 | def _start_new_step(self, build, stepname): |
---|
| 452 | """Creates the in-memory representation for a newly started |
---|
| 453 | step, ready to be persisted to the database. |
---|
| 454 | """ |
---|
13 | 455 | step = BuildStep(self.env, build=build.id, name=stepname) |
---|
13 | 456 | step.status = BuildStep.IN_PROGRESS |
---|
13 | 457 | step.started = int(time.time()) |
---|
13 | 458 | step.stopped = 0 |
---|
| 459 | |
---|
13 | 460 | return step |
---|