cmlenz@13: # -*- coding: iso8859-1 -*- cmlenz@13: # cmlenz@13: # Copyright (C) 2005 Christopher Lenz cmlenz@13: # cmlenz@13: # Bitten is free software; you can redistribute it and/or cmlenz@13: # modify it under the terms of the GNU General Public License as cmlenz@13: # published by the Free Software Foundation; either version 2 of the cmlenz@13: # License, or (at your option) any later version. cmlenz@13: # cmlenz@13: # Trac is distributed in the hope that it will be useful, cmlenz@13: # but WITHOUT ANY WARRANTY; without even the implied warranty of cmlenz@13: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU cmlenz@13: # General Public License for more details. cmlenz@13: # cmlenz@13: # You should have received a copy of the GNU General Public License cmlenz@13: # along with this program; if not, write to the Free Software cmlenz@13: # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. cmlenz@13: # cmlenz@13: # Author: Christopher Lenz cmlenz@13: cmlenz@15: import logging cmlenz@13: import os cmlenz@13: import sys cmlenz@42: import tempfile cmlenz@13: import time cmlenz@13: cmlenz@61: from bitten.build import BuildError cmlenz@56: from bitten.recipe import Recipe cmlenz@48: from bitten.util import archive, beep, xmlio cmlenz@13: cmlenz@13: cmlenz@13: class Slave(beep.Initiator): cmlenz@51: """Build slave.""" cmlenz@13: cmlenz@13: def greeting_received(self, profiles): cmlenz@19: if OrchestrationProfileHandler.URI not in profiles: cmlenz@47: err = 'Peer does not support the Bitten orchestration profile' cmlenz@47: logging.error(err) cmlenz@47: raise beep.TerminateSession, err cmlenz@29: self.channels[0].profile.send_start([OrchestrationProfileHandler]) cmlenz@13: cmlenz@14: cmlenz@19: class OrchestrationProfileHandler(beep.ProfileHandler): cmlenz@19: """Handler for communication on the Bitten build orchestration profile from cmlenz@19: the perspective of the build slave. cmlenz@13: """ cmlenz@19: URI = 'http://bitten.cmlenz.net/beep/orchestration' cmlenz@13: cmlenz@51: def handle_connect(self): cmlenz@13: """Register with the build master.""" cmlenz@51: self.recipe_path = None cmlenz@51: cmlenz@56: def handle_reply(cmd, msgno, ansno, msg): cmlenz@15: if cmd == 'ERR': cmlenz@13: if msg.get_content_type() == beep.BEEP_XML: cmlenz@29: elem = xmlio.parse(msg.get_payload()) cmlenz@51: if elem.name == 'error': cmlenz@51: raise beep.TerminateSession, '%s (%d)' \ cmlenz@51: % (elem.gettext(), int(elem.attr['code'])) cmlenz@13: raise beep.TerminateSession, 'Registration failed!' cmlenz@15: logging.info('Registration successful') cmlenz@29: cmlenz@29: sysname, nodename, release, version, machine = os.uname() cmlenz@29: logging.info('Registering with build master as %s', nodename) cmlenz@29: xml = xmlio.Element('register', name=nodename)[ cmlenz@29: xmlio.Element('platform')[machine], cmlenz@29: xmlio.Element('os', family=os.name, version=release)[sysname] cmlenz@29: ] cmlenz@29: self.channel.send_msg(beep.MIMEMessage(xml), handle_reply) cmlenz@13: cmlenz@13: def handle_msg(self, msgno, msg): cmlenz@49: content_type = msg.get_content_type() cmlenz@51: if content_type == beep.BEEP_XML: cmlenz@51: elem = xmlio.parse(msg.get_payload()) cmlenz@51: if elem.name == 'build': cmlenz@51: # Received a build request cmlenz@51: self.recipe_path = elem.attr['recipe'] cmlenz@51: cmlenz@51: xml = xmlio.Element('proceed')[ cmlenz@51: xmlio.Element('accept', type='application/tar', cmlenz@51: encoding='bzip2'), cmlenz@51: xmlio.Element('accept', type='application/tar', cmlenz@51: encoding='gzip'), cmlenz@51: xmlio.Element('accept', type='application/zip') cmlenz@51: ] cmlenz@51: self.channel.send_rpy(msgno, beep.MIMEMessage(xml)) cmlenz@51: cmlenz@51: elif content_type in ('application/tar', 'application/zip'): cmlenz@51: # Received snapshot archive for build cmlenz@42: workdir = tempfile.mkdtemp(prefix='bitten') cmlenz@49: cmlenz@49: archive_name = msg.get('Content-Disposition') cmlenz@49: if not archive_name: cmlenz@49: if content_type == 'application/tar': cmlenz@49: encoding = msg.get('Content-Transfer-Encoding') cmlenz@49: if encoding == 'gzip': cmlenz@49: archive_name = 'snapshot.tar.gz' cmlenz@49: elif encoding == 'bzip2': cmlenz@49: archive_name = 'snapshot.tar.bz2' cmlenz@51: elif not encoding: cmlenz@49: archive_name = 'snapshot.tar' cmlenz@49: else: cmlenz@49: archive_name = 'snapshot.zip' cmlenz@42: archive_path = os.path.join(workdir, archive_name) cmlenz@42: file(archive_path, 'wb').write(msg.get_payload()) cmlenz@48: logging.debug('Received snapshot archive: %s', archive_path) cmlenz@42: cmlenz@48: # Unpack the archive cmlenz@48: prefix = archive.unpack(archive_path, workdir) cmlenz@48: path = os.path.join(workdir, prefix) cmlenz@57: logging.debug('Unpacked snapshot to %s' % path) cmlenz@48: cmlenz@48: # Fix permissions cmlenz@48: for root, dirs, files in os.walk(workdir, topdown=False): cmlenz@48: for dirname in dirs: cmlenz@48: os.chmod(os.path.join(root, dirname), 0700) cmlenz@48: for filename in files: cmlenz@48: os.chmod(os.path.join(root, filename), 0400) cmlenz@42: cmlenz@56: self.execute_build(msgno, path, self.recipe_path) cmlenz@42: cmlenz@56: def execute_build(self, msgno, basedir, recipe_path): cmlenz@56: logging.info('Building in directory %s using recipe %s', basedir, cmlenz@56: recipe_path) cmlenz@48: cmlenz@56: recipe = Recipe(recipe_path, basedir) cmlenz@56: cmlenz@56: xml = xmlio.Element('started') cmlenz@56: self.channel.send_ans(msgno, beep.MIMEMessage(xml)) cmlenz@56: cmlenz@56: for step in recipe: cmlenz@56: logging.info('Executing build step "%s"', step.id) cmlenz@56: try: cmlenz@56: for function, args in step: cmlenz@56: logging.debug('Executing command "%s"', function) cmlenz@60: function(recipe.ctxt, **args) cmlenz@56: xml = xmlio.Element('step', id=step.id, result='success', cmlenz@56: description=step.description) cmlenz@56: self.channel.send_ans(msgno, beep.MIMEMessage(xml)) cmlenz@56: except BuildError, e: cmlenz@56: xml = xmlio.Element('step', id=step.id, result='failure', cmlenz@56: description=step.description)[e] cmlenz@56: self.channel.send_ans(msgno, beep.MIMEMessage(xml)) cmlenz@56: cmlenz@56: self.channel.send_nul(msgno) cmlenz@13: cmlenz@13: cmlenz@31: def main(): cmlenz@56: from bitten import __version__ as VERSION cmlenz@19: from optparse import OptionParser cmlenz@19: cmlenz@19: parser = OptionParser(usage='usage: %prog [options] host [port]', cmlenz@19: version='%%prog %s' % VERSION) cmlenz@19: parser.add_option('--debug', action='store_const', dest='loglevel', cmlenz@19: const=logging.DEBUG, help='enable debugging output') cmlenz@19: parser.add_option('-v', '--verbose', action='store_const', dest='loglevel', cmlenz@19: const=logging.INFO, help='print as much as possible') cmlenz@19: parser.add_option('-q', '--quiet', action='store_const', dest='loglevel', cmlenz@19: const=logging.ERROR, help='print as little as possible') cmlenz@19: parser.set_defaults(loglevel=logging.WARNING) cmlenz@19: options, args = parser.parse_args() cmlenz@19: cmlenz@13: if len(args) < 1: cmlenz@19: parser.error('incorrect number of arguments') cmlenz@13: host = args[0] cmlenz@13: if len(args) > 1: cmlenz@18: try: cmlenz@18: port = int(args[1]) cmlenz@19: assert (1 <= port <= 65535), 'port number out of range' cmlenz@47: except (AssertionError, ValueError): cmlenz@19: parser.error('port must be an integer in the range 1-65535') cmlenz@13: else: cmlenz@13: port = 7633 cmlenz@13: cmlenz@19: logging.getLogger().setLevel(options.loglevel) cmlenz@13: cmlenz@13: slave = Slave(host, port) cmlenz@34: try: cmlenz@34: slave.run() cmlenz@34: except KeyboardInterrupt: cmlenz@34: slave.quit() cmlenz@31: cmlenz@31: if __name__ == '__main__': cmlenz@33: main()