~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/canonical/buildd/slave.py

  • Committer: Brad Crittenden
  • Date: 2011-11-17 19:41:24 UTC
  • mto: This revision was merged to the branch mainline in revision 14317.
  • Revision ID: bac@canonical.com-20111117194124-x834v6jscsknkl6y
RevertĀ 14311

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
# Authors: Daniel Silverstone <daniel.silverstone@canonical.com>
 
5
#      and Adam Conrad <adam.conrad@canonical.com>
 
6
 
 
7
# Buildd Slave implementation
 
8
 
 
9
__metaclass__ = type
 
10
 
 
11
import hashlib
 
12
import os
 
13
import re
 
14
import urllib2
 
15
import xmlrpclib
 
16
 
 
17
from twisted.internet import protocol
 
18
from twisted.internet import reactor
 
19
from twisted.internet import process
 
20
from twisted.web import xmlrpc
 
21
 
 
22
# cprov 20080611: in python2.4 posixfile.SEEK_END is deprecated and our
 
23
# importfascist-check denies its import. When we migrate to python2.5,
 
24
# we can use os.SEEK_END. See bug #239213.
 
25
SEEK_END = 2
 
26
 
 
27
devnull = open("/dev/null", "r")
 
28
 
 
29
 
 
30
def _sanitizeURLs(text_seq):
 
31
    """A generator that deletes URL passwords from a string sequence.
 
32
 
 
33
    This generator removes user/password data from URLs if embedded
 
34
    in the latter as follows: scheme://user:passwd@netloc/path.
 
35
 
 
36
    :param text_seq: A sequence of strings (that may contain URLs).
 
37
    :return: A (sanitized) line stripped of authentication credentials.
 
38
    """
 
39
    # This regular expression will be used to remove authentication
 
40
    # credentials from URLs.
 
41
    password_re = re.compile('://([^:]+:[^@]+@)(\S+)')
 
42
 
 
43
    for line in text_seq:
 
44
        sanitized_line = password_re.sub(r'://\2', line)
 
45
        yield sanitized_line
 
46
 
 
47
 
 
48
# XXX cprov 2005-06-28:
 
49
# RunCapture can be replaced with a call to
 
50
#
 
51
#   twisted.internet.utils.getProcessOutputAndValue
 
52
#
 
53
# when we start using Twisted 2.0.
 
54
class RunCapture(protocol.ProcessProtocol):
 
55
    """Run a command and capture its output to a slave's log"""
 
56
 
 
57
    def __init__(self, slave, callback):
 
58
        self.slave = slave
 
59
        self.notify = callback
 
60
        self.killCall = None
 
61
 
 
62
    def outReceived(self, data):
 
63
        """Pass on stdout data to the log."""
 
64
        self.slave.log(data)
 
65
 
 
66
    def errReceived(self, data):
 
67
        """Pass on stderr data to the log.
 
68
 
 
69
        With a bit of luck we won't interleave horribly."""
 
70
        self.slave.log(data)
 
71
 
 
72
    def processEnded(self, statusobject):
 
73
        """This method is called when a child process got terminated.
 
74
 
 
75
        Three actions are required at this point: identify if we are within an
 
76
        "aborting" process, eliminate pending calls to "kill" and invoke the
 
77
        programmed notification callback. We only really care about invoking
 
78
        the notification callback last thing in this method. The order
 
79
        of the rest of the method is not critical.
 
80
        """
 
81
        # finishing the ABORTING workflow
 
82
        if self.slave.builderstatus == BuilderStatus.ABORTING:
 
83
            self.slave.builderstatus = BuilderStatus.ABORTED
 
84
 
 
85
        # check if there is a pending request for kill the process,
 
86
        # in afirmative case simply cancel this request since it
 
87
        # already died.
 
88
        if self.killCall and self.killCall.active():
 
89
            self.killCall.cancel()
 
