diff --git a/Makefile.am b/Makefile.am
index 507a34d5035f2c614cd321a170b142631863a667..bc41c353dbe11639d256d7819e23ad907da02cb3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -235,6 +235,7 @@ EXTRA_DIST = \
 
 man_MANS = \
 	man/ganeti.7 \
+	man/ganeti-cleaner.8 \
 	man/ganeti-masterd.8 \
 	man/ganeti-noded.8 \
 	man/ganeti-os-interface.7 \
@@ -258,6 +259,7 @@ TEST_FILES = \
 	test/data/bdev-disk.txt \
 	test/data/bdev-net.txt \
 	test/data/proc_drbd8.txt \
+	test/data/proc_drbd80-emptyline.txt \
 	test/data/proc_drbd83.txt
 
 dist_TESTS = \
diff --git a/NEWS b/NEWS
index c540562c9b4edde679be1b1acaa2a97f5ebec9aa..f20110eb8ca392ea4eeae2f98db4f60059d4b3fa 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,46 @@
 News
 ====
+Version 2.0.4
+-------------
+
+- Fixed many wrong messages
+- Fixed a few bugs related to the locking library
+- Fixed MAC checking at instance creation time
+- Fixed a DRBD parsing bug related to gaps in /proc/drbd
+- Fixed a few issues related to signal handling in both daemons and
+  scripts
+- Fixed the example startup script provided
+- Fixed insserv dependencies in the example startup script (patch from
+  Debian)
+- Fixed handling of drained nodes in the iallocator framework
+- Fixed handling of KERNEL_PATH parameter for xen-hvm (Debian bug
+  #528618)
+- Fixed error related to invalid job IDs in job polling
+- Fixed job/opcode persistence on unclean master shutdown
+- Fixed handling of partial job processing after unclean master
+  shutdown
+- Fixed error reporting from LUs, previously all errors were converted
+  into execution errors
+- Fixed error reporting from burnin
+- Decreased significantly the memory usage of the job queue
+- Optimised slightly multi-job submission
+- Optimised slightly opcode loading
+- Backported the multi-job submit framework from the development
+  branch; multi-instance start and stop should be faster
+- Added script to clean archived jobs after 21 days; this will reduce
+  the size of the queue directory
+- Added some extra checks in disk size tracking
+- Added an example ethers hook script
+- Added a cluster parameter that prevents Ganeti from modifying of
+  /etc/hosts
+- Added more node information to RAPI responses
+- Added a β€œgnt-job watch” command that allows following the ouput of a
+  job
+- Added a bind-address option to ganeti-rapi
+- Added more checks to the configuration verify
+- Enhanced the burnin script such that some operations can be retried
+  automatically
+- Converted instance reinstall to multi-instance model
 
 Version 2.0.3
 -------------
diff --git a/configure.ac b/configure.ac
index e13635f4529b4a39401bf0c44baaf3385531a6d4..ac5f265e3fa670dfb24f749ba8269e636392b356 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,7 +1,7 @@
 # Configure script for Ganeti
 m4_define([gnt_version_major], [2])
 m4_define([gnt_version_minor], [0])
-m4_define([gnt_version_revision], [3])
+m4_define([gnt_version_revision], [4])
 m4_define([gnt_version_suffix], [])
 m4_define([gnt_version_full],
           m4_format([%d.%d.%d%s],
diff --git a/lib/bdev.py b/lib/bdev.py
index f72302149826f9dc317c932eb21bae1e0275d5bb..595f7ae415b6ea7a6decf35f9dce2442b3e25880 100644
--- a/lib/bdev.py
+++ b/lib/bdev.py
@@ -803,6 +803,8 @@ class BaseDRBD(BlockDev):
     results = {}
     old_minor = old_line = None
     for line in data:
+      if not line: # completely empty lines, as can be returned by drbd8.0+
+        continue
       lresult = lmatch.match(line)
       if lresult is not None:
         if old_minor is not None:
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 41e24080a7033500bc77a2a3807e5e61877c96d9..98147a0e8eb8d73f1794f570f649b70cb525fed4 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -1594,7 +1594,6 @@ class LURepairDiskSizes(NoHooksLU):
         if full_name is None:
           raise errors.OpPrereqError("Instance '%s' not known" % name)
         self.wanted_names.append(full_name)
-      self.needed_locks[locking.LEVEL_INSTANCE] = self.wanted_names
       self.needed_locks = {
         locking.LEVEL_NODE: [],
         locking.LEVEL_INSTANCE: self.wanted_names,
@@ -1624,6 +1623,29 @@ class LURepairDiskSizes(NoHooksLU):
     self.wanted_instances = [self.cfg.GetInstanceInfo(name) for name
                              in self.wanted_names]
 
+  def _EnsureChildSizes(self, disk):
+    """Ensure children of the disk have the needed disk size.
+
+    This is valid mainly for DRBD8 and fixes an issue where the
+    children have smaller disk size.
+
+    @param disk: an L{ganeti.objects.Disk} object
+
+    """
+    if disk.dev_type == constants.LD_DRBD8:
+      assert disk.children, "Empty children for DRBD8?"
+      fchild = disk.children[0]
+      mismatch = fchild.size < disk.size
+      if mismatch:
+        self.LogInfo("Child disk has size %d, parent %d, fixing",
+                     fchild.size, disk.size)
+        fchild.size = disk.size
+
+      # and we recurse on this child only, not on the metadev
+      return self._EnsureChildSizes(fchild) or mismatch
+    else:
+      return False
+
   def Exec(self, feedback_fn):
     """Verify the size of cluster disks.
 
@@ -1640,7 +1662,10 @@ class LURepairDiskSizes(NoHooksLU):
 
     changed = []
     for node, dskl in per_node_disks.items():
-      result = self.rpc.call_blockdev_getsizes(node, [v[2] for v in dskl])
+      newl = [v[2].Copy() for v in dskl]
+      for dsk in newl:
+        self.cfg.SetDiskID(dsk, node)
+      result = self.rpc.call_blockdev_getsizes(node, newl)
       if result.fail_msg:
         self.LogWarning("Failure in blockdev_getsizes call to node"
                         " %s, ignoring", node)
@@ -1666,6 +1691,9 @@ class LURepairDiskSizes(NoHooksLU):
           disk.size = size
           self.cfg.Update(instance)
           changed.append((instance.name, idx, size))
+        if self._EnsureChildSizes(disk):
+          self.cfg.Update(instance)
+          changed.append((instance.name, idx, disk.size))
     return changed
 
 
@@ -2861,7 +2889,8 @@ class LUAddNode(LogicalUnit):
       nl_payload = result[verifier].payload[constants.NV_NODELIST]
       if nl_payload:
         for failed in nl_payload:
-          feedback_fn("ssh/hostname verification failed %s -> %s" %
+          feedback_fn("ssh/hostname verification failed"
+                      " (checking from %s): %s" %
                       (verifier, nl_payload[failed]))
         raise errors.OpExecError("ssh/hostname verification failed.")
 
diff --git a/lib/ssh.py b/lib/ssh.py
index f0362b4b81933a3939d0f2975def532948be2ea2..e89131422586eb532d4ee4240eec9b50a4cfbd4f 100644
--- a/lib/ssh.py
+++ b/lib/ssh.py
@@ -227,7 +227,12 @@ class SshRunner:
     remotehostname = retval.stdout.strip()
 
     if not remotehostname or remotehostname != node:
-      return False, "hostname mismatch, got %s" % remotehostname
+      if node.startswith(remotehostname + "."):
+        msg = "hostname not FQDN"
+      else:
+        msg = "hostname mistmatch"
+      return False, ("%s: expected %s but got %s" %
+                     (msg, node, remotehostname))
 
     return True, "host matches"
 
diff --git a/man/footer.sgml b/man/footer.sgml
index 70582739f668ad4e174a7f6497930316946d09d2..369c2aba108b5cb5b3d7f772b65f00a26f6f76a4 100644
--- a/man/footer.sgml
+++ b/man/footer.sgml
@@ -59,6 +59,10 @@
         <refentrytitle>ganeti-watcher</refentrytitle>
         <manvolnum>8</manvolnum>
       </citerefentry> (automatic instance restarter),
+      <citerefentry>
+        <refentrytitle>ganeti-cleaner</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry> (job queue cleaner),
       <citerefentry>
         <refentrytitle>ganeti-noded</refentrytitle>
         <manvolnum>8</manvolnum>
diff --git a/man/ganeti-cleaner.sgml b/man/ganeti-cleaner.sgml
new file mode 100644
index 0000000000000000000000000000000000000000..ac60bdaf620125f3ed79fb89970f240c12e4f2dc
--- /dev/null
+++ b/man/ganeti-cleaner.sgml
@@ -0,0 +1,78 @@
+<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
+
+  <!-- Fill in your name for FIRSTNAME and SURNAME. -->
+  <!-- Please adjust the date whenever revising the manpage. -->
+  <!ENTITY dhdate      "<date>February 11, 2009</date>">
+  <!-- SECTION should be 1-8, maybe w/ subsection other parameters are
+       allowed: see man(7), man(1). -->
+  <!ENTITY dhsection   "<manvolnum>8</manvolnum>">
+  <!ENTITY dhucpackage "<refentrytitle>ganeti-cleaner</refentrytitle>">
+  <!ENTITY dhpackage   "ganeti-cleaner">
+
+  <!ENTITY debian      "<productname>Debian</productname>">
+  <!ENTITY gnu         "<acronym>GNU</acronym>">
+  <!ENTITY gpl         "&gnu; <acronym>GPL</acronym>">
+  <!ENTITY footer SYSTEM "footer.sgml">
+]>
+
+<refentry>
+  <refentryinfo>
+    <copyright>
+      <year>2009</year>
+      <holder>Google Inc.</holder>
+    </copyright>
+    &dhdate;
+  </refentryinfo>
+  <refmeta>
+    &dhucpackage;
+
+    &dhsection;
+    <refmiscinfo>ganeti 2.0</refmiscinfo>
+  </refmeta>
+  <refnamediv>
+    <refname>&dhpackage;</refname>
+
+    <refpurpose>ganeti job queue cleaner</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>&dhpackage;</command>
+
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>DESCRIPTION</title>
+
+    <para>
+      The <command>&dhpackage;</command> is a periodically run script to clean
+      old job files from the job queue archive.
+    </para>
+
+    <para>
+      <command>&dhpackage;</command> automatically removes all files older than
+      21 days from
+      <filename>@LOCALSTATEDIR@/lib/ganeti/queue/archive</filename>.
+    </para>
+
+  </refsect1>
+
+  &footer;
+
+</refentry>
+
+<!-- Keep this comment at the end of the file
+Local variables:
+mode: sgml
+sgml-omittag:t
+sgml-shorttag:t
+sgml-minimize-attributes:nil
+sgml-always-quote-attributes:t
+sgml-indent-step:2
+sgml-indent-data:t
+sgml-parent-document:nil
+sgml-default-dtd-file:nil
+sgml-exposed-tags:nil
+sgml-local-catalogs:nil
+sgml-local-ecat-files:nil
+End:
+-->
diff --git a/test/data/proc_drbd80-emptyline.txt b/test/data/proc_drbd80-emptyline.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d7c3d58ac5a155f163d6e3a7b1ce33e9bef5ad7f
--- /dev/null
+++ b/test/data/proc_drbd80-emptyline.txt
@@ -0,0 +1,10 @@
+GIT-hash: 5c9f89594553e32adb87d9638dce591782f947e3 build by root@node1.example.com, 2009-05-22 12:47:52
+ 0: cs:Connected st:Primary/Secondary ds:UpToDate/UpToDate C r---
+    ns:78728316 nr:0 dw:77675644 dr:1277039 al:254 bm:270 lo:0 pe:0 ua:0 ap:0
+        resync: used:0/61 hits:65657 misses:135 starving:0 dirty:0 changed:135
+        act_log: used:0/257 hits:11378843 misses:254 starving:0 dirty:0 changed:254
+ 1: cs:Unconfigured
+ 2: cs:Unconfigured
+
+ 5: cs:Unconfigured
+ 6: cs:Unconfigured
diff --git a/test/ganeti.bdev_unittest.py b/test/ganeti.bdev_unittest.py
index b2299b18d481eed28302323743072481ddd6a529..eaf4c0768a3eccb75acd9facc7b5108d1024a335 100755
--- a/test/ganeti.bdev_unittest.py
+++ b/test/ganeti.bdev_unittest.py
@@ -114,10 +114,13 @@ class TestDRBD8Status(testutils.GanetiTestCase):
     """Read in txt data"""
     testutils.GanetiTestCase.setUp(self)
     proc_data = self._TestDataFilename("proc_drbd8.txt")
+    proc80e_data = self._TestDataFilename("proc_drbd80-emptyline.txt")
     proc83_data = self._TestDataFilename("proc_drbd83.txt")
     self.proc_data = bdev.DRBD8._GetProcData(filename=proc_data)
+    self.proc80e_data = bdev.DRBD8._GetProcData(filename=proc80e_data)
     self.proc83_data = bdev.DRBD8._GetProcData(filename=proc83_data)
     self.mass_data = bdev.DRBD8._MassageProcData(self.proc_data)
+    self.mass80e_data = bdev.DRBD8._MassageProcData(self.proc80e_data)
     self.mass83_data = bdev.DRBD8._MassageProcData(self.proc83_data)
 
   def testIOErrors(self):
@@ -131,6 +134,7 @@ class TestDRBD8Status(testutils.GanetiTestCase):
     """Test not-found-minor in /proc"""
     self.failUnless(9 not in self.mass_data)
     self.failUnless(9 not in self.mass83_data)
+    self.failUnless(3 not in self.mass80e_data)
 
   def testLineNotMatch(self):
     """Test wrong line passed to DRBD8Status"""
@@ -154,7 +158,7 @@ class TestDRBD8Status(testutils.GanetiTestCase):
 
   def testMinor2(self):
     """Test unconfigured device"""
-    for data in [self.mass_data, self.mass83_data]:
+    for data in [self.mass_data, self.mass83_data, self.mass80e_data]:
       stats = bdev.DRBD8Status(data[2])
       self.failIf(stats.is_in_use)
 
diff --git a/tools/burnin b/tools/burnin
index 836b42a24b8e0c89ea3518f9d86b11c0630d2b1b..35848c45f49699407df120894c9a872410dc0d8e 100755
--- a/tools/burnin
+++ b/tools/burnin
@@ -229,10 +229,10 @@ class Burner(object):
 
   def Feedback(self, msg):
     """Acumulate feedback in our buffer."""
-    self._feed_buf.write("%s %s\n" % (time.ctime(utils.MergeTime(msg[0])),
-                                      msg[2]))
+    formatted_msg = "%s %s" % (time.ctime(utils.MergeTime(msg[0])), msg[2])
+    self._feed_buf.write(formatted_msg + "\n")
     if self.opts.verbose:
-      Log(msg, indent=3)
+      Log(formatted_msg, indent=3)
 
   def MaybeRetry(self, retry_count, msg, fn, *args):
     """Possibly retry a given function execution.