1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import os
18 import signal
19 import socket
20
21 from zope.interface import implements
22 from twisted.python import log, components, failure
23 from twisted.internet import defer, reactor, task
24 from twisted.application import service
25
26 import buildbot
27 import buildbot.pbmanager
28 from buildbot.util import subscription, epoch2datetime
29 from buildbot.status.master import Status
30 from buildbot.changes import changes
31 from buildbot.changes.manager import ChangeManager
32 from buildbot import interfaces
33 from buildbot.process.builder import BuilderControl
34 from buildbot.db import connector
35 from buildbot.schedulers.manager import SchedulerManager
36 from buildbot.process.botmaster import BotMaster
37 from buildbot.process import debug
38 from buildbot.process import metrics
39 from buildbot.process import cache
40 from buildbot.process.users import users
41 from buildbot.process.users.manager import UserManagerManager
42 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE
43 from buildbot import monkeypatches
44 from buildbot import config
50 self.rotateLength = 1 * 1000 * 1000
51 self.maxRotatedFiles = 10
52
53 -class BuildMaster(config.ReconfigurableServiceMixin, service.MultiService):
210
211
213 if self.db_loop:
214 self.db_loop.stop()
215 self.db_loop = None
216
217
219
220
221 if self.reconfig_active:
222 log.msg("reconfig already active; will reconfig again after")
223 self.reconfig_requested = True
224 return
225
226 self.reconfig_active = reactor.seconds()
227 metrics.MetricCountEvent.log("loaded_config", 1)
228
229
230
231 self.reconfig_notifier = task.LoopingCall(lambda :
232 log.msg("reconfig is ongoing for %d s" %
233 (reactor.seconds() - self.reconfig_active)))
234 self.reconfig_notifier.start(10, now=False)
235
236 timer = metrics.Timer("BuildMaster.reconfig")
237 timer.start()
238
239 d = self.doReconfig()
240
241 @d.addBoth
242 def cleanup(res):
243 timer.stop()
244 self.reconfig_notifier.stop()
245 self.reconfig_notifier = None
246 self.reconfig_active = False
247 if self.reconfig_requested:
248 self.reconfig_requested = False
249 self.reconfig()
250 return res
251
252 d.addErrback(log.err, 'while reconfiguring')
253
254 return d
255
256
257 @defer.inlineCallbacks
259 log.msg("beginning configuration update")
260 changes_made = False
261 failed = False
262 try:
263 new_config = config.MasterConfig.loadConfig(self.basedir,
264 self.configFileName)
265 changes_made = True
266 self.config = new_config
267 yield self.reconfigService(new_config)
268
269 except config.ConfigErrors, e:
270 for msg in e.errors:
271 log.msg(msg)
272 failed = True
273
274 except:
275 log.err(failure.Failure(), 'during reconfig:')
276 failed = True
277
278 if failed:
279 if changes_made:
280 log.msg("WARNING: reconfig partially applied; master "
281 "may malfunction")
282 else:
283 log.msg("reconfig aborted without making any changes")
284 else:
285 log.msg("configuration update complete")
286
287
289 if self.config.db['db_url'] != new_config.db['db_url']:
290 config.error(
291 "Cannot change c['db']['db_url'] after the master has started",
292 )
293
294
295 if (self.config.db['db_poll_interval']
296 != new_config.db['db_poll_interval']):
297 if self.db_loop:
298 self.db_loop.stop()
299 self.db_loop = None
300 poll_interval = new_config.db['db_poll_interval']
301 if poll_interval:
302 self.db_loop = task.LoopingCall(self.pollDatabase)
303 self.db_loop.start(poll_interval, now=False)
304
305 return config.ReconfigurableServiceMixin.reconfigService(self,
306 new_config)
307
308
309
310
312 return list(self.scheduler_manager)
313
315 """
316 @rtype: L{buildbot.status.builder.Status}
317 """
318 return self.status
319
321 """
322 Return the obejct id for this master, for associating state with the
323 master.
324
325 @returns: ID, via Deferred
326 """
327
328 if self._object_id is not None:
329 return defer.succeed(self._object_id)
330
331
332
333 try:
334 hostname = os.uname()[1]
335 except AttributeError:
336 hostname = socket.getfqdn()
337 master_name = "%s:%s" % (hostname, os.path.abspath(self.basedir))
338
339 d = self.db.state.getObjectId(master_name,
340 "buildbot.master.BuildMaster")
341 def keep(id):
342 self._object_id = id
343 return id
344 d.addCallback(keep)
345 return d
346
347
348
349
350 - def addChange(self, who=None, files=None, comments=None, author=None,
351 isdir=None, is_dir=None, revision=None, when=None,
352 when_timestamp=None, branch=None, category=None, revlink='',
353 properties={}, repository='', codebase=None, project='', src=None):
354 """
355 Add a change to the buildmaster and act on it.
356
357 This is a wrapper around L{ChangesConnectorComponent.addChange} which
358 also acts on the resulting change and returns a L{Change} instance.
359
360 Note that all parameters are keyword arguments, although C{who},
361 C{files}, and C{comments} can be specified positionally for
362 backward-compatibility.
363
364 @param author: the author of this change
365 @type author: unicode string
366
367 @param who: deprecated name for C{author}
368
369 @param files: a list of filenames that were changed
370 @type branch: list of unicode strings
371
372 @param comments: user comments on the change
373 @type branch: unicode string
374
375 @param is_dir: deprecated
376
377 @param isdir: deprecated name for C{is_dir}
378
379 @param revision: the revision identifier for this change
380 @type revision: unicode string
381
382 @param when_timestamp: when this change occurred, or the current time
383 if None
384 @type when_timestamp: datetime instance or None
385
386 @param when: deprecated name and type for C{when_timestamp}
387 @type when: integer (UNIX epoch time) or None
388
389 @param branch: the branch on which this change took place
390 @type branch: unicode string
391
392 @param category: category for this change (arbitrary use by Buildbot
393 users)
394 @type category: unicode string
395
396 @param revlink: link to a web view of this revision
397 @type revlink: unicode string
398
399 @param properties: properties to set on this change
400 @type properties: dictionary with string keys and simple values
401 (JSON-able). Note that the property source is I{not} included
402 in this dictionary.
403
404 @param repository: the repository in which this change took place
405 @type repository: unicode string
406
407 @param project: the project this change is a part of
408 @type project: unicode string
409
410 @param src: source of the change (vcs or other)
411 @type src: string
412
413 @returns: L{Change} instance via Deferred
414 """
415 metrics.MetricCountEvent.log("added_changes", 1)
416
417
418 def handle_deprec(oldname, old, newname, new, default=None,
419 converter = lambda x:x):
420 if old is not None:
421 if new is None:
422 log.msg("WARNING: change source is using deprecated "
423 "addChange parameter '%s'" % oldname)
424 return converter(old)
425 raise TypeError("Cannot provide '%s' and '%s' to addChange"
426 % (oldname, newname))
427 if new is None:
428 new = default
429 return new
430
431 author = handle_deprec("who", who, "author", author)
432 is_dir = handle_deprec("isdir", isdir, "is_dir", is_dir,
433 default=0)
434 when_timestamp = handle_deprec("when", when,
435 "when_timestamp", when_timestamp,
436 converter=epoch2datetime)
437
438
439 for n in properties:
440 properties[n] = (properties[n], 'Change')
441
442 if codebase is None:
443 if self.config.codebaseGenerator is not None:
444 chdict = {
445 'changeid': None,
446 'author': author,
447 'files': files,
448 'comments': comments,
449 'is_dir': is_dir,
450 'revision': revision,
451 'when_timestamp': when_timestamp,
452 'branch': branch,
453 'category': category,
454 'revlink': revlink,
455 'properties': properties,
456 'repository': repository,
457 'project': project,
458 }
459 codebase = self.config.codebaseGenerator(chdict)
460 else:
461 codebase = ''
462
463 d = defer.succeed(None)
464 if src:
465
466 d.addCallback(lambda _ : users.createUserObject(self, author, src))
467
468
469 d.addCallback(lambda uid :
470 self.db.changes.addChange(author=author, files=files,
471 comments=comments, is_dir=is_dir,
472 revision=revision,
473 when_timestamp=when_timestamp,
474 branch=branch, category=category,
475 revlink=revlink, properties=properties,
476 repository=repository, codebase=codebase,
477 project=project, uid=uid))
478
479
480 d.addCallback(lambda changeid :
481 self.db.changes.getChange(changeid))
482 d.addCallback(lambda chdict :
483 changes.Change.fromChdict(self, chdict))
484
485 def notify(change):
486 msg = u"added change %s to database" % change
487 log.msg(msg.encode('utf-8', 'replace'))
488
489 if not self.config.db['db_poll_interval']:
490 self._change_subs.deliver(change)
491 return change
492 d.addCallback(notify)
493 return d
494
496 """
497 Request that C{callback} be called with each Change object added to the
498 cluster.
499
500 Note: this method will go away in 0.9.x
501 """
502 return self._change_subs.subscribe(callback)
503
505 """
506 Add a buildset to the buildmaster and act on it. Interface is
507 identical to
508 L{buildbot.db.buildsets.BuildsetConnectorComponent.addBuildset},
509 including returning a Deferred, but also potentially triggers the
510 resulting builds.
511 """
512 d = self.db.buildsets.addBuildset(**kwargs)
513 def notify((bsid,brids)):
514 log.msg("added buildset %d to database" % bsid)
515
516 self._new_buildset_subs.deliver(bsid=bsid, **kwargs)
517
518 if not self.config.db['db_poll_interval']:
519 for bn, brid in brids.iteritems():
520 self.buildRequestAdded(bsid=bsid, brid=brid,
521 buildername=bn)
522 return (bsid,brids)
523 d.addCallback(notify)
524 return d
525
527 """
528 Request that C{callback(bsid=bsid, ssid=ssid, reason=reason,
529 properties=properties, builderNames=builderNames,
530 external_idstring=external_idstring)} be called whenever a buildset is
531 added. Properties is a dictionary as expected for
532 L{BuildsetsConnectorComponent.addBuildset}.
533
534 Note that this only works for buildsets added on this master.
535
536 Note: this method will go away in 0.9.x
537 """
538 return self._new_buildset_subs.subscribe(callback)
539
540 @defer.inlineCallbacks
542 """
543 Instructs the master to check whether the buildset is complete,
544 and notify appropriately if it is.
545
546 Note that buildset completions are only reported on the master
547 on which the last build request completes.
548 """
549 brdicts = yield self.db.buildrequests.getBuildRequests(
550 bsid=bsid, complete=False)
551
552
553 if brdicts:
554 return
555
556 brdicts = yield self.db.buildrequests.getBuildRequests(bsid=bsid)
557
558
559 cumulative_results = SUCCESS
560 for brdict in brdicts:
561 if brdict['results'] not in (SUCCESS, WARNINGS):
562 cumulative_results = FAILURE
563
564
565 yield self.db.buildsets.completeBuildset(bsid, cumulative_results)
566
567
568 self._buildsetComplete(bsid, cumulative_results)
569
572
574 """
575 Request that C{callback(bsid, result)} be called whenever a
576 buildset is complete.
577
578 Note: this method will go away in 0.9.x
579 """
580 return self._complete_buildset_subs.subscribe(callback)
581
583 """
584 Notifies the master that a build request is available to be claimed;
585 this may be a brand new build request, or a build request that was
586 previously claimed and unclaimed through a timeout or other calamity.
587
588 @param bsid: containing buildset id
589 @param brid: buildrequest ID
590 @param buildername: builder named by the build request
591 """
592 self._new_buildrequest_subs.deliver(
593 dict(bsid=bsid, brid=brid, buildername=buildername))
594
596 """
597 Request that C{callback} be invoked with a dictionary with keys C{brid}
598 (the build request id), C{bsid} (buildset id) and C{buildername}
599 whenever a new build request is added to the database. Note that, due
600 to the delayed nature of subscriptions, the build request may already
601 be claimed by the time C{callback} is invoked.
602
603 Note: this method will go away in 0.9.x
604 """
605 return self._new_buildrequest_subs.subscribe(callback)
606
607
608
609
623
624 _last_processed_change = None
625 @defer.inlineCallbacks
682
683 _last_unclaimed_brids_set = None
684 _last_claim_cleanup = 0
685 @defer.inlineCallbacks
730
731
732
738 d.addCallback(get)
739 return d
740
742 "private wrapper around C{self.db.state.setState}"
743 d = self.getObjectId()
744 def set(objectid):
745 return self.db.state.setState(objectid, name, value)
746 d.addCallback(set)
747 return d
748
764
765 components.registerAdapter(Control, BuildMaster, interfaces.IControl)
766