90
 
 
91
        # notify the slave, it'll perform the required actions
 
92
        self.notify(statusobject.value.exitCode)
 
93
 
 
94
 
 
95
class BuildManager(object):
 
96
    """Build Daemon slave build manager abstract parent"""
 
97
 
 
98
    def __init__(self, slave, buildid):
 
99
        """Create a BuildManager.
 
100
 
 
101
        :param slave: A `BuildDSlave`.
 
102
        :param buildid: Identifying string for this build.
 
103
        """
 
104
        object.__init__(self)
 
105
        self._buildid = buildid
 
106
        self._slave = slave
 
107
        self._unpackpath = slave._config.get("allmanagers", "unpackpath")
 
108
        self._cleanpath = slave._config.get("allmanagers", "cleanpath")
 
109
        self._mountpath = slave._config.get("allmanagers", "mountpath")
 
110
        self._umountpath = slave._config.get("allmanagers", "umountpath")
 
111
        self.is_archive_private = False
 
112
        self.home = os.environ['HOME']
 
113
 
 
114
    def runSubProcess(self, command, args):
 
115
        """Run a sub process capturing the results in the log."""
 
116
        self._subprocess = RunCapture(self._slave, self.iterate)
 
117
        self._slave.log("RUN: %s %r\n" % (command, args))
 
118
        childfds = {0: devnull.fileno(), 1: "r", 2: "r"}
 
119
        reactor.spawnProcess(
 
120
            self._subprocess, command, args, env=os.environ,
 
121
            path=self.home, childFDs=childfds)
 
122
 
 
123
    def doUnpack(self):
 
124
        """Unpack the build chroot."""
 
125
        self.runSubProcess(
 
126
            self._unpackpath,
 
127
            ["unpack-chroot", self._buildid, self._chroottarfile])
 
128
 
 
129
    def doCleanup(self):
 
130
        """Remove the build tree etc."""
 
131
        self.runSubProcess(self._cleanpath, ["remove-build", self._buildid])
 
132
 
 
133
        # Sanitize the URLs in the buildlog file if this is a build
 
134
        # in a private archive.
 
135
        if self.is_archive_private:
 
136
            self._slave.sanitizeBuildlog(self._slave.cachePath("buildlog"))
 
137
 
 
138
    def doMounting(self):
 
139
        """Mount things in the chroot, e.g. proc."""
 
140
        self.runSubProcess( self._mountpath,
 
141
                            ["mount-chroot", self._buildid])
 
142
 
 
143
    def doUnmounting(self):
 
144
        """Unmount the chroot."""
 
145
        self.runSubProcess( self._umountpath,
 
146
                            ["umount-chroot", self._buildid])
 
147
 
 
148
    def initiate(self, files, chroot, extra_args):
 
149
        """Initiate a build given the input files.
 
150
 
 
151
        Please note: the 'extra_args' dictionary may contain a boolean
 
152
        value keyed under the 'archive_private' string. If that value
 
153
        evaluates to True the build at hand is for a private archive.
 
154
        """
 
155
        os.mkdir("%s/build-%s" % (self.home, self._buildid))
 
156
        for f in files:
 
157
            os.symlink( self._slave.cachePath(files[f]),
 
158
                        "%s/build-%s/%s" % (self.home,
 
159
                                            self._buildid, f))
 
160
        self._chroottarfile = self._slave.cachePath(chroot)
 
161
 
 
162
        # Check whether this is a build in a private archive and
 
163
        # whether the URLs in the buildlog file should be sanitized
 
164
        # so that they do not contain any embedded authentication
 
165
        # credentials.
 
166
        if extra_args.get('archive_private'):
 
167
            self.is_archive_private = True
 
168
 
 
169
        self.runSubProcess(
 
170
            "/bin/echo", ["echo", "Forking build subprocess..."])
 
171
 
 
172
    def iterate(self, success):
 
