# HG changeset patch # User cmlenz # Date 1119801990 0 # Node ID 5caccd7b247e5e2a9ff287866eb6c86f09e92ff3 # Parent 0d5ad32948b7a63d129f7863ad44b10fc9ab60bd Proper archive format negotiation; improved representation of parsed XML content in {{{bitten.util.xmlio}}}. diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -65,11 +65,6 @@ # already been built builds = Build.select(self.env, config.name, node.rev) if not list(builds): - snapshot = archive.pack(self.env, repos, node.path, - node.rev, config.name) - logging.info('Created snapshot archive at %s' % snapshot) - self.snapshots[(config.name, str(node.rev))] = snapshot - logging.info('Enqueuing build of configuration "%s" as of revision [%s]', config.name, node.rev) build = Build(self.env) @@ -88,14 +83,28 @@ for build in Build.select(self.env, status=Build.PENDING): logging.debug('Building configuration "%s" as of revision [%s]', build.config, build.rev) - snapshot = self.snapshots[(build.config, build.rev)] for slave in self.slaves.values(): active_builds = Build.select(self.env, slave=slave.name, status=Build.IN_PROGRESS) if not list(active_builds): - slave.send_build(build, snapshot) + slave.initiate_build(build) break + def get_snapshot(self, build, type, encoding): + formats = { + ('application/tar', 'bzip2'): 'bzip2', + ('application/tar', 'gzip'): 'bzip', + ('application/tar', None): 'tar', + ('application/zip', None): 'zip', + } + if not (build.config, build.rev, type, encoding) in self.snapshots: + config = BuildConfig(self.env, build.config) + snapshot = archive.pack(self.env, path=config.path, rev=build.rev, + prefix=config.name, + format=formats[(type, encoding)]) + logging.info('Prepared snapshot archive at %s' % snapshot) + self.snapshots[(build.config, build.rev, type, encoding)] = snapshot + return self.snapshots[(build.config, build.rev, type, encoding)] class OrchestrationProfileHandler(beep.ProfileHandler): """Handler for communication on the Bitten build orchestration profile from @@ -103,41 +112,40 @@ """ URI = 'http://bitten.cmlenz.net/beep/orchestration' - def handle_connect(self, init_elem=None): + def handle_connect(self): self.master = self.session.listener assert self.master - self.building = False self.name = None def handle_disconnect(self): del self.master.slaves[self.name] + + for build in Build.select(self.master.env, slave=self.name, + status=Build.IN_PROGRESS): + logging.info('Build [%s] of "%s" by %s cancelled', build.rev, + build.config, self.name) + build.slave = None + build.status = Build.PENDING + build.time = None + build.update() + break logging.info('Unregistered slave "%s"', self.name) - if self.building: - for build in Build.select(self.master.env, slave=self.name, - status=Build.IN_PROGRESS): - logging.info('Build [%s] of "%s" by %s cancelled', build.rev, - build.config, self.name) - build.slave = None - build.status = Build.PENDING - build.time = None - build.update() - break def handle_msg(self, msgno, msg): assert msg.get_content_type() == beep.BEEP_XML elem = xmlio.parse(msg.get_payload()) - if elem.tagname == 'register': + if elem.name == 'register': platform, os, os_family, os_version = None, None, None, None - for child in elem['*']: - if child.tagname == 'platform': + for child in elem.children(): + if child.name == 'platform': platform = child.gettext() - elif child.tagname == 'os': + elif child.name == 'os': os = child.gettext() - os_family = child.family - os_version = child.version + os_family = child.attr['family'] + os_version = child.attr['version'] - self.name = elem.name + self.name = elem.attr['name'] self.master.slaves[self.name] = self xml = xmlio.Element('ok') @@ -145,17 +153,49 @@ logging.info('Registered slave "%s" (%s running %s %s [%s])', self.name, platform, os, os_version, os_family) - def send_build(self, build, snapshot_path, handle_reply=None): - logging.debug('Initiating build on slave %s', self.name) - self.building = True + def initiate_build(self, build): + logging.debug('Initiating build of "%s" on slave %s', build.config, + self.name) def handle_reply(cmd, msgno, msg): if cmd == 'ERR': if msg.get_content_type() == beep.BEEP_XML: elem = xmlio.parse(msg.get_payload()) - if elem.tagname == 'error': + if elem.name == 'error': logging.warning('Slave refused build request: %s (%d)', - elem.gettext(), int(elem.code)) + elem.gettext(), int(elem.attr['code'])) + return + + elem = xmlio.parse(msg.get_payload()) + assert elem.name == 'proceed' + type = encoding = None + for child in elem.children('accept'): + type, encoding = child.attr['type'], child.attr.get('encoding') + if (type, encoding) in (('application/tar', 'gzip'), + ('application/tar', 'bzip2'), + ('application/tar', None), + ('application/zip', None)): + break + if not type: + xml = xmlio.Element('error', code=550)[ + 'None of the supported archive formats accepted' + ] + self.channel.send_err(beep.MIMEMessage(xml)) + return + self.send_snapshot(build, type, encoding) + + xml = xmlio.Element('build', recipe='recipe.xml') + self.channel.send_msg(beep.MIMEMessage(xml), handle_reply=handle_reply) + + def send_snapshot(self, build, type, encoding): + def handle_reply(cmd, msgno, msg): + if cmd == 'ERR': + if msg.get_content_type() == beep.BEEP_XML: + elem = xmlio.parse(msg.get_payload()) + if elem.name == 'error': + logging.warning('Slave did not accept archive: %s (%d)', + elem.gettext(), int(elem.attr['code'])) + return build.slave = self.name build.time = int(time.time()) build.status = Build.IN_PROGRESS @@ -165,11 +205,11 @@ # TODO: should not block while reading the file; rather stream it using # asyncore push_with_producer() + snapshot_path = self.master.get_snapshot(build, type, encoding) snapshot_name = os.path.basename(snapshot_path) message = beep.MIMEMessage(file(snapshot_path).read(), - content_type='application/tar', content_disposition=snapshot_name, - content_encoding='gzip') + content_type=type, content_encoding=encoding) self.channel.send_msg(message, handle_reply=handle_reply) diff --git a/bitten/recipe.py b/bitten/recipe.py --- a/bitten/recipe.py +++ b/bitten/recipe.py @@ -19,9 +19,9 @@ # Author: Christopher Lenz import os.path -from xml.dom import minidom from bitten import BuildError +from bitten.util import xmlio __all__ = ['Recipe'] @@ -33,35 +33,31 @@ their keyword arguments. """ - def __init__(self, node): - self._node = node - self.id = node.getAttribute('id') - self.description = node.getAttribute('description') + def __init__(self, elem): + self._elem = elem + self.id = elem.id + self.description = elem.description def __iter__(self): - for child in [c for c in self._node.childNodes if c.nodeType == 1]: - if child.namespaceURI: + for child in self._elem: + if child.namespace: # Commands - yield self._translate(child) - elif child.tagName == 'reports': + yield self._translate(child), child.attrs + elif child.name == 'reports': # Reports - for child in [c for c in child.childNodes if c.nodeType == 1]: - yield self._translate(child) + for grandchild in child: + yield self._translate(grandchild), grandchild.attrs else: - raise BuildError, "Unknown element <%s>" % child.tagName + raise BuildError, "Unknown element <%s>" % child.name - def _translate(self, node): - if not node.namespaceURI.startswith('bitten:'): - # Ignore elements in a foreign namespace + def _translate(self, elem): + if not elem.namespace.startswith('bitten:'): + # Ignore elements in foreign namespaces return None - module = __import__(node.namespaceURI[7:], globals(), locals(), - node.localName) - func = getattr(module, node.localName) - attrs = {} - for name, value in node.attributes.items(): - attrs[name.encode()] = value.encode() - return func, attrs + module = __import__(elem.namespace[7:], globals(), locals(), elem.name) + func = getattr(module, elem.name) + return func class Recipe(object): @@ -74,10 +70,11 @@ self.filename = filename self.basedir = basedir self.path = os.path.join(basedir, filename) - self.root = minidom.parse(self.path).documentElement - self.description = self.root.getAttribute('description') + self.root = xmlio.parse(file(self.path, 'r')) + assert self.root.name == 'build' + self.description = self.root.attr['description'] def __iter__(self): """Provide an iterator over the individual steps of the recipe.""" - for child in self.root.getElementsByTagName('step'): + for child in self.root.children('step'): yield Step(child) diff --git a/bitten/slave.py b/bitten/slave.py --- a/bitten/slave.py +++ b/bitten/slave.py @@ -29,6 +29,7 @@ class Slave(beep.Initiator): + """Build slave.""" def greeting_received(self, profiles): if OrchestrationProfileHandler.URI not in profiles: @@ -44,15 +45,17 @@ """ URI = 'http://bitten.cmlenz.net/beep/orchestration' - def handle_connect(self, init_elem=None): + def handle_connect(self): """Register with the build master.""" + self.recipe_path = None + def handle_reply(cmd, msgno, msg): if cmd == 'ERR': if msg.get_content_type() == beep.BEEP_XML: elem = xmlio.parse(msg.get_payload()) - if elem.tagname == 'error': - raise beep.TerminateSession, \ - '%s (%d)' % (elem.gettext(), int(elem.code)) + if elem.name == 'error': + raise beep.TerminateSession, '%s (%d)' \ + % (elem.gettext(), int(elem.attr['code'])) raise beep.TerminateSession, 'Registration failed!' logging.info('Registration successful') @@ -66,7 +69,23 @@ def handle_msg(self, msgno, msg): content_type = msg.get_content_type() - if content_type in ('application/tar', 'application/zip'): + if content_type == beep.BEEP_XML: + elem = xmlio.parse(msg.get_payload()) + if elem.name == 'build': + # Received a build request + self.recipe_path = elem.attr['recipe'] + + xml = xmlio.Element('proceed')[ + xmlio.Element('accept', type='application/tar', + encoding='bzip2'), + xmlio.Element('accept', type='application/tar', + encoding='gzip'), + xmlio.Element('accept', type='application/zip') + ] + self.channel.send_rpy(msgno, beep.MIMEMessage(xml)) + + elif content_type in ('application/tar', 'application/zip'): + # Received snapshot archive for build workdir = tempfile.mkdtemp(prefix='bitten') archive_name = msg.get('Content-Disposition') @@ -77,7 +96,7 @@ archive_name = 'snapshot.tar.gz' elif encoding == 'bzip2': archive_name = 'snapshot.tar.bz2' - else: + elif not encoding: archive_name = 'snapshot.tar' else: archive_name = 'snapshot.zip' @@ -100,11 +119,12 @@ xml = xmlio.Element('ok') self.channel.send_rpy(msgno, beep.MIMEMessage(xml)) - # TODO: Start the build process + self.execute_build(path, self.recipe_path) - else: - xml = xmlio.Element('error', code=500)['Sorry, what?'] - self.channel.send_err(msgno, beep.MIMEMessage(xml)) + def execute_build(self, basedir, recipe): + logging.info('Would now build in directory %s using recipe %s', + basedir, recipe) + # TODO: Start the build process def main(): diff --git a/bitten/tests/recipe.py b/bitten/tests/recipe.py --- a/bitten/tests/recipe.py +++ b/bitten/tests/recipe.py @@ -35,10 +35,10 @@ def tearDown(self): os.unlink(os.path.join(self.temp_dir, 'recipe.xml')) - def testDescription(self): + def test_description(self): self.recipe_xml.write('' - '' - '') + '' + '') self.recipe_xml.close() recipe = Recipe(basedir=self.temp_dir) self.assertEqual('test', recipe.description) diff --git a/bitten/util/archive.py b/bitten/util/archive.py --- a/bitten/util/archive.py +++ b/bitten/util/archive.py @@ -27,7 +27,8 @@ _formats = {'gzip': ('.tar.gz', 'gz'), 'bzip2': ('.tar.bz2', 'bz2'), 'zip': ('.zip', None)} -def pack(env, repos=None, path=None, rev=None, prefix=None, format='gzip'): +def pack(env, repos=None, path=None, rev=None, prefix=None, format='gzip', + overwrite=False): if repos is None: repos = env.get_repository() root = repos.get_node(path or '/', rev) @@ -44,6 +45,9 @@ prefix += '_r%s' % root.rev filename = os.path.join(filedir, prefix + _formats[format][0]) + if not overwrite and os.path.isfile(filename): + return filename + if format in ('bzip2', 'gzip'): archive = tarfile.open(filename, 'w:' + _formats[format][1]) else: diff --git a/bitten/util/beep.py b/bitten/util/beep.py --- a/bitten/util/beep.py +++ b/bitten/util/beep.py @@ -586,26 +586,26 @@ assert message.get_content_type() == BEEP_XML elem = xmlio.parse(message.get_payload()) - if elem.tagname == 'start': - channelno = int(elem.number) + if elem.name == 'start': + channelno = int(elem.attr['number']) if channelno in self.session.channels: self.send_error(msgno, 550, 'Channel already in use') return - for profile in elem['profile']: - if profile.uri in self.session.profiles: + for profile in elem.children('profile'): + if profile.attr['uri'] in self.session.profiles: logging.debug('Start channel %s for profile <%s>', - elem.number, profile.uri) + elem.attr['number'], profile.attr['uri']) channel = Channel(self.session, channelno, - self.session.profiles[profile.uri]) + self.session.profiles[profile.attr['uri']]) self.session.channels[channelno] = channel - xml = xmlio.Element('profile', uri=profile.uri) + xml = xmlio.Element('profile', uri=profile.attr['uri']) self.channel.send_rpy(msgno, MIMEMessage(xml)) return self.send_error(msgno, 550, 'None of the requested profiles is supported') - elif elem.tagname == 'close': - channelno = int(elem.number) + elif elem.name == 'close': + channelno = int(elem.attr['number']) if not channelno in self.session.channels: self.send_error(msgno, 550, 'Channel not open') return @@ -626,9 +626,9 @@ assert message.get_content_type() == BEEP_XML elem = xmlio.parse(message.get_payload()) - if elem.tagname == 'greeting': + if elem.name == 'greeting': if isinstance(self.session, Initiator): - profiles = [profile.uri for profile in elem['profile']] + profiles = [p.attr['uri'] for p in elem.children('profile')] self.session.greeting_received(profiles) else: # and are handled by callbacks @@ -641,9 +641,9 @@ # TODO: Terminate the session, I guess assert message.get_content_type() == BEEP_XML elem = xmlio.parse(message.get_payload()) - assert elem.tagname == 'error' + assert elem.name == 'error' logging.warning('Received error in response to message #%d: %s (%d)', - msgno, elem.gettext(), int(elem.code)) + msgno, elem.gettext(), int(elem.attr['code'])) def send_close(self, channelno=0, code=200, handle_ok=None, handle_error=None): @@ -659,7 +659,7 @@ elif cmd == 'ERR': elem = xmlio.parse(message.get_payload()) text = elem.gettext() - code = int(elem.code) + code = int(elem.attr['code']) logging.debug('Peer refused to start channel %d: %s (%d)', channelno, text, code) if handle_error is not None: @@ -690,18 +690,18 @@ def handle_reply(cmd, msgno, message): if cmd == 'RPY': elem = xmlio.parse(message.get_payload()) - for cls in [cls for cls in profiles if cls.URI == elem.uri]: + for cls in [p for p in profiles if p.URI == elem.attr['uri']]: logging.debug('Channel %d started with profile %s', - channelno, elem.uri) + channelno, elem.attr['uri']) self.session.channels[channelno] = Channel(self.session, channelno, cls) break if handle_ok is not None: - handle_ok(channelno, elem.uri) + handle_ok(channelno, elem.attr['uri']) elif cmd == 'ERR': elem = xmlio.parse(message.get_payload()) text = elem.gettext() - code = int(elem.code) + code = int(elem.attr['code']) logging.debug('Peer refused to start channel %d: %s (%d)', channelno, text, code) if handle_error is not None: diff --git a/bitten/util/xmlio.py b/bitten/util/xmlio.py --- a/bitten/util/xmlio.py +++ b/bitten/util/xmlio.py @@ -76,14 +76,15 @@ >>> print Element('foo')[''] ]]> """ - __slots__ = ['tagname', 'attrs', 'children'] + __slots__ = ['name', 'attrs', 'children'] - def __init__(self, tagname, **attrs): + def __init__(self, *args, **attrs): """Create an XML element using the specified tag name. - All keyword arguments are handled as attributes of the element. + The tag name must be supplied as the first positional argument. All + keyword arguments following it are handled as attributes of the element. """ - self.tagname = tagname + self.name = args[0] self.attrs = attrs self.children = [] @@ -106,7 +107,7 @@ stream. """ out.write('<') - out.write(self.tagname) + out.write(self.name) for name, value in self.attrs.items(): out.write(' %s="%s"' % (name, self._escape_attr(value))) if self.children: @@ -119,7 +120,7 @@ out.write('') else: out.write(self._escape_text(child)) - out.write('') + out.write('') else: out.write('/>') if newlines: @@ -137,13 +138,17 @@ __slots__ = [] - def __init__(self, parent, tagname, **attrs): + def __init__(self, *args, **attrs): """Create an XML element using the specified tag name. - All keyword arguments are handled as attributes of the element. + The first positional argument is the instance of the parent element that + this subelement should be appended to; the second positional argument is + the name of the tag. All keyword arguments are handled as attributes of + the element. """ - Element.__init__(self, tagname, **attrs) - parent.children.append(self) + assert len(args) == 2 + Element.__init__(self, args[1], **attrs) + args[0].children.append(self) def parse(text): @@ -156,26 +161,26 @@ class ParsedElement(object): - __slots__ = ['node'] + __slots__ = ['_node', 'attr'] def __init__(self, node): - self.node = node - - tagname = property(fget=lambda self: self.node.tagName) + self._node = node + self.attr = dict([(name.encode(), value.encode()) for name, value + in node.attributes.items()]) - def __getattr__(self, name): - return self.node.getAttribute(name) + name = property(fget=lambda self: self._node.localName) + namespace = property(fget=lambda self: self._node.namespaceURI) - def __getitem__(self, name): - for child in [c for c in self.node.childNodes if c.nodeType == 1]: - if name in ('*', child.tagName): + def children(self, name=None): + for child in [c for c in self._node.childNodes if c.nodeType == 1]: + if name in (None, child.tagName): yield ParsedElement(child) def __iter__(self): - return self['*'] + return self.children() def gettext(self): - return ''.join([c.nodeValue for c in self.node.childNodes]) + return ''.join([c.nodeValue for c in self._node.childNodes]) if __name__ == '__main__':