changeset 51:5caccd7b247e

Proper archive format negotiation; improved representation of parsed XML content in {{{bitten.util.xmlio}}}.
author cmlenz
date Sun, 26 Jun 2005 16:06:30 +0000
parents 0d5ad32948b7
children 82a9c225f073
files bitten/master.py bitten/recipe.py bitten/slave.py bitten/tests/recipe.py bitten/util/archive.py bitten/util/beep.py bitten/util/xmlio.py
diffstat 7 files changed, 177 insertions(+), 111 deletions(-) [+]
line wrap: on
line diff
--- 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)
 
 
--- a/bitten/recipe.py
+++ b/bitten/recipe.py
@@ -19,9 +19,9 @@
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
 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)
--- 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():
--- 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('<?xml version="1.0"?>'
-                              '<recipe description="test">'
-                              '</recipe>')
+                              '<build description="test">'
+                              '</build>')
         self.recipe_xml.close()
         recipe = Recipe(basedir=self.temp_dir)
         self.assertEqual('test', recipe.description)
--- 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:
--- 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: # <profile/> and <ok/> 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:
--- a/bitten/util/xmlio.py
+++ b/bitten/util/xmlio.py
@@ -76,14 +76,15 @@
     >>> print Element('foo')['<bar a="3" b="4"><baz/></bar>']
     <foo><![CDATA[<bar a="3" b="4"><baz/></bar>]]></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('<![CDATA[' + child + ']]>')
                     else:
                         out.write(self._escape_text(child))
-            out.write('</' + self.tagname + '>')
+            out.write('</' + self.name + '>')
         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__':
Copyright (C) 2012-2017 Edgewall Software