173
        """Perform an iteration of the slave.
 
174
 
 
175
        The BuildManager tends to work by invoking several
 
176
        subprocesses in order. the iterate method is called by the
 
177
        object created by runSubProcess to gather the results of the
 
178
        sub process.
 
179
        """
 
180
        raise NotImplementedError("BuildManager should be subclassed to be "
 
181
                                  "used")
 
182
 
 
183
    def abort(self):
 
184
        """Abort the build by killing the subprocess."""
 
185
        if not self.alreadyfailed:
 
186
            self.alreadyfailed = True
 
187
        # Either SIGKILL and SIGTERM presents the same behavior,
 
188
        # the process is just killed some time after the signal was sent
 
189
        # 10 s ~ 40 s, and returns None as exit_code, instead of the normal
 
190
        # interger. See further info on DebianBuildermanager.iterate in
 
191
        # debian.py
 
192
        # XXX cprov 2005-09-02:
 
193
        # we may want to follow the canonical.tachandler kill process style,
 
194
        # which sends SIGTERM to the process wait a given timeout and if was
 
195
        # not killed sends a SIGKILL. IMO it only would be worth if we found
 
196
        # different behaviour than the previous described.
 
197
        self._subprocess.transport.signalProcess('TERM')
 
198
        # alternativelly to simply send SIGTERM, we can pend a request to
 
199
        # send SIGKILL to the process if nothing happened in 10 seconds
 
200
        # see base class process
 
201
        self._subprocess.killCall = reactor.callLater(10, self.kill)
 
202
 
 
203
    def kill(self):
 
204
        """Send SIGKILL to child process
 
205
 
 
206
        Mask exception generated when the child process has already exited.
 
207
        """
 
208
        try:
 
209
            self._subprocess.transport.signalProcess('KILL')
 
210
        except process.ProcessExitedAlready:
 
211
            self._slave.log("ABORTING: Process Exited Already\n")
 
212
 
 
213
class BuilderStatus:
 
214
    """Status values for the builder."""
 
215
 
 
216
    IDLE = "BuilderStatus.IDLE"
 
217
    BUILDING = "BuilderStatus.BUILDING"
 
218
    WAITING = "BuilderStatus.WAITING"
 
219
    ABORTING = "BuilderStatus.ABORTING"
 
220
    ABORTED = "BuilderStatus.ABORTED"
 
221
 
 
222
    UNKNOWNSUM = "BuilderStatus.UNKNOWNSUM"
 
223
    UNKNOWNBUILDER = "BuilderStatus.UNKNOWNBUILDER"
 
224
 
 
225
 
 
226
class BuildStatus:
 
227
    """Status values for builds themselves."""
 
228
 
 
229
    OK = "BuildStatus.OK"
 
230
    DEPFAIL = "BuildStatus.DEPFAIL"
 
231
    GIVENBACK = "BuildStatus.GIVENBACK"
 
232
    PACKAGEFAIL = "BuildStatus.PACKAGEFAIL"
 
233
    CHROOTFAIL = "BuildStatus.CHROOTFAIL"
 
234
    BUILDERFAIL = "BuildStatus.BUILDERFAIL"
 
235
 
 
236
 
 
237
class BuildDSlave(object):
 
238
    """Build Daemon slave. Implementation of most needed functions
 
239
    for a Build-Slave device.
 
240
    """
 
241
 
 
242
    def __init__(self, config):
 
243
        object.__init__(self)
 
244
        self._config = config
 
245
        self.builderstatus = BuilderStatus.IDLE
 
246
        self._cachepath = self._config.get("slave","filecache")
 
247
        self.buildstatus = BuildStatus.OK
 
248
        self.waitingfiles = {}
 
249
        self.builddependencies = ""
 
250
        self._log = None
 
251
 
 
252
        if not os.path.isdir(self._cachepath):
 
253
            raise ValueError("FileCache path is not a dir")
 
254
 
 
255
    def getArch(self):
 
256
        """Return the Architecture tag for the slave."""
 
257
        return self._config.get("slave","architecturetag")
 
258
 
 
259
    def cachePath(self, file):
 
260
        """Return the path in the cache of the file specified."""
 
261
        return os.path.join(self._cachepath, file)
 
262
 
 
263
    def setupAuthHandler(self, url, username, password):
 
264
        """Set up a BasicAuthHandler to open the url.
 
265
 
 
266
        :param url: The URL that needs authenticating.
 
267
        :param username: The username for authentication.
 
268
        :param password: The password for authentication.
 
269
        :return: The OpenerDirector instance.
 
270
 
 
271
        This helper installs a urllib2.HTTPBasicAuthHandler that will deal
 
272
        with any HTTP basic authentication required when opening the
 
273
        URL.
 
274
        """
 
275
        password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
 
276
        password_mgr.add_password(None, url, username, password)
 
277
        handler = urllib2.HTTPBasicAuthHandler(password_mgr)
 
278
        opener = urllib2.build_opener(handler)
 
279
        return opener
 
280
 
 
281
    def ensurePresent(self, sha1sum, url=None, username=None, password=None):
 
282
        """Ensure we have the file with the checksum specified.
 
283
 
 
284
        Optionally you can provide the librarian URL and
 
285
        the build slave will fetch the file if it doesn't have it.
 
286
        Return a tuple containing: (<present>, <info>)
 
287
        """
 
288
        extra_info = 'No URL'
 
289
        if url is not None:
 
290
            extra_info = 'Cache'
 
291
            if not os.path.exists(self.cachePath(sha1sum)):
 
292
                self.log('Fetching %s by url %s' % (sha1sum, url))
 
293
                if username:
 
294
                    opener = self.setupAuthHandler(
 
295
                        url, username, password).open
 
296
                else:
 
297
                    opener = urllib2.urlopen
 
298
                try:
 
299
                    f = opener(url)
 
300
                # Don't change this to URLError without thoroughly
 
301
                # testing for regressions. For now, just suppress
 
302
                # the PyLint warnings.
 
303
                # pylint: disable-msg=W0703
 
304
                except Exception, info:
 
305
                    extra_info = 'Error accessing Librarian: %s' % info
 
306
                    self.log(extra_info)
 
307
                else:
 
308
                    of = open(self.cachePath(sha1sum), "w")
 
309
                    # Upped for great justice to 256k
 
310
                    check_sum = hashlib.sha1()
 
311
                    for chunk in iter(lambda: f.read(256*1024), ''):
 
312
                        of.write(chunk)
 
313
                        check_sum.update(chunk)
 
314
                    of.close()
 
315
                    f.close()
 
316
                    extra_info = 'Download'
 
317
                    if check_sum.hexdigest() != sha1sum:
 
318
                        os.remove(self.cachePath(sha1sum))
 
319
                        extra_info = "Digests did not match, removing again!"
 
320
                    self.log(extra_info)
 
321
        return (os.path.exists(self.cachePath(sha1sum)), extra_info)
 
322
 
 
323
    def storeFile(self, content):
 
324
        """Take the provided content and store it in the file cache."""
 
325
        sha1sum = hashlib.sha1(content).hexdigest()
 
326
        present, info = self.ensurePresent(sha1sum)
 
327
        if present:
 
328
            return sha1sum
 
329
        f = open(self.cachePath(sha1sum), "w")
 
330
        f.write(content)
 
331
        f.close()
 
332
        return sha1sum
 
333
 
 
334
    def addWaitingFile(self, path):
 
335
        """Add a file to the cache and store its details for reporting."""
 
336
        fn = os.path.basename(path)
 
337
        f = open(path)
 
338
        try:
 
339
            self.waitingfiles[fn] = self.storeFile(f.read())
 
340
        finally:
 
341
            f.close()
 
342
 
 
343
    def fetchFile(self, sha1sum):
 
344
        """Fetch the file of the given sha1sum."""
 
345
        present, info = self.ensurePresent(sha1sum)
 
346
        if not present:
 
347
            raise ValueError("Unknown SHA1sum %s" % sha1sum)
 
348
        f = open(self.cachePath(sha1sum), "r")
 
349
        c = f.read()
 
350
        f.close()
 
351
        return c
 
352
 
 
353
    def abort(self):
 
354
        """Abort the current build."""
 
355
        # XXX: dsilvers: 2005-01-21: Current abort mechanism doesn't wait
 
356
        # for abort to complete. This is potentially an issue in a heavy
 
357
        # load situation.
 
358
        if self.builderstatus != BuilderStatus.BUILDING:
 
359
            # XXX: Should raise a known Fault so that the client can make
 
360
            # useful decisions about the error!
 
361
            raise ValueError("Slave is not BUILDING when asked to abort")
 
362
        self.manager.abort()
 
363
        self.builderstatus = BuilderStatus.ABORTING
 
364
 
 
365
    def clean(self):
 
366
        """Clean up pending files and reset the internal build state."""
 
367
        if self.builderstatus not in [BuilderStatus.WAITING,
 
368
                                      BuilderStatus.ABORTED]:
 
369
            raise ValueError('Slave is not WAITING|ABORTED when asked'
 
370
                             'to clean')
 
371
        for f in self.waitingfiles:
 
372
            os.remove(self.cachePath(self.waitingfiles[f]))
 
373
        self.builderstatus = BuilderStatus.IDLE
 
374
        if self._log is not None:
 
375
            self._log.close()
 
376
            os.remove(self.cachePath("buildlog"))
 
377
            self._log = None
 
378
        self.waitingfiles = {}
 
379
        self.builddependencies = ""
 
380
        self.manager = None
 
381
        self.buildstatus = BuildStatus.OK
 
382
 
 
383
    def log(self, data):
 
384
        """Write the provided data to the log."""
 
385
        if self._log is not None:
 
386
            self._log.write(data)
 
387
            self._log.flush()
 
388
        if data.endswith("\n"):
 
389
            data = data[:-1]
 
390
        print "Build log: " + data
 
391
 
 
392
    def getLogTail(self):
 
393
        """Return the tail of the log.
 
394
 
 
395
        If the buildlog is not yet opened for writing (self._log is None),
 
396
        return a empty string.
 
397
 
 
398
        It safely tries to open the 'buildlog', if it doesn't exist, due to
 
399
        job cleanup or buildlog sanitization race-conditions, it also returns
 
400
        an empty string.
 
401
 
 
402
        When the 'buildlog' is present it return up to 2 KiB character of
 
403
        the end of the file.
 
404
 
 
405
        The returned content will be 'sanitized', see `_sanitizeURLs` for
 
406
        further information.
 
407
        """
 
408
        if self._log is None:
 
409
            return ""
 
410
 
 
411
        rlog = None
 
412
        try:
 
413
            try:
 
414
                rlog = open(self.cachePath("buildlog"), "r")
 
415
            except IOError:
 
416
                ret = ""
 
417
            else:
 
418
                # We rely on good OS practices that keep the file handler
 
419
                # usable once it's opened. So, if open() is ok, a subsequent
 
420
                # seek/tell/read will be safe.
 
421
                rlog.seek(0, SEEK_END)
 
422
                count = rlog.tell()
 
423
                if count > 2048:
 
424
                    count = 2048
 
425
                rlog.seek(-count, SEEK_END)
 
426
                ret = rlog.read(count)
 
427
        finally:
 
428
            if rlog is not None:
 
429
                rlog.close()
 
430
 
 
431
        if self.manager.is_archive_private:
 
432
            # This is a build in a private archive. We need to scrub
 
433
            # the URLs contained in the buildlog excerpt in order to
 
434
            # avoid leaking passwords.
 
435
            log_lines = ret.splitlines()
 
436
 
 
437
            # Please note: we are throwing away the first line (of the
 
438
            # excerpt to be scrubbed) because it may be cut off thus
 
439
            # thwarting the detection of embedded passwords.
 
440
            clean_content_iter = _sanitizeURLs(log_lines[1:])
 
441
            ret = '\n'.join(clean_content_iter)
 
442
 
 
443
        return ret
 
444
 
 
445
    def startBuild(self, manager):
 
446
        """Start a build with the provided BuildManager instance."""
 
447
        if self.builderstatus != BuilderStatus.IDLE:
 
448
            raise ValueError("Slave is not IDLE when asked to start building")
 
449
        self.manager = manager
 
450
        self.builderstatus = BuilderStatus.BUILDING
 
451
        self.emptyLog()
 
452
 
 
453
    def emptyLog(self):
 
454
        """Empty the log and start again."""
 
455
        if self._log is not None:
 
456
            self._log.close()
 
457
        self._log = open(self.cachePath("buildlog"), "w")
 
458
 
 
459
    def builderFail(self):
 
460
        """Cease building because the builder has a problem."""
 
461
        if self.builderstatus != BuilderStatus.BUILDING:
 
462
            raise ValueError("Slave is not BUILDING when set to BUILDERFAIL")
 
463
        self.buildstatus = BuildStatus.BUILDERFAIL
 
464
 
 
465
    def chrootFail(self):
 
466
        """Cease building because the chroot could not be created or contained
 
467
        a set of package control files which couldn't upgrade themselves, or
 
468
        yet a lot of causes that imply the CHROOT is corrupted not the
 
469
        package.
 
470
        """
 
471
        if self.builderstatus != BuilderStatus.BUILDING:
 
472
            raise ValueError("Slave is not BUILDING when set to CHROOTFAIL")
 
473
        self.buildstatus = BuildStatus.CHROOTFAIL
 
474
 
 
475
    def buildFail(self):
 
476
        """Cease building because the package failed to build."""
 
477
        if self.builderstatus != BuilderStatus.BUILDING:
 
478
            raise ValueError("Slave is not BUILDING when set to PACKAGEFAIL")
 
479
        self.buildstatus = BuildStatus.PACKAGEFAIL
 
480
 
 
481
    def buildOK(self):
 
482
        """Having passed all possible failure states, mark a build as OK."""
 
483
        if self.builderstatus != BuilderStatus.BUILDING:
 
484
            raise ValueError("Slave is not BUILDING when set to OK")
 
485
        self.buildstatus = BuildStatus.OK
 
486
 
 
487
    def depFail(self, dependencies):
 
488
        """Cease building due to a dependency issue."""
 
489
        if self.builderstatus != BuilderStatus.BUILDING:
 
490
            raise ValueError("Slave is not BUILDING when set to DEPFAIL")
 
491
        self.buildstatus = BuildStatus.DEPFAIL
 
492
        self.builddependencies = dependencies
 
493
 
 
494
    def giveBack(self):
 
495
        """Give-back package due to a transient buildd/archive issue."""
 
496
        if self.builderstatus != BuilderStatus.BUILDING:
 
497
            raise ValueError("Slave is not BUILDING when set to GIVENBACK")
 
498
        self.buildstatus = BuildStatus.GIVENBACK
 
499
 
 
500
    def buildComplete(self):
 
501
        """Mark the build as complete and waiting interaction from the build
 
502
        daemon master.
 
503
        """
 
504
        if self.builderstatus != BuilderStatus.BUILDING:
 
505
            raise ValueError("Slave is not BUILDING when told build is "
 
506
                             "complete")
 
507
        self.builderstatus = BuilderStatus.WAITING
 
508
 
 
509
    def sanitizeBuildlog(self, log_path):
 
510
        """Removes passwords from buildlog URLs.
 
511
 
 
512
        Because none of the URLs to be processed are expected to span
 
513
        multiple lines and because build log files are potentially huge
 
514
        they will be processed line by line.
 
515
 
 
516
        :param log_path: The path to the buildlog file that is to be
 
517
            sanitized.
 
518
        :type log_path: ``str``
 
519
        """
 
520
        # First move the buildlog file that is to be sanitized out of
 
521
        # the way.
 
522
        unsanitized_path = self.cachePath(
 
523
            os.path.basename(log_path) + '.unsanitized')
 
524
        os.rename(log_path, unsanitized_path)
 
525
 
 
526
        # Open the unsanitized buildlog file for reading.
 
527
        unsanitized_file = open(unsanitized_path)
 
528
 
 
529
        # Open the file that will hold the resulting, sanitized buildlog
 
530
        # content for writing.
 
531
        sanitized_file = None
 
532
 
 
533
        try:
 
534
            sanitized_file = open(log_path, 'w')
 
535
 
 
536
            # Scrub the buildlog file line by line
 
537
            clean_content_iter = _sanitizeURLs(unsanitized_file)
 
538
            for line in clean_content_iter:
 
539
                sanitized_file.write(line)
 
540
        finally:
 
541
            # We're done with scrubbing, close the file handles.
 
542
            unsanitized_file.close()
 
543
            if sanitized_file is not None:
 
544
                sanitized_file.close()
 
545
 
 
546
 
 
547
class XMLRPCBuildDSlave(xmlrpc.XMLRPC):
 
548
    """XMLRPC build daemon slave management interface"""
 
549
 
 
550
    def __init__(self, config):
 
551
        xmlrpc.XMLRPC.__init__(self, allowNone=True)
 
552
        # The V1.0 new-style protocol introduces string-style protocol
 
553
        # versions of the form 'MAJOR.MINOR', the protocol is '1.0' for now
 
554
        # implying the presence of /filecache/ /filecache/buildlog and
 
555
        # the reduced and optimised XMLRPC interface.
 
556
        self.protocolversion = '1.0'
 
557
        self.slave = BuildDSlave(config)
 
558
        self._builders = {}
 
559
        print "Initialized"
 
560
 
 
561
    def registerBuilder(self, builderclass, buildertag):
 
562
        self._builders[buildertag] = builderclass
 
563
 
 
564
    def xmlrpc_echo(self, *args):
 
565
        """Echo the argument back."""
 
566
        return args
 
567
 
 
568
    def xmlrpc_info(self):
 
569
        """Return the protocol version and the builder methods supported."""
 
570
        return (self.protocolversion, self.slave.getArch(),
 
571
                self._builders.keys())
 
572
 
 
573
    def xmlrpc_status(self):
 
574
        """Return the status of the build daemon.
 
575
 
 
576
        Depending on the builder status we return differing amounts of
 
577
        data. We do however always return the builder status as the first
 
578
        value.
 
579
        """
 
580
        status = self.slave.builderstatus
 
581
        statusname = status.split('.')[-1]
 
582
        func = getattr(self, "status_" + statusname, None)
 
583
        if func is None:
 
584
            raise ValueError("Unknown status '%s'" % status)
 
585
        return (status, ) + func()
 
586
 
 
587
    def status_IDLE(self):
 
588
        """Handler for xmlrpc_status IDLE.
 
589
 
 
590
        Returns a tuple containing a empty string since there's nothing
 
591
        to report.
 
592
        """
 
593
        # keep the result code sane
 
594
        return ('', )
 
595
 
 
596
    def status_BUILDING(self):
 
597
        """Handler for xmlrpc_status BUILDING.
 
598
 
 
599
        Returns the build id and up to one kilobyte of log tail
 
600
        """
 
601
        tail = self.slave.getLogTail()
 
602
        return (self.buildid, xmlrpclib.Binary(tail))
 
603
 
 
604
    def status_WAITING(self):
 
605
        """Handler for xmlrpc_status WAITING.
 
606
 
 
607
        Returns the build id and the set of files waiting to be returned
 
608
        unless the builder failed in which case we return the buildstatus
 
609
        and the build id but no file set.
 
610
        """
 
611
        if self.slave.buildstatus in (BuildStatus.OK, BuildStatus.PACKAGEFAIL,
 
612
                                      BuildStatus.DEPFAIL):
 
613
            return (self.slave.buildstatus, self.buildid,
 
614
                    self.slave.waitingfiles, self.slave.builddependencies)
 
615
        return (self.slave.buildstatus, self.buildid)
 
616
 
 
617
    def status_ABORTED(self):
 
618
        """Handler for xmlrpc_status ABORTED.
 
619
 
 
620
        The only action the master can take is clean, other than ask status,
 
621
        of course, it returns the build id only.
 
622
        """
 
623
        return (self.buildid, )
 
624
 
 
625
    def status_ABORTING(self):
 
626
        """Handler for xmlrpc_status ABORTING.
 
627
 
 
628
        This state means the builder performing the ABORT command and is
 
629
        not able to do anything else than answer its status, returns the
 
630
        build id only.
 
631
        """
 
632
        return (self.buildid, )
 
633
 
 
634
    def xmlrpc_ensurepresent(self, sha1sum, url, username, password):
 
635
        """Attempt to ensure the given file is present."""
 
636
        return self.slave.ensurePresent(sha1sum, url, username, password)
 
637
 
 
638
    def xmlrpc_abort(self):
 
639
        """Abort the current build."""
 
640
        self.slave.abort()
 
641
        return BuilderStatus.ABORTING
 
642
 
 
643
    def xmlrpc_clean(self):
 
644
        """Clean up the waiting files and reset the slave's internal state."""
 
645
        self.slave.clean()
 
646
        return BuilderStatus.IDLE
 
647
 
 
648
    def xmlrpc_build(self, buildid, builder, chrootsum, filemap, args):
 
649
        """Check if requested arguments are sane and initiate build procedure
 
650
 
 
651
        return a tuple containing: (<builder_status>, <info>)
 
652
 
 
653
        """
 
654
        # check requested builder
 
655
        if not builder in self._builders:
 
656
            extra_info = "%s not in %r" % (builder, self._builders.keys())
 
657
            return (BuilderStatus.UNKNOWNBUILDER, extra_info)
 
658
        # check requested chroot availability
 
659
        chroot_present, info = self.slave.ensurePresent(chrootsum)
 
660
        if not chroot_present:
 
661
            extra_info = """CHROOTSUM -> %s
 
662
            ***** INFO *****
 
663
            %s
 
664
            ****************
 
665
            """ % (chrootsum, info)
 
666
            return (BuilderStatus.UNKNOWNSUM, extra_info)
 
667
        # check requested files availability
 
668
        for filesum in filemap.itervalues():
 
669
            file_present, info = self.slave.ensurePresent(filesum)
 
670
            if not file_present:
 
671
                extra_info = """FILESUM -> %s
 
672
                ***** INFO *****
 
673
                %s
 
674
                ****************
 
675
                """ % (filesum, info)
 
676
                return (BuilderStatus.UNKNOWNSUM, extra_info)
 
677
        # check buildid sanity
 
678
        if buildid is None or buildid == "" or buildid == 0:
 
679
            raise ValueError(buildid)
 
680
 
 
681
        # builder is available, buildd is non empty,
 
682
        # filelist is consistent, chrootsum is available, let's initiate...
 
683
        self.buildid = buildid
 
684
        self.slave.startBuild(self._builders[builder](self.slave, buildid))
 
685
        self.slave.manager.initiate(filemap, chrootsum, args)
 
686
        return (BuilderStatus.BUILDING, buildid)