diff --git a/ChangeLog b/ChangeLog
index 22962c4bdeb4120787decb76a24d0ef1608d9cda..3035ad2d69271fa7e9461e7f81ee761dd30904ed 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,22 @@
+2015-01-05, v0.8
+	* Support locally mounting the image on the host system
+	* Add option for running user defined scripts in the image's root after
+	  locally mounting it, in the non-interactive version of the program
+	* Fully support OpenBSD and NetBSD images (libguestfs >= 1.29.4)
+	* Allow the user to save the current execution log in the dialog-based
+	  version of the program
+	* Output the server answer in stdout after successfully registering an
+	  image with a synnefo deployment in the non-interactive version of the
+	  program
+	* Add a menu entry after "Register" to show the server's answer in the
+	  dialog-based version of the program after a successful registration
+	* Automatically detect and assign SORTORDER, GUI and KERNEL metadata
+	* Fix bugs
+
 2014-11-04, v0.7.4
 	* Handle cases where qemu-nbd command is missing
+	* Create REMOTE_CONNECTION property. Can be used by cyclades to
+	  determine the way to remotely connect to a VM created by this image
 
 2014-10-21, v0.7.3
 	* Instruct kamaki to ignore the ssl certificates (kamaki >= 0.13rc5).
diff --git a/docs/install.rst b/docs/install.rst
index 39260342de14939fee3870bf7e9f24ffb85f4f47..17144e82a3b8b7c796e610ca8deb5b02196489b4 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -48,7 +48,7 @@ Add the apt-dev GPG key to the list of trusted keys:
 
 .. code-block:: console
 
-   # curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
+   # wget --no-check-certificate  -qO-  http://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
 
 And resynchronize the package index files from their sources:
 
diff --git a/docs/usage.rst b/docs/usage.rst
index 0a6e6b1dca9ce23a6e77ee5ce09e51058a23818f..72a775eab446340a53642109afd2e08ccbf2d907 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -23,43 +23,52 @@ snf-mkimage receives the following options:
   Options:
     --version             show program's version number and exit
     -h, --help            show this help message and exit
-    -o FILE, --outfile=FILE
-                          dump image to FILE
-    -f, --force           overwrite output files if they exist
-    -s, --silent          output only errors
-    -u FILENAME, --upload=FILENAME
-                          upload the image to the cloud with name FILENAME
-    -r IMAGENAME, --register=IMAGENAME
-                          register the image with a cloud as IMAGENAME
-    -m KEY=VALUE, --metadata=KEY=VALUE
-                          add custom KEY=VALUE metadata to the image
-    -t TOKEN, --token=TOKEN
-                          use this authentication token when
-                          uploading/registering images
     -a URL, --authentication-url=URL
                           use this authentication URL when uploading/registering
                           images
+    --allow-unsupported   proceed with the image creation even if the media is
+                          not supported
     -c CLOUD, --cloud=CLOUD
                           use this saved cloud account to authenticate against a
                           cloud when uploading/registering images
-    --print-syspreps      print the enabled and disabled system preparation
-                          operations for this input media
-    --enable-sysprep=SYSPREP
-                          run SYSPREP operation on the input media
     --disable-sysprep=SYSPREP
                           prevent SYSPREP operation from running on the input
                           media
+    --enable-sysprep=SYSPREP
+                          run SYSPREP operation on the input media
+    -f, --force           overwrite output files if they exist
+    --host-run=SCRIPT     mount the media in the host and run a script against
+                          the guest media. This option may be defined multiple
+                          times. The script's working directory will be the
+                          guest's root directory. BE CAREFUL! DO NOT USE
+                          ABSOLUTE PATHS INSIDE THE SCRIPT! YOU MAY HARM YOUR
+                          SYSTEM!
     --install-virtio=DIR  install VirtIO drivers hosted under DIR (Windows only)
+    -m KEY=VALUE, --metadata=KEY=VALUE
+                          add custom KEY=VALUE metadata to the image
+    --no-snapshot         don't snapshot the input media. (THIS IS DANGEROUS AS
+                          IT WILL ALTER THE ORIGINAL MEDIA!!!)
+    --no-sysprep          don't perform any system preparation operation
+    -o FILE, --outfile=FILE
+                          dump image to FILE
+    --print-metadata      print the detected image metadata
+    --print-syspreps      print the enabled and disabled system preparation
+                          operations for this input media
     --print-sysprep-params
                           print the defined system preparation parameters for
                           this input media
+    --public              register image with the cloud as public
+    -r IMAGENAME, --register=IMAGENAME
+                          register the image with a cloud as IMAGENAME
+    -s, --silent          output only errors
     --sysprep-param=SYSPREP_PARAMS
                           add KEY=VALUE system preparation parameter
-    --no-sysprep          don't perform any system preparation operation
-    --public              register image with the cloud as public
-    --allow-unsupported   proceed with the image creation even if the media is
-                          not supported
+    -t TOKEN, --token=TOKEN
+                          use this authentication token when
+                          uploading/registering images
     --tmpdir=DIR          create large temporary image files under DIR
+    -u FILENAME, --upload=FILENAME
+                        upload the image to the cloud with name FILENAME
 
 Most input options are self-describing. If you want to save a local copy of
 the image you create, provide a filename using the *-o* option. To upload the
@@ -71,13 +80,17 @@ registration name using *-r*. All images are registered as *private*. Only the
 user that registers the image can create VM's out of it. If you want the image
 to be visible by other user too, use the *--public* option.
 
-By default, before extracting the image, snf-mkimage will perform a
-number of system preparation operations on the snapshot of the media and will
-shrink the last partition found. Both actions can be disabled by specifying
-*--no-sysprep* and *--no-shrink* respectively.
+By default, before extracting the image, snf-mkimage will perform a number of
+system preparation operations on the snapshot of the media. You can disable
+this by specifying *--no-sysprep*.
+
+You may use the *--host-run* option multiple times to define scripts that will
+run on the image's locally mounted root directory before the image
+customization is performed. Be careful when using those. The scripts run on the
+host system without any jail protection. Use only relative paths.
 
 If *--print-sysprep* is defined, the program will exit after printing a
-list of enabled and disabled system preparation operation applicable to this
+list of enabled and disabled system preparation operations applicable to this
 input media. The user can enable or disable specific *syspreps*, using
 *-{enable,disable}-sysprep* options. The user may specify those options
 multiple times.
@@ -336,11 +349,9 @@ Limitations
 Supported operating systems
 ---------------------------
 
-*snf-image-creator* can only fully function on input media hosting *Linux*,
-*FreeBSD* (tested on version 9.1) and *Windows* (Server 2008 R2 and Server
-2012) systems. The program will detect the needed metadata and you may use it
-to upload and register other *Unix* images, but you cannot use it to shrink
-them or perform system preparation operations.
+*snf-image-creator* can fully function on input media hosting *Linux*,
+*FreeBSD*, *OpenBSD*, *NetBSD* and *Windows* (Server starting from 2008R2 and
+Desktop starting from 7) systems.
 
 Logical Volumes
 ---------------
diff --git a/image_creator/bundle_volume.py b/image_creator/bundle_volume.py
index 7d0e36cc12040043639bc53b9a12648142a7b31d..09373f99fe0a75091461ebc1af4203f0d8b10dee 100644
--- a/image_creator/bundle_volume.py
+++ b/image_creator/bundle_volume.py
@@ -92,6 +92,118 @@ def mkfs(fs, device, uuid=None, label=None):
         UUID_UPDATE[fs](device, uuid)
 
 
+def read_fstable(f):
+    """Use this generator to iterate over the lines of an fstab file"""
+
+    if not os.path.isfile(f):
+        raise FatalError("Unable to open: `%s'. File is missing." % f)
+
+    FileSystemTableEntry = namedtuple('FileSystemTableEntry',
+                                      'dev mpoint fs opts freq passno')
+    with open(f) as table:
+        for line in iter(table):
+            entry = line.split('#')[0].strip().split()
+            if len(entry) != 6:
+                continue
+            yield FileSystemTableEntry(*entry)
+
+
+def get_root_partition():
+    """Return the fstab entry associated with the root file system"""
+    for entry in read_fstable('/etc/fstab'):
+        if entry.mpoint == '/':
+            return entry.dev
+
+    raise FatalError("Unable to find root device in /etc/fstab")
+
+
+def is_mpoint(path):
+    """Check if a directory is currently a mount point"""
+    for entry in read_fstable('/proc/mounts'):
+        if entry.mpoint == path:
+            return True
+    return False
+
+
+def get_mount_options(device):
+    """Return the mount entry associated with a mounted device"""
+    for entry in read_fstable('/proc/mounts'):
+        if not entry.dev.startswith('/'):
+            continue
+
+        if os.path.realpath(entry.dev) == os.path.realpath(device):
+            return entry
+
+    return None
+
+
+def get_partitions(disk):
+    """Returns a list with the partitions of the provided disk"""
+    Partition = namedtuple('Partition', 'num start end type fs')
+
+    partitions = []
+    for p in disk.partitions:
+        num = p.number
+        start = p.geometry.start
+        end = p.geometry.end
+        ptype = p.type
+        fs = p.fileSystem.type if p.fileSystem is not None else ''
+        partitions.append(Partition(num, start, end, ptype, fs))
+
+    return partitions
+
+
+def map_partition(dev, num, start, end):
+    """Map a partition into a block device using the device mapper"""
+    name = os.path.basename(dev) + "_" + uuid.uuid4().hex
+    tablefd, table = tempfile.mkstemp()
+    try:
+        try:
+            size = end - start + 1
+            os.write(tablefd, "0 %d linear %s %d" % (size, dev, start))
+        finally:
+            os.close(tablefd)
+        dmsetup('create', "%sp%d" % (name, num), table)
+    finally:
+        os.unlink(table)
+
+    return "/dev/mapper/%sp%d" % (name, num)
+
+
+def unmap_partition(dev):
+    """Unmap a previously mapped partition"""
+    if not os.path.exists(dev):
+        return
+
+    try_fail_repeat(dmsetup, 'remove', dev.split('/dev/mapper/')[1])
+
+
+def mount_all(target, devs):
+    """Mount a list of file systems in mount points relative to target"""
+    devs.sort(key=lambda d: d[1])
+    for dev, mpoint, options in devs:
+        absmpoint = os.path.abspath(target + mpoint)
+        if not os.path.exists(absmpoint):
+            os.makedirs(absmpoint)
+
+        if len(options) > 0:
+            mount(dev, absmpoint, '-o', ",".join(options))
+        else:
+            mount(dev, absmpoint)
+
+
+def umount_all(target):
+    """Umount all file systems that are mounted under the target directory"""
+    mpoints = []
+    for entry in read_fstable('/proc/mounts'):
+        if entry.mpoint.startswith(os.path.abspath(target)):
+            mpoints.append(entry.mpoint)
+
+    mpoints.sort()
+    for mpoint in reversed(mpoints):
+        try_fail_repeat(umount, mpoint)
+
+
 class BundleVolume(object):
     """This class can be used to create an image out of the running system"""
 
@@ -101,8 +213,8 @@ class BundleVolume(object):
         self.meta = meta
         self.tmp = tmp
 
-        self.out.output('Searching for root device ...', False)
-        root = self._get_root_partition()
+        self.out.info('Searching for root device ...', False)
+        root = get_root_partition()
 
         if root.startswith("UUID=") or root.startswith("LABEL="):
             root = findfs(root).stdout.strip()
@@ -116,47 +228,6 @@ class BundleVolume(object):
         device = parted.Device(disk_file)
         self.disk = parted.Disk(device)
 
-    def _read_fstable(self, f):
-        """Use this generator to iterate over the lines of and fstab file"""
-
-        if not os.path.isfile(f):
-            raise FatalError("Unable to open: `%s'. File is missing." % f)
-
-        FileSystemTableEntry = namedtuple('FileSystemTableEntry',
-                                          'dev mpoint fs opts freq passno')
-        with open(f) as table:
-            for line in iter(table):
-                entry = line.split('#')[0].strip().split()
-                if len(entry) != 6:
-                    continue
-                yield FileSystemTableEntry(*entry)
-
-    def _get_root_partition(self):
-        """Return the fstab entry associated with the root file system"""
-        for entry in self._read_fstable('/etc/fstab'):
-            if entry.mpoint == '/':
-                return entry.dev
-
-        raise FatalError("Unable to find root device in /etc/fstab")
-
-    def _is_mpoint(self, path):
-        """Check if a directory is currently a mount point"""
-        for entry in self._read_fstable('/proc/mounts'):
-            if entry.mpoint == path:
-                return True
-        return False
-
-    def _get_mount_options(self, device):
-        """Return the mount entry associated with a mounted device"""
-        for entry in self._read_fstable('/proc/mounts'):
-            if not entry.dev.startswith('/'):
-                continue
-
-            if os.path.realpath(entry.dev) == os.path.realpath(device):
-                return entry
-
-        return None
-
     def _create_partition_table(self, image):
         """Copy the partition table of the host system into the image"""
 
@@ -192,21 +263,6 @@ class BundleVolume(object):
                'seek=%d' % start, 'skip=%d' % start)
             start = logical[i].geometry.end + 1
 
-    def _get_partitions(self, disk):
-        """Returns a list with the partitions of the provided disk"""
-        Partition = namedtuple('Partition', 'num start end type fs')
-
-        partitions = []
-        for p in disk.partitions:
-            num = p.number
-            start = p.geometry.start
-            end = p.geometry.end
-            ptype = p.type
-            fs = p.fileSystem.type if p.fileSystem is not None else ''
-            partitions.append(Partition(num, start, end, ptype, fs))
-
-        return partitions
-
     def _shrink_partitions(self, image):
         """Remove the last partition of the image if it is a swap partition and
         shrink the partition before that. Make sure it can still host all the
@@ -217,7 +273,7 @@ class BundleVolume(object):
         is_extended = lambda p: p.type == parted.PARTITION_EXTENDED
         is_logical = lambda p: p.type == parted.PARTITION_LOGICAL
 
-        partitions = self._get_partitions(self.disk)
+        partitions = get_partitions(self.disk)
 
         last = partitions[-1]
         new_end = last.end
@@ -241,7 +297,7 @@ class BundleVolume(object):
 
             new_end = last.end
 
-        mount_options = self._get_mount_options(
+        mount_options = get_mount_options(
             self.disk.getPartitionBySector(last.start).path)
         if mount_options is not None:
             stat = os.statvfs(mount_options.mpoint)
@@ -276,55 +332,7 @@ class BundleVolume(object):
                 # Fix the extended partition
                 image_disk.minimizeExtendedPartition()
 
-        return (new_end, self._get_partitions(image_disk))
-
-    def _map_partition(self, dev, num, start, end):
-        """Map a partition into a block device using the device mapper"""
-        name = os.path.basename(dev) + "_" + uuid.uuid4().hex
-        tablefd, table = tempfile.mkstemp()
-        try:
-            try:
-                size = end - start + 1
-                os.write(tablefd, "0 %d linear %s %d" % (size, dev, start))
-            finally:
-                os.close(tablefd)
-            dmsetup('create', "%sp%d" % (name, num), table)
-        finally:
-            os.unlink(table)
-
-        return "/dev/mapper/%sp%d" % (name, num)
-
-    def _unmap_partition(self, dev):
-        """Unmap a previously mapped partition"""
-        if not os.path.exists(dev):
-            return
-
-        try_fail_repeat(dmsetup, 'remove', dev.split('/dev/mapper/')[1])
-
-    def _mount(self, target, devs):
-        """Mount a list of file systems in mount points relative to target"""
-        devs.sort(key=lambda d: d[1])
-        for dev, mpoint, options in devs:
-            absmpoint = os.path.abspath(target + mpoint)
-            if not os.path.exists(absmpoint):
-                os.makedirs(absmpoint)
-
-            if len(options) > 0:
-                mount(dev, absmpoint, '-o', ",".join(options))
-            else:
-                mount(dev, absmpoint)
-
-    def _umount_all(self, target):
-        """Umount all file systems that are mounted under the target directory
-        """
-        mpoints = []
-        for entry in self._read_fstable('/proc/mounts'):
-            if entry.mpoint.startswith(os.path.abspath(target)):
-                mpoints.append(entry.mpoint)
-
-        mpoints.sort()
-        for mpoint in reversed(mpoints):
-            try_fail_repeat(umount, mpoint)
+        return (new_end, get_partitions(image_disk))
 
     def _to_exclude(self):
         """Find which directories to exclude during the image copy. This is
@@ -335,7 +343,7 @@ class BundleVolume(object):
         if self.tmp is not None:
             excluded.append(self.tmp)
         local_filesystems = MKFS_OPTS.keys() + ['rootfs']
-        for entry in self._read_fstable('/proc/mounts'):
+        for entry in read_fstable('/proc/mounts'):
             if entry.fs in local_filesystems:
                 continue
 
@@ -343,8 +351,7 @@ class BundleVolume(object):
             if mpoint in excluded:
                 continue
 
-            descendants = filter(
-                lambda p: p.startswith(mpoint + '/'), excluded)
+            descendants = [e for e in excluded if e.startswith(mpoint + '/')]
             if len(descendants):
                 for d in descendants:
                     excluded.remove(d)
@@ -373,16 +380,16 @@ class BundleVolume(object):
         filesystem = {}
         orig_dev = {}
         for p in self.disk.partitions:
-            filesystem[p.number] = self._get_mount_options(p.path)
+            filesystem[p.number] = get_mount_options(p.path)
             orig_dev[p.number] = p.path
 
-        unmounted = filter(lambda p: filesystem[p.num] is None, partitions)
-        mounted = filter(lambda p: filesystem[p.num] is not None, partitions)
+        unmounted = [p for p in partitions if filesystem[p.num] is None]
+        mounted = [p for p in partitions if filesystem[p.num] is not None]
 
         # For partitions that are not mounted right now, we can simply dd them
         # into the image.
         for p in unmounted:
-            self.out.output('Cloning partition %d ... ' % p.num, False)
+            self.out.info('Cloning partition %d ... ' % p.num, False)
             dd('if=%s' % self.disk.device.path, 'of=%s' % image,
                'count=%d' % (p.end - p.start + 1), 'conv=notrunc',
                'seek=%d' % p.start, 'skip=%d' % p.start)
@@ -395,7 +402,7 @@ class BundleVolume(object):
         try:
             for p in mounted:
                 i = p.num
-                mapped[i] = self._map_partition(loop, i, p.start, p.end)
+                mapped[i] = map_partition(loop, i, p.start, p.end)
 
             new_uuid = {}
             # Create the file systems
@@ -406,8 +413,8 @@ class BundleVolume(object):
                     '-s', 'LABEL', '-o', 'value', orig_dev[i]).stdout.strip()
                 fs = filesystem[i].fs
 
-                self.out.output('Creating %s file system on partition %d ... '
-                                % (fs, i), False)
+                self.out.info('Creating %s file system on partition %d ... '
+                              % (fs, i), False)
                 mkfs(fs, dev, uuid=uuid, label=label)
 
                 # For ext[234] enable the default mount options
@@ -436,7 +443,7 @@ class BundleVolume(object):
                         opts.append(opt)
                 devs.append((mapped[i], mpoint, opts))
             try:
-                self._mount(target, devs)
+                mount_all(target, devs)
 
                 excluded = self._to_exclude()
 
@@ -461,7 +468,7 @@ class BundleVolume(object):
                 # /tmp and /var/tmp are special cases. We exclude then even if
                 # they aren't mount points. Restore their permissions.
                 for excl in ('/tmp', '/var/tmp'):
-                    if self._is_mpoint(excl):
+                    if is_mpoint(excl):
                         os.chmod(target + excl, 041777)
                         os.chown(target + excl, 0, 0)
                     else:
@@ -470,11 +477,11 @@ class BundleVolume(object):
                         os.chown(target + excl, stat.st_uid, stat.st_gid)
 
             finally:
-                self._umount_all(target)
+                umount_all(target)
                 os.rmdir(target)
         finally:
             for dev in mapped.values():
-                self._unmap_partition(dev)
+                unmap_partition(dev)
             losetup('-d', loop)
 
     def create_image(self, image):
@@ -513,7 +520,7 @@ class BundleVolume(object):
 
         # Check if the available space is enough to host the image
         dirname = os.path.dirname(image)
-        self.out.output("Examining available space ...", False)
+        self.out.info("Examining available space ...", False)
         if free_space(dirname) <= size:
             raise FatalError("Not enough space under %s to host the temporary "
                              "image" % dirname)
diff --git a/image_creator/dialog_main.py b/image_creator/dialog_main.py
index 2177aaf96b43d19fe4c28871128940eb08be8e7d..a08f73bbba18c11818cee6c2682e2e413be440ed 100644
--- a/image_creator/dialog_main.py
+++ b/image_creator/dialog_main.py
@@ -24,16 +24,15 @@ user is asked if he wants to use the program in expert or wizard mode.
 import dialog
 import sys
 import os
-import textwrap
 import signal
 import optparse
 import types
 import termios
 import traceback
+import tempfile
 
 from image_creator import __version__ as version
 from image_creator.util import FatalError
-from image_creator.output import Output
 from image_creator.output.cli import SimpleOutput
 from image_creator.output.dialog import GaugeOutput
 from image_creator.output.composite import CompositeOutput
@@ -51,7 +50,7 @@ def create_image(d, media, out, tmp, snapshot):
     d.setBackgroundTitle('snf-image-creator')
 
     gauge = GaugeOutput(d, "Initialization", "Initializing...")
-    out.add(gauge)
+    out.append(gauge)
     disk = Disk(media, out, tmp)
 
     def signal_handler(signum, frame):
@@ -66,15 +65,6 @@ def create_image(d, media, out, tmp, snapshot):
 
         image = disk.get_image(device)
 
-        out.output("Collecting image metadata ...")
-        metadata = {}
-        for (key, value) in image.meta.items():
-            metadata[str(key)] = str(value)
-
-        for (key, value) in image.os.meta.items():
-            metadata[str(key)] = str(value)
-
-        out.success("done")
         gauge.cleanup()
         out.remove(gauge)
 
@@ -85,8 +75,7 @@ def create_image(d, media, out, tmp, snapshot):
 
         session = {"dialog": d,
                    "disk": disk,
-                   "image": image,
-                   "metadata": metadata}
+                   "image": image}
 
         if image.is_unsupported():
 
@@ -172,6 +161,7 @@ def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[],
 
 
 def dialog_main(media, logfile, tmpdir, snapshot):
+    """Main function for the dialog-based version of the program"""
 
     # In openSUSE dialog is buggy under xterm
     if os.environ['TERM'] == 'xterm':
@@ -191,36 +181,54 @@ def dialog_main(media, logfile, tmpdir, snapshot):
     dialog._common_args_syntax["no_label"] = \
         lambda string: ("--no-label", string)
 
+    # Add exit label overwriting
+    dialog._common_args_syntax["exit_label"] = \
+        lambda string: ("--exit-label", string)
+
     # Monkey-patch pythondialog to include support for form dialog boxes
     if not hasattr(dialog, 'form'):
         d.form = types.MethodType(_dialog_form, d)
 
     d.setBackgroundTitle('snf-image-creator')
 
+    # Pick input media
+    while True:
+        media = select_file(d, init=media, ftype="br", bundle_host=True,
+                            title="Please select an input media.")
+        if media is None:
+            if confirm_exit(
+                    d, "You canceled the media selection dialog box."):
+                return 0
+            continue
+        break
+
+    tmplog = None if logfile else tempfile.NamedTemporaryFile(prefix='fatal-',
+                                                              delete=False)
     try:
-        while True:
-            media = select_file(d, init=media, ftype="br", bundle_host=True,
-                                title="Please select an input media.")
-            if media is None:
-                if confirm_exit(
-                        d, "You canceled the media selection dialog box."):
-                    return 0
-                continue
-            break
-
-        log = SimpleOutput(False, logfile) if logfile is not None else Output()
+        stream = logfile if logfile else tmplog
+        log = SimpleOutput(colored=False, stderr=stream, stdout=stream)
         while 1:
             try:
                 out = CompositeOutput([log])
-                out.output("Starting %s v%s ..." % (PROGNAME, version))
-                return create_image(d, media, out, tmpdir, snapshot)
+                out.info("Starting %s v%s ..." % (PROGNAME, version))
+                ret = create_image(d, media, out, tmpdir, snapshot)
+                break
             except Reset:
-                log.output("Resetting everything ...")
-                continue
+                log.info("Resetting everything ...")
+
     except FatalError as error:
-        msg = textwrap.fill(str(error), width=WIDTH-4)
+        log.error(str(error))
+        msg = 'A fatal error occured. See %s for a full log.' % log.stderr.name
         d.infobox(msg, width=WIDTH, title="Fatal Error")
         return 1
+    else:
+        if tmplog:
+            os.unlink(tmplog.name)
+    finally:
+        if tmplog:
+            tmplog.close()
+
+    return ret
 
 
 def main():
diff --git a/image_creator/dialog_menu.py b/image_creator/dialog_menu.py
index 0731cb4700cf7ec50d566ae21e0efb073ecab918..49f2e07050f7d77de86df5fa7fb9536bd9e54951 100644
--- a/image_creator/dialog_menu.py
+++ b/image_creator/dialog_menu.py
@@ -25,6 +25,7 @@ import StringIO
 import json
 import re
 import time
+import tempfile
 
 from image_creator import __version__ as version
 from image_creator.util import FatalError, virtio_versions
@@ -33,7 +34,8 @@ from image_creator.kamaki_wrapper import Kamaki, ClientError
 from image_creator.help import get_help_file
 from image_creator.dialog_util import SMALL_WIDTH, WIDTH, \
     update_background_title, confirm_reset, confirm_exit, Reset, \
-    extract_image, add_cloud, edit_cloud, update_sysprep_param
+    extract_image, add_cloud, edit_cloud, update_sysprep_param, select_file, \
+    copy_file
 
 CONFIGURATION_TASKS = [
     ("Partition table manipulation", ["FixPartitionTable"],
@@ -94,8 +96,6 @@ class MetadataMonitor(object):
                 msg += '    %s: "%s" -> "%s"\n' % (k, self.old[k], v)
             msg += "\n"
 
-        self.session['metadata'].update(added)
-        self.session['metadata'].update(altered)
         d.msgbox(msg, title="Image Property Changes", width=SMALL_WIDTH)
 
 
@@ -112,8 +112,8 @@ def upload_image(session):
     while 1:
         if 'upload' in session:
             init = session['upload']
-        elif 'OS' in session['metadata']:
-            init = "%s.diskdump" % session['metadata']['OS']
+        elif 'OS' in session['image'].meta:
+            init = "%s.diskdump" % session['image'].meta['OS']
         else:
             init = ""
         (code, answer) = d.inputbox("Please provide a filename:", init=init,
@@ -145,7 +145,7 @@ def upload_image(session):
     gauge = GaugeOutput(d, "Image Upload", "Uploading ...")
     try:
         out = image.out
-        out.add(gauge)
+        out.append(gauge)
         kamaki.out = out
         try:
             if 'checksum' not in session:
@@ -160,7 +160,7 @@ def upload_image(session):
                                           "Calculating block hashes",
                                           "Uploading missing blocks")
                 # Upload md5sum file
-                out.output("Uploading md5sum file ...")
+                out.info("Uploading md5sum file ...")
                 md5str = "%s %s\n" % (session['checksum'], filename)
                 kamaki.upload(StringIO.StringIO(md5str), size=len(md5str),
                               remote_path="%s.md5sum" % filename)
@@ -187,6 +187,7 @@ def upload_image(session):
 def register_image(session):
     """Register image with the compute service"""
     d = session["dialog"]
+    image = session['image']
 
     is_public = False
 
@@ -200,9 +201,9 @@ def register_image(session):
                  "register it", width=SMALL_WIDTH)
         return False
 
-    name = ""
-    description = session['metadata']['DESCRIPTION'] if 'DESCRIPTION' in \
-        session['metadata'] else ""
+    name = "" if 'registered' not in session else session['registered'].name
+    description = image.meta['DESCRIPTION'] if 'DESCRIPTION' in image.meta \
+        else ""
 
     while 1:
         fields = [("Registration name:", name, 60),
@@ -232,9 +233,9 @@ def register_image(session):
         is_public = (answer == 0)
         break
 
-    session['metadata']['DESCRIPTION'] = description
+    image.meta['DESCRIPTION'] = description
     metadata = {}
-    metadata.update(session['metadata'])
+    metadata.update(image.meta)
     if 'task_metadata' in session:
         for key in session['task_metadata']:
             metadata[key] = 'yes'
@@ -243,24 +244,26 @@ def register_image(session):
     gauge = GaugeOutput(d, "Image Registration", "Registering image ...")
     try:
         out = session['image'].out
-        out.add(gauge)
+        out.append(gauge)
         try:
             try:
-                out.output("Registering %s image with the cloud ..." %
-                           img_type)
+                out.info("Registering %s image with the cloud ..." % img_type,
+                         False)
                 kamaki = Kamaki(session['account'], out)
-                result = kamaki.register(name, session['pithos_uri'], metadata,
-                                         is_public)
+                session['registered'] = kamaki.register(
+                    name, session['pithos_uri'], metadata, is_public)
                 out.success('done')
+
                 # Upload metadata file
-                out.output("Uploading metadata file ...")
-                metastring = unicode(json.dumps(result, ensure_ascii=False))
+                out.info("Uploading metadata file ...", False)
+                metastring = unicode(json.dumps(session['registered'],
+                                                indent=4, ensure_ascii=False))
                 kamaki.upload(StringIO.StringIO(metastring),
                               size=len(metastring),
                               remote_path="%s.meta" % session['upload'])
                 out.success("done")
                 if is_public:
-                    out.output("Sharing metadata and md5sum files ...")
+                    out.info("Sharing metadata and md5sum files ...", False)
                     kamaki.share("%s.meta" % session['upload'])
                     kamaki.share("%s.md5sum" % session['upload'])
                     out.success('done')
@@ -324,7 +327,7 @@ def delete_clouds(session):
 
     (code, to_delete) = d.checklist("Choose which cloud accounts to delete:",
                                     choices=choices, width=WIDTH)
-    to_delete = map(lambda x: x.strip('"'), to_delete)  # Needed for OpenSUSE
+    to_delete = [x.strip('"') for x in to_delete]  # Needed for OpenSUSE
 
     if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
         return False
@@ -352,7 +355,13 @@ def delete_clouds(session):
 def kamaki_menu(session):
     """Show kamaki related actions"""
     d = session['dialog']
-    default_item = "Cloud"
+
+    if 'registered' in session:
+        default_item = "Info"
+    elif 'upload' in session:
+        default_item = "Register"
+    else:
+        default_item = "Upload"
 
     if 'cloud' not in session:
         cloud = Kamaki.get_default_cloud_name()
@@ -377,11 +386,14 @@ def kamaki_menu(session):
         if 'upload' in session:
             choices.append(("Register", "Register image with the cloud: %s"
                             % session['upload']))
+        if 'registered' in session:
+            choices.append(("Info", "Show registration info for \"%s\"" %
+                                    session['registered']['name']))
 
         (code, choice) = d.menu(
             text="Choose one of the following or press <Back> to go back.",
-            width=WIDTH, choices=choices, cancel="Back", height=13,
-            menu_height=5, default_item=default_item,
+            width=WIDTH, choices=choices, cancel="Back", height=8+len(choices),
+            menu_height=len(choices), default_item=default_item,
             title="Image Registration Menu")
 
         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
@@ -444,14 +456,51 @@ def kamaki_menu(session):
                 default_item = "Upload"
         elif choice == "Register":
             if register_image(session):
-                return True
+                default_item = "Info"
             else:
                 default_item = "Register"
+        elif choice == "Info":
+            show_info(session)
+
+
+def show_info(session):
+    """Show registration info"""
+
+    assert 'registered' in session
+    info = json.dumps(session['registered'], ensure_ascii=False, indent=4)
+
+    d = session['dialog']
+
+    while 1:
+        code = d.scrollbox(info, width=WIDTH, title="Registration info",
+                           extra_label="Save", extra_button=1,
+                           exit_label="Close")
+        if code == d.DIALOG_EXTRA:
+            path = select_file(d, title="Save registration information as...")
+            if path is None:
+                break
+            if os.path.isdir(path):
+                continue
+
+            if os.path.exists(path):
+                if d.yesno("File: `%s' already exists. Do you want to "
+                           "overwrite it?" % path, width=WIDTH, defaultno=1):
+                    continue
+
+            with open(path, 'w') as f:
+                f.write(info + '\n')
+
+            d.msgbox("File `%s was successfully written." % path,
+                     width=SMALL_WIDTH)
+            break
+        else:
+            return
 
 
 def add_property(session):
     """Add a new property to the image"""
     d = session['dialog']
+    image = session['image']
 
     regexp = re.compile('^[A-Za-z_]+$')
 
@@ -473,7 +522,7 @@ def add_property(session):
         # Image properties are case-insensitive
         name = name.upper()
 
-        if name in session['metadata']:
+        if name in image.meta:
             d.msgbox("Image property: `%s' already exists" % name, width=WIDTH)
             continue
 
@@ -492,7 +541,7 @@ def add_property(session):
 
         break
 
-    session['metadata'][name] = value
+    image.meta[name] = value
 
     return True
 
@@ -503,16 +552,17 @@ def show_properties_help(session):
 
     help_file = get_help_file("image_properties")
     assert os.path.exists(help_file)
-    d.textbox(help_file, title="Image Properties", width=70, height=40)
+    d.textbox(help_file, title="Image Properties", width=78, height=40)
 
 
 def modify_properties(session):
     """Modify an existing image property"""
     d = session['dialog']
+    image = session['image']
 
     while 1:
         choices = []
-        for (key, val) in session['metadata'].items():
+        for (key, val) in image.meta.items():
             choices.append((str(key), str(val)))
 
         if len(choices) == 0:
@@ -544,7 +594,7 @@ def modify_properties(session):
             (code, answer) = d.inputbox(
                 "Please provide a new value for `%s' image property or press "
                 "<Delete> to completely delete it." % choice,
-                init=session['metadata'][choice], width=WIDTH, extra_button=1,
+                init=image.meta[choice], width=WIDTH, extra_button=1,
                 extra_label="Delete")
             if code == d.DIALOG_OK:
                 value = answer.strip()
@@ -552,12 +602,12 @@ def modify_properties(session):
                     d.msgbox("Value cannot be empty!")
                     continue
                 else:
-                    session['metadata'][choice] = value
+                    image.meta[choice] = value
             # Delete button
             elif code == d.DIALOG_EXTRA:
                 if not d.yesno("Are you sure you want to delete `%s' image "
                                "property?" % choice, width=WIDTH):
-                    del session['metadata'][choice]
+                    del image.meta[choice]
                     d.msgbox("Image property: `%s' was deleted." % choice,
                              width=SMALL_WIDTH)
         # ADD button
@@ -592,7 +642,7 @@ def exclude_tasks(session):
             return False
 
     for (msg, task, osfamily) in CONFIGURATION_TASKS:
-        if session['metadata']['OSFAMILY'] in osfamily:
+        if image.meta['OSFAMILY'] in osfamily:
             checked = 1 if index in session['excluded_tasks'] else 0
             choices.append((str(displayed_index), msg, checked))
             mapping[displayed_index] = index
@@ -610,7 +660,7 @@ def exclude_tasks(session):
             text=text, choices=choices, height=19, list_height=8, width=WIDTH,
             help_button=1, extra_button=1, extra_label="No Config",
             title="Exclude Configuration Tasks")
-        tags = map(lambda x: x.strip('"'), tags)  # Needed for OpenSUSE
+        tags = [x.strip('"') for x in tags]  # Needed for OpenSUSE
 
         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
             return False
@@ -633,8 +683,8 @@ def exclude_tasks(session):
             for task in session['excluded_tasks']:
                 exclude_metadata.extend(CONFIGURATION_TASKS[task][1])
 
-            session['task_metadata'] = map(lambda x: "EXCLUDE_TASK_%s" % x,
-                                           exclude_metadata)
+            session['task_metadata'] = ["EXCLUDE_TASK_%s" % x
+                                        for x in exclude_metadata]
             break
 
     return True
@@ -654,7 +704,8 @@ def sysprep_params(session):
             if param.hidden:
                 continue
 
-            value = str(param.value)
+            value = "|".join([str(i) for i in param.value]) if param.is_list \
+                else str(param.value)
             if len(value) == 0:
                 value = "<not_set>"
             choices.append((name, value))
@@ -786,7 +837,7 @@ def install_virtio_drivers(session):
     title = "VirtIO Drivers Installation"
     infobox = InfoBoxOutput(d, title)
     try:
-        image.out.add(infobox)
+        image.out.append(infobox)
         try:
             image.os.install_virtio_drivers()
             infobox.finalize()
@@ -849,7 +900,7 @@ def sysprep(session):
             choices=choices, width=70, ok_label="Run", help_button=1,
             extra_button=1, extra_label="Params")
 
-        tags = map(lambda x: x.strip('"'), tags)  # Needed for OpenSUSE
+        tags = [x.strip('"') for x in tags]  # Needed for OpenSUSE
 
         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
             return False
@@ -873,7 +924,7 @@ def sysprep(session):
 
             infobox = InfoBoxOutput(d, "Image Configuration")
             try:
-                image.out.add(infobox)
+                image.out.append(infobox)
                 try:
                     # The checksum is invalid. We have mounted the image rw
                     if 'checksum' in session:
@@ -897,12 +948,67 @@ def sysprep(session):
     return True
 
 
+def mount(session):
+    """Mount image on the local file system"""
+    d = session['dialog']
+    image = session['image']
+
+    mpoint = tempfile.mkdtemp()
+    try:
+        try:
+            image.mount(mpoint)
+            if not image.is_mounted():
+                d.msgbox("Mounting Failed!", title="Mount Image",
+                         width=SMALL_WIDTH)
+                return
+            d.msgbox("The image was mounted successfully. You may access it "
+                     "under %s. Press <OK> when you have finished "
+                     "accessing it." % mpoint, title="Mount Image",
+                     width=SMALL_WIDTH)
+        finally:
+            while 1:
+                if image.umount():
+                    break
+                d.msgbox("Umount failed. Make sure no process is using any "
+                         "files under %s and press <OK>." % mpoint,
+                         width=SMALL_WIDTH)
+    finally:
+        os.rmdir(mpoint)
+
+
+def show_log(session):
+    """Show the current execution log"""
+
+    d = session['dialog']
+    log = session['image'].out[0].stderr
+
+    log.file.flush()
+
+    while 1:
+        code = d.textbox(log.name, title="Log", width=70, height=40,
+                         extra_button=1, extra_label="Save", ok_label="Close")
+        if code == d.DIALOG_EXTRA:
+            while 1:
+                path = select_file(d, title="Save log as...")
+                if path is None:
+                    break
+                if os.path.isdir(path):
+                    continue
+
+                if copy_file(d, log.name, path):
+                    break
+        else:
+            return
+
+
 def customization_menu(session):
     """Show image customization menu"""
     d = session['dialog']
     image = session['image']
 
     choices = []
+    if image.mount_local_support:
+        choices.append(("Mount", "Mount image on the local file system"))
     if hasattr(image.os, "install_virtio_drivers"):
         choices.append(("VirtIO", "Install or update the VirtIO drivers"))
     choices.extend(
@@ -912,7 +1018,8 @@ def customization_menu(session):
 
     default_item = 0
 
-    actions = {"VirtIO": virtio,
+    actions = {"Mount": mount,
+               "VirtIO": virtio,
                "Sysprep": sysprep,
                "Properties": modify_properties,
                "Exclude": exclude_tasks}
@@ -940,13 +1047,14 @@ def main_menu(session):
     choices = [("Customize", "Customize image & cloud deployment options"),
                ("Register", "Register image to a cloud"),
                ("Extract", "Dump image to local file system"),
+               ("Log", "Show current execution log"),
                ("Reset", "Reset everything and start over again"),
                ("Help", "Get help for using snf-image-creator")]
 
     default_item = "Customize"
 
     actions = {"Customize": customization_menu, "Register": kamaki_menu,
-               "Extract": extract_image}
+               "Extract": extract_image, "Log": show_log}
     title = "Image Creator for Synnefo (snf-image-creator v%s)" % version
     while 1:
         (code, choice) = d.menu(
@@ -970,4 +1078,7 @@ def main_menu(session):
         elif choice in actions:
             actions[choice](session)
 
+        if len(choice):
+            default_item = choice
+
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/dialog_util.py b/image_creator/dialog_util.py
index a757a963fbf687b9dacf260df10e5c657288e328..675322b216b4b72792823962f8065223f134dea2 100644
--- a/image_creator/dialog_util.py
+++ b/image_creator/dialog_util.py
@@ -23,6 +23,8 @@ import os
 import stat
 import re
 import json
+import shutil
+
 from image_creator.output.dialog import GaugeOutput
 from image_creator.kamaki_wrapper import Kamaki
 
@@ -59,6 +61,9 @@ def select_file(d, **kwargs):
     fname = None if "init" not in kwargs else kwargs['init']
     ftype = set(t for t in kwargs['ftype']) if 'ftype' in kwargs else set('r')
     title = kwargs['title'] if 'title' in kwargs else 'Please select a file.'
+    existing = kwargs['existing'] if 'existing' in kwargs else False
+    basedir = kwargs['basedir'] if 'basedir' in kwargs else \
+        (os.getcwd() + os.sep)
 
     bundle_host = kwargs['bundle_host'] if 'bundle_host' in kwargs else None
     extra_button = 1 if bundle_host else 0
@@ -70,13 +75,16 @@ def select_file(d, **kwargs):
     if bundle_host and fname == os.sep:
         return os.sep
 
-    default = os.getcwd() + os.sep
+    default = basedir
 
     while 1:
         if fname is not None:
             if not os.path.exists(fname):
-                d.msgbox("The file `%s' you choose does not exist." % fname,
-                         width=SMALL_WIDTH)
+                if existing:
+                    d.msgbox("The file `%s' you choose does not exist." %
+                             fname, width=SMALL_WIDTH)
+                else:
+                    return fname
             else:
                 mode = os.stat(fname).st_mode
                 for i in ftype:
@@ -84,7 +92,7 @@ def select_file(d, **kwargs):
                         return fname
 
                 if stat.S_ISDIR(mode):
-                    default = fname
+                    default = fname + os.sep
                 else:
                     d.msgbox("Invalid input.", width=SMALL_WIDTH)
 
@@ -135,7 +143,7 @@ class Reset(Exception):
 def extract_metadata_string(session):
     """Convert image metadata to text"""
     metadata = {}
-    metadata.update(session['metadata'])
+    metadata.update(session['image'].meta)
     if 'task_metadata' in session:
         for key in session['task_metadata']:
             metadata[key] = 'yes'
@@ -147,13 +155,16 @@ def extract_metadata_string(session):
 def extract_image(session):
     """Dump the image to a local file"""
     d = session['dialog']
+
     dir = os.getcwd()
+
     while 1:
         if dir and dir[-1] != os.sep:
             dir = dir + os.sep
 
-        (code, path) = d.fselect(dir, 10, 50, title="Save image as...")
-        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+        path = select_file(d, title="Save image as...", existing=False,
+                           basedir=dir)
+        if path is None:
             return False
 
         if os.path.isdir(path):
@@ -194,7 +205,7 @@ def extract_image(session):
         try:
             image = session['image']
             out = image.out
-            out.add(gauge)
+            out.append(gauge)
             try:
                 if "checksum" not in session:
                     session['checksum'] = image.md5()
@@ -203,13 +214,13 @@ def extract_image(session):
                 image.dump(path)
 
                 # Extract metadata file
-                out.output("Extracting metadata file ...")
+                out.info("Extracting metadata file ...", False)
                 with open('%s.meta' % path, 'w') as f:
                     f.write(extract_metadata_string(session))
                 out.success('done')
 
                 # Extract md5sum file
-                out.output("Extracting md5sum file ...")
+                out.info("Extracting md5sum file ...", False)
                 md5str = "%s %s\n" % (session['checksum'], name)
                 with open('%s.md5sum' % path, 'w') as f:
                     f.write(md5str)
@@ -334,41 +345,123 @@ def edit_cloud(session, name):
     return True
 
 
+def _get_sysprep_param_value(session, param, default, title=None,
+                             delete=False):
+    """Get the value of a sysprep parameter"""
+    d = session['dialog']
+
+    if param.type in ("file", "dir"):
+        if not title:
+            title = "Please select a %s to use for the `%s' parameter" % \
+                ('file' if param.type == 'file' else 'directory', param.name)
+        ftype = "br" if param.type == 'file' else 'd'
+
+        value = select_file(d, ftype=ftype, title=title)
+    else:
+        if not title:
+            title = ("Please provide a new value for configuration parameter: "
+                     "`%s' or press <Delete> to completely delete it." %
+                     param.name)
+        (code, answer) = d.inputbox(title, width=WIDTH, init=str(default),
+                                    extra_button=int(delete),
+                                    extra_label="Delete")
+
+        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+            return (None, False)
+        if code == d.DIALOG_EXTRA:
+            return ("", True)
+
+        value = answer.strip()
+
+    return (value, False)
+
+
 def update_sysprep_param(session, name, title=None):
     """Modify the value of a sysprep parameter"""
+
     d = session['dialog']
     image = session['image']
 
     param = image.os.sysprep_params[name]
 
+    default_item = 1
     while 1:
-        if param.type in ("file", "dir"):
-            if not title:
-                title = "Please select a %s to use for the `%s' parameter" % \
-                    ('file' if param.type == 'file' else 'directory', name)
-            ftype = "br" if param.type == 'file' else 'd'
-
-            value = select_file(d, ftype=ftype, title=title)
-            if value is None:
-                return False
+        value = []
+        for i in param.value:
+            value.append(i)
+        if param.is_list:
+            choices = [(str(i+1), str(value[i])) for i in xrange(len(value))]
+            if len(choices) == 0:
+                action = 'add'
+                default_value = ""
+            else:
+                (code, choice) = d.menu(
+                    "Please press <Edit> to edit or remove a value or <Add> "
+                    "to add a new one. Press <Back> to go back.", height=18,
+                    width=WIDTH, choices=choices, menu_height=10,
+                    ok_label="Edit", extra_button=1, extra_label="Add",
+                    cancel="Back", default_item=str(default_item), title=name)
+
+                if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+                    return True
+                elif code == d.DIALOG_EXTRA:
+                    action = 'add'
+                    default_value = ""
+                elif code == d.DIALOG_OK:
+                    action = 'edit'
+                    choice = int(choice)
+                    default_value = choices[choice-1][1]
+                    default_item = choice
         else:
-            if not title:
-                title = "Please provide a new value for configuration " \
-                        "parameter: `%s'" % name
-            (code, answer) = d.inputbox(
-                title, width=WIDTH, init=str(param.value))
+            default_value = param.value
+            action = 'edit'
+
+        (new_value, delete) = _get_sysprep_param_value(
+            session, param, default_value, title,
+            delete=(param.is_list and action == 'edit'))
 
-            if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+        if new_value is None:
+            if not param.is_list or len(param.value) == 0:
                 return False
+            continue
 
-            value = answer.strip()
+        if param.is_list:
+            if action == 'add':
+                value = value + [new_value]
+            if action == 'edit':
+                if delete:
+                    del value[choice-1]
+                else:
+                    value[choice-1] = new_value
 
         if param.set_value(value) is False:
             d.msgbox("Error: %s" % param.error, width=WIDTH)
             param.error = None
             continue
-        break
+        elif param.is_list:
+            if action == 'add':
+                default_item = len(param.value)
+            elif delete:
+                default_item = (default_item - 1) if default_item > 1 else 1
+
+        if not param.is_list or len(param.value) == 0:
+            break
+
+    return True
+
+
+def copy_file(d, src, dest):
+    """Copy src file to dest"""
+
+    assert os.path.exists(src), "File: `%s' does not exist" % src
+
+    if os.path.exists(dest):
+        if d.yesno("File: `%s' exists! Are you sure you want to overwrite it?",
+                   defaultno=1, width=WIDTH):
+            return False
 
+    shutil.copyfile(src, dest)
+    d.msgbox("File: `%s' was successfully written!")
     return True
 
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/dialog_wizard.py b/image_creator/dialog_wizard.py
index 6104c66209be7b492916bffcdd49175d885a1c6b..70a5f572006bc80b7140bb13b5820491907454b6 100644
--- a/image_creator/dialog_wizard.py
+++ b/image_creator/dialog_wizard.py
@@ -294,7 +294,7 @@ class WizardMenuPage(WizardPageWthChoices):
 def start_wizard(session):
     """Run the image creation wizard"""
 
-    metadata = session['metadata']
+    metadata = session['image'].meta
     distro = session['image'].distro
     ostype = session['image'].ostype
 
@@ -445,7 +445,7 @@ def create_image(session, answers):
     image = session['image']
 
     with_progress = OutputWthProgress(True)
-    image.out.add(with_progress)
+    image.out.append(with_progress)
     try:
         image.out.clear()
 
@@ -458,15 +458,14 @@ def create_image(session, answers):
 
         update_background_title(session)
 
-        metadata.update(image.meta)
         metadata['DESCRIPTION'] = answers['ImageDescription']
 
         # MD5
         session['checksum'] = image.md5()
 
-        image.out.output()
+        image.out.info()
         try:
-            image.out.output("Uploading image to the cloud:")
+            image.out.info("Uploading image to the cloud:")
             account = Kamaki.get_account(answers['Cloud'])
             assert account, "Cloud: %s is not valid" % answers['Cloud']
             kamaki = Kamaki(account, image.out)
@@ -479,33 +478,33 @@ def create_image(session, answers):
                                            "(1/3)  Calculating block hashes",
                                            "(2/3)  Uploading image blocks")
 
-            image.out.output("(3/3)  Uploading md5sum file ...", False)
+            image.out.info("(3/3)  Uploading md5sum file ...", False)
             md5sumstr = '%s %s\n' % (session['checksum'], name)
             kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr),
                           remote_path="%s.%s" % (name, 'md5sum'))
             image.out.success('done')
-            image.out.output()
+            image.out.info()
 
-            image.out.output('Registering %s image with the cloud ...' %
-                             answers['RegistrationType'].lower(), False)
+            image.out.info('Registering %s image with the cloud ...' %
+                           answers['RegistrationType'].lower(), False)
             result = kamaki.register(answers['ImageName'], remote, metadata,
                                      answers['RegistrationType'] == "Public")
             image.out.success('done')
-            image.out.output("Uploading metadata file ...", False)
+            image.out.info("Uploading metadata file ...", False)
             metastring = unicode(json.dumps(result, ensure_ascii=False))
             kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
                           remote_path="%s.%s" % (name, 'meta'))
             image.out.success('done')
 
             if answers['RegistrationType'] == "Public":
-                image.out.output("Sharing md5sum file ...", False)
+                image.out.info("Sharing md5sum file ...", False)
                 kamaki.share("%s.md5sum" % name)
                 image.out.success('done')
-                image.out.output("Sharing metadata file ...", False)
+                image.out.info("Sharing metadata file ...", False)
                 kamaki.share("%s.meta" % name)
                 image.out.success('done')
 
-            image.out.output()
+            image.out.info()
 
         except ClientError as error:
             raise FatalError("Storage service client: %d %s" %
diff --git a/image_creator/disk.py b/image_creator/disk.py
index 81bb5be4e097e2271ce353d567c6f375e64f3242..7cae1d39aff0ab59f192e48faab9b40a561bcb9a 100644
--- a/image_creator/disk.py
+++ b/image_creator/disk.py
@@ -43,7 +43,7 @@ def get_tmp_dir(default=None):
 
     TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt']
 
-    space = map(free_space, TMP_CANDIDATES)
+    space = [free_space(t) for t in TMP_CANDIDATES]
 
     max_idx = 0
     max_val = space[0]
@@ -130,7 +130,7 @@ class Disk(object):
         if self._file is not None:
             return self._file
 
-        self.out.output("Examining source media `%s' ..." % self.source, False)
+        self.out.info("Examining source media `%s' ..." % self.source, False)
         mode = os.stat(self.source).st_mode
         if stat.S_ISDIR(mode):
             self.out.success('looks like a directory')
@@ -159,7 +159,7 @@ class Disk(object):
         # Examine media file
         info = image_info(self.file)
 
-        self.out.output("Snapshotting media source ...", False)
+        self.out.info("Snapshotting media source ...", False)
 
         # Create a qcow2 snapshot for image files that are not raw
         if info['format'] != 'raw':
diff --git a/image_creator/gpt.py b/image_creator/gpt.py
index b473ec32a07e2468ab8523f9e01a1726283e8ca5..b1ec3fc0f28fba8a7224930f88f5518be943bb51 100644
--- a/image_creator/gpt.py
+++ b/image_creator/gpt.py
@@ -64,7 +64,8 @@ class MBR(object):
             return "%d %s %d %s %d %d" % (self.status, start, self.type, end,
                                           self.first_sector, self.sector_count)
 
-        def unpack_chs(self, chs):
+        @staticmethod
+        def unpack_chs(chs):
             """Unpacks a CHS address string to a tuple."""
 
             assert len(chs) == 3
@@ -76,7 +77,8 @@ class MBR(object):
 
             return (cylinder, head, sector)
 
-        def pack_chs(self, cylinder, head, sector):
+        @staticmethod
+        def pack_chs(cylinder, head, sector):
             """Packs a CHS tuple to an address string."""
 
             assert 1 <= sector <= 63
diff --git a/image_creator/image.py b/image_creator/image.py
index 2e243e86d88a588afad7b878b99a90ca3dee000f..91263cfa6c27bf4838e5a925e28506a5af2e1347 100644
--- a/image_creator/image.py
+++ b/image_creator/image.py
@@ -17,7 +17,7 @@
 
 """Module hosting the Image class."""
 
-from image_creator.util import FatalError, QemuNBD
+from image_creator.util import FatalError, QemuNBD, get_command
 from image_creator.gpt import GPTPartitionTable
 from image_creator.os_type import os_cls
 
@@ -25,6 +25,7 @@ import re
 import guestfs
 import hashlib
 from sendfile import sendfile
+import threading
 
 
 class Image(object):
@@ -56,6 +57,11 @@ class Image(object):
             raise FatalError("qemu-nbd command is missing, only raw input "
                              "media are supported")
 
+        # Check If MOUNT LOCAL is supported for this guestfs build
+        self.mount_local_support = hasattr(self.g, "mount_local")
+        if self.mount_local_support:
+            self._mount_thread = None
+
     def check_guestfs_version(self, major, minor, release):
         """Checks if the version of the used libguestfs is smaller, equal or
         greater than the one specified by the major, minor and release triplet
@@ -79,7 +85,7 @@ class Image(object):
 
         self.enable_guestfs()
 
-        self.out.output('Inspecting Operating System ...', False)
+        self.out.info('Inspecting Operating System ...', False)
         roots = self.g.inspect_os()
 
         if len(roots) == 0 or len(roots) > 1:
@@ -143,7 +149,7 @@ class Image(object):
         # process has opened pipes. Since the recovery process is an optional
         # feature of libguestfs, it's better to disable it.
         if self.check_guestfs_version(1, 17, 14) >= 0:
-            self.out.output("Enabling recovery process ...", False)
+            self.out.info("Enabling recovery process ...", False)
             self.g.set_recovery_proc(1)
             self.out.success('done')
         else:
@@ -152,7 +158,7 @@ class Image(object):
         # self.g.set_trace(1)
         # self.g.set_verbose(1)
 
-        self.out.output('Launching helper VM (may take a while) ...', False)
+        self.out.info('Launching helper VM (may take a while) ...', False)
         # self.progressbar = self.out.Progress(100, "Launching helper VM",
         #                                     "percent")
         # eh = self.g.set_event_callback(self.progress_callback,
@@ -161,7 +167,8 @@ class Image(object):
             self.g.launch()
         except RuntimeError as e:
             raise FatalError(
-                "Launching libguestfs's helper VM failed! Reason: %s" % str(e))
+                "Launching libguestfs's helper VM failed!\nReason: %s.\n\n"
+                "Please run `libguestfs-test-tool' for more info." % str(e))
 
         self.guestfs_enabled = True
         # self.g.delete_event_callback(eh)
@@ -180,7 +187,7 @@ class Image(object):
             self.out.warn("Guestfs is already disabled")
             return
 
-        self.out.output("Shutting down helper VM ...", False)
+        self.out.info("Shutting down helper VM ...", False)
         self.g.sync()
         # guestfs_shutdown which is the preferred way to shutdown the backend
         # process was introduced in version 1.19.16
@@ -225,7 +232,7 @@ class Image(object):
         # Self gets overwritten
         img = self
 
-        class RawImage:
+        class RawImage(object):
             """The RawImage context manager"""
             def __enter__(self):
                 return img.device if img.format == 'raw' else \
@@ -273,7 +280,7 @@ class Image(object):
 
         if is_logical(last_partition):
             # The disk contains extended and logical partitions....
-            extended = filter(is_extended, partitions)[0]
+            extended = [p for p in partitions if is_extended(p)][0]
             last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
 
             # check if extended is the last primary partition
@@ -406,7 +413,7 @@ class Image(object):
 
         new_size = (end + 1) * sector_size
 
-        assert (new_size <= self.size)
+        assert new_size <= self.size
 
         if self.meta['PARTITION_TABLE'] == 'gpt':
             with self.raw_device(readonly=False) as raw:
@@ -421,6 +428,70 @@ class Image(object):
 
         return self.size
 
+    def mount(self, mpoint, readonly=False):
+        """Mount the image file system under a local directory"""
+
+        assert self.mount_local_support, \
+            "MOUNT LOCAL not supported for this build of libguestfs"
+
+        assert self._mount_thread is None, "Image is already mounted"
+
+        def do_mount():
+            """Use libguestfs's guestmount API"""
+            with self.os.mount(readonly=readonly, silent=True):
+                if self.g.mount_local(mpoint, readonly=readonly) == -1:
+                    return
+                # The thread will block in mount_local_run until the file
+                self.g.mount_local_run()
+
+        self._mount_thread = threading.Thread(target=do_mount)
+        self._mount_thread.mpoint = mpoint
+        self._mount_thread.start()
+
+    def is_mounted(self):
+        """Check if the image is mounted"""
+
+        assert self.mount_local_support, \
+            "MOUNT LOCAL not supported for this build of libguestfs"
+
+        if self._mount_thread is None:
+            return False
+
+        # Wait for 0.1 second to avoid race conditions if the thread is in an
+        # initialization state but not alive yet.
+        self._mount_thread.join(0.1)
+
+        return self._mount_thread.is_alive()
+
+    def umount(self, lazy=False):
+        """umount the previously mounted image file system"""
+
+        assert self.mount_local_support, \
+            "MOUNT LOCAL not supported for this build of libguestfs"
+
+        assert self._mount_thread is not None, "Image is not mounted"
+
+        # Maybe the image was umounted externally
+        if not self._mount_thread.is_alive():
+            self._mount_thread = None
+            return True
+
+        try:
+            args = (['-l'] if lazy else []) + [self._mount_thread.mpoint]
+            get_command('umount')(*args)
+        except:
+            return False
+
+        # Wait for a little while. If the image is umounted, mount_local_run
+        # should have terminated
+        self._mount_thread.join(5)
+
+        if self._mount_thread.is_alive():
+            raise FatalError('Unable to join the mount thread')
+
+        self._mount_thread = None
+        return True
+
     def dump(self, outfile):
         """Dumps the content of the image into a file.
 
diff --git a/image_creator/kamaki_wrapper.py b/image_creator/kamaki_wrapper.py
index 38950ae0ba4b89cab9b9a4b95c6d753061e34f26..57518ba6ef86c022266182280aba043d5f7d2e82 100644
--- a/image_creator/kamaki_wrapper.py
+++ b/image_creator/kamaki_wrapper.py
@@ -21,6 +21,7 @@ deployment.
 """
 
 import sys
+import logging
 
 from os.path import basename
 
@@ -37,6 +38,8 @@ except ImportError:
     pass
 
 try:
+    logger = logging.getLogger("kamaki.cli.config")
+    logger.setLevel(logging.ERROR)
     config = Config()
 except Exception as e:
     sys.stderr.write("Kamaki config error: %s\n" % str(e))
diff --git a/image_creator/main.py b/image_creator/main.py
index 4dffc5c46c3a8e0165da1aee9ca1738eb8cc47a0..9407a6d22319a9b40996baf8c1ac95a2171a585f 100644
--- a/image_creator/main.py
+++ b/image_creator/main.py
@@ -33,9 +33,13 @@ import StringIO
 import signal
 import json
 import textwrap
+import tempfile
+import subprocess
+import time
 
 
 def check_writable_dir(option, opt_str, value, parser):
+    """Check if a directory is writable"""
     dirname = os.path.dirname(value)
     name = os.path.basename(value)
     if dirname and not os.path.isdir(dirname):
@@ -48,95 +52,105 @@ def check_writable_dir(option, opt_str, value, parser):
 
 
 def parse_options(input_args):
+    """Parse input parameters"""
     usage = "Usage: %prog [options] <input_media>"
     parser = optparse.OptionParser(version=version, usage=usage)
 
-    parser.add_option("-o", "--outfile", type="string", dest="outfile",
-                      default=None, action="callback",
-                      callback=check_writable_dir, help="dump image to FILE",
-                      metavar="FILE")
-
-    parser.add_option("-f", "--force", dest="force", default=False,
-                      action="store_true",
-                      help="overwrite output files if they exist")
-
-    parser.add_option("-s", "--silent", dest="silent", default=False,
-                      help="output only errors",
-                      action="store_true")
-
-    parser.add_option("-u", "--upload", dest="upload", type="string",
-                      default=False,
-                      help="upload the image to the cloud with name FILENAME",
-                      metavar="FILENAME")
-
-    parser.add_option("-r", "--register", dest="register", type="string",
-                      default=False,
-                      help="register the image with a cloud as IMAGENAME",
-                      metavar="IMAGENAME")
-
-    parser.add_option("-m", "--metadata", dest="metadata", default=[],
-                      help="add custom KEY=VALUE metadata to the image",
-                      action="append", metavar="KEY=VALUE")
-
-    parser.add_option("-t", "--token", dest="token", type="string",
-                      default=None, help="use this authentication token when "
-                      "uploading/registering images")
-
     parser.add_option("-a", "--authentication-url", dest="url", type="string",
                       default=None, help="use this authentication URL when "
                       "uploading/registering images")
 
+    parser.add_option("--allow-unsupported", dest="allow_unsupported",
+                      help="proceed with the image creation even if the media "
+                      "is not supported", default=False, action="store_true")
+
     parser.add_option("-c", "--cloud", dest="cloud", type="string",
                       default=None, help="use this saved cloud account to "
                       "authenticate against a cloud when "
                       "uploading/registering images")
 
-    parser.add_option("--print-syspreps", dest="print_syspreps", default=False,
-                      help="print the enabled and disabled system preparation "
-                      "operations for this input media", action="store_true")
+    parser.add_option("--disable-sysprep", dest="disabled_syspreps",
+                      help="prevent SYSPREP operation from running on the "
+                      "input media", default=[], action="append",
+                      metavar="SYSPREP")
 
     parser.add_option("--enable-sysprep", dest="enabled_syspreps", default=[],
                       help="run SYSPREP operation on the input media",
                       action="append", metavar="SYSPREP")
 
-    parser.add_option("--disable-sysprep", dest="disabled_syspreps",
-                      help="prevent SYSPREP operation from running on the "
-                      "input media", default=[], action="append",
-                      metavar="SYSPREP")
+    parser.add_option("-f", "--force", dest="force", default=False,
+                      action="store_true",
+                      help="overwrite output files if they exist")
+
+    parser.add_option("--host-run", dest="host_run", default=[],
+                      help="mount the media in the host and run a script "
+                      "against the guest media. This option may be defined "
+                      "multiple times. The script's working directory will be "
+                      "the guest's root directory. BE CAREFUL! DO NOT USE "
+                      "ABSOLUTE PATHS INSIDE THE SCRIPT! YOU MAY HARM YOUR "
+                      "SYSTEM!", metavar="SCRIPT", action="append")
 
     parser.add_option("--install-virtio", dest="virtio", type="string",
                       help="install VirtIO drivers hosted under DIR "
                       "(Windows only)", metavar="DIR")
-    parser.add_option("--print-sysprep-params", dest="print_sysprep_params",
-                      default=False, action="store_true",
-                      help="print the defined system preparation parameters "
-                      "for this input media")
-
-    parser.add_option("--sysprep-param", dest="sysprep_params", default=[],
-                      help="add KEY=VALUE system preparation parameter",
-                      action="append")
 
-    parser.add_option("--no-sysprep", dest="sysprep", default=True,
-                      help="don't perform any system preparation operation",
-                      action="store_false")
+    parser.add_option("-m", "--metadata", dest="metadata", default=[],
+                      help="add custom KEY=VALUE metadata to the image",
+                      action="append", metavar="KEY=VALUE")
 
     parser.add_option("--no-snapshot", dest="snapshot", default=True,
                       help="don't snapshot the input media. (THIS IS "
                       "DANGEROUS AS IT WILL ALTER THE ORIGINAL MEDIA!!!)",
                       action="store_false")
 
+    parser.add_option("--no-sysprep", dest="sysprep", default=True,
+                      help="don't perform any system preparation operation",
+                      action="store_false")
+
+    parser.add_option("-o", "--outfile", type="string", dest="outfile",
+                      default=None, action="callback", metavar="FILE",
+                      callback=check_writable_dir, help="dump image to FILE")
+
+    parser.add_option("--print-metadata", dest="print_metadata", default=False,
+                      help="print the detected image metadata",
+                      action='store_true')
+
+    parser.add_option("--print-syspreps", dest="print_syspreps", default=False,
+                      help="print the enabled and disabled system preparation "
+                      "operations for this input media", action="store_true")
+
+    parser.add_option("--print-sysprep-params", dest="print_sysprep_params",
+                      default=False, action="store_true",
+                      help="print the defined system preparation parameters "
+                      "for this input media")
+
     parser.add_option("--public", dest="public", default=False,
                       help="register image with the cloud as public",
                       action="store_true")
 
-    parser.add_option("--allow-unsupported", dest="allow_unsupported",
-                      help="proceed with the image creation even if the media "
-                      "is not supported", default=False, action="store_true")
+    parser.add_option("-r", "--register", dest="register", type="string",
+                      default=False, metavar="IMAGENAME",
+                      help="register the image with a cloud as IMAGENAME")
+
+    parser.add_option("-s", "--silent", dest="silent", default=False,
+                      help="output only errors", action="store_true")
+
+    parser.add_option("--sysprep-param", dest="sysprep_params", default=[],
+                      help="add KEY=VALUE system preparation parameter",
+                      action="append")
+
+    parser.add_option("-t", "--token", dest="token", type="string",
+                      default=None, help="use this authentication token when "
+                      "uploading/registering images")
 
     parser.add_option("--tmpdir", dest="tmp", type="string", default=None,
                       help="create large temporary image files under DIR",
                       metavar="DIR")
 
+    parser.add_option("-u", "--upload", dest="upload", type="string",
+                      default=False, metavar="FILENAME",
+                      help="upload the image to the cloud with name FILENAME")
+
     options, args = parser.parse_args(input_args)
 
     if len(args) != 1:
@@ -189,22 +203,25 @@ def parse_options(input_args):
 
 
 def image_creator():
+    """snf-mkimage main function"""
     options = parse_options(sys.argv[1:])
 
     if options.outfile is None and not options.upload and not \
-            options.print_syspreps and not options.print_sysprep_params:
-        raise FatalError("At least one of `-o', `-u', `--print-syspreps' or "
-                         "`--print-sysprep-params' must be set")
+            options.print_syspreps and not options.print_sysprep_params \
+            and not options.print_metadata:
+        raise FatalError("At least one of `-o', `-u', `--print-syspreps', "
+                         "`--print-sysprep-params' or `--print-metadata' must "
+                         "be set")
 
     if options.silent:
-        out = SilentOutput()
+        out = SilentOutput(colored=sys.stderr.isatty())
     else:
-        out = OutputWthProgress(True) if sys.stderr.isatty() else \
-            SimpleOutput(False)
+        out = OutputWthProgress() if sys.stderr.isatty() else \
+            SimpleOutput(colored=False)
 
     title = 'snf-image-creator %s' % version
-    out.output(title)
-    out.output('=' * len(title))
+    out.info(title)
+    out.info('=' * len(title))
 
     if os.geteuid() != 0:
         raise FatalError("You must run %s as root"
@@ -281,6 +298,17 @@ def image_creator():
                               "not be cleared out of sensitive data and will "
                               "not get customized during the deployment."))
 
+        if len(options.host_run) != 0 and not image.mount_local_support:
+            raise FatalError("Running scripts against the guest media is not "
+                             "supported for this build of libguestfs.")
+
+        if len(options.host_run) != 0:
+            for script in options.host_run:
+                if not os.path.isfile(script):
+                    raise FatalError("File: `%s' does not exist." % script)
+                if not os.access(script, os.X_OK):
+                    raise FatalError("File: `%s' is not executable." % script)
+
         for sysprep in options.disabled_syspreps:
             image.os.disable_sysprep(image.os.get_sysprep_by_name(sysprep))
 
@@ -289,11 +317,15 @@ def image_creator():
 
         if options.print_syspreps:
             image.os.print_syspreps()
-            out.output()
+            out.info()
 
         if options.print_sysprep_params:
             image.os.print_sysprep_params()
-            out.output()
+            out.info()
+
+        if options.print_metadata:
+            image.os.print_metadata()
+            out.info()
 
         if options.outfile is None and not options.upload:
             return 0
@@ -302,42 +334,66 @@ def image_creator():
                 hasattr(image.os, 'install_virtio_drivers'):
             image.os.install_virtio_drivers()
 
+        if len(options.host_run) != 0:
+            out.info("Running scripts on the input media:")
+            mpoint = tempfile.mkdtemp()
+            try:
+                image.mount(mpoint)
+                if not image.is_mounted():
+                    raise FatalError("Mounting the media on the host failed.")
+                try:
+                    size = len(options.host_run)
+                    cnt = 1
+                    for script in options.host_run:
+                        script = os.path.abspath(script)
+                        out.info(("(%d/%d)" % (cnt, size)).ljust(7), False)
+                        out.info("Running `%s'" % script)
+                        ret = subprocess.Popen([script], cwd=mpoint).wait()
+                        if ret != 0:
+                            raise FatalError("Script: `%s' failed (rc=%d)" %
+                                             (script, ret))
+                        cnt += 1
+                finally:
+                    while not image.umount():
+                        out.warn("Unable to umount the media. Retrying ...")
+                        time.sleep(1)
+                    out.info()
+            finally:
+                os.rmdir
+
         if options.sysprep:
             image.os.do_sysprep()
 
-        metadata = image.os.meta
-        metadata.update(image.meta)
-
         if image.is_unsupported():
-            metadata['EXCLUDE_ALL_TASKS'] = "yes"
+            image.meta['EXCLUDE_ALL_TASKS'] = "yes"
 
         # Add command line metadata to the collected ones...
-        metadata.update(options.metadata)
+        image.meta.update(options.metadata)
 
         checksum = image.md5()
 
         metastring = unicode(json.dumps(
-            {'properties': metadata,
+            {'properties': image.meta,
              'disk-format': 'diskdump'}, ensure_ascii=False))
 
         if options.outfile is not None:
             image.dump(options.outfile)
 
-            out.output('Dumping metadata file ...', False)
+            out.info('Dumping metadata file ...', False)
             with open('%s.%s' % (options.outfile, 'meta'), 'w') as f:
                 f.write(metastring)
             out.success('done')
 
-            out.output('Dumping md5sum file ...', False)
+            out.info('Dumping md5sum file ...', False)
             with open('%s.%s' % (options.outfile, 'md5sum'), 'w') as f:
                 f.write('%s %s\n' % (checksum,
                                      os.path.basename(options.outfile)))
             out.success('done')
 
-        out.output()
+        out.info()
         try:
             if options.upload:
-                out.output("Uploading image to the storage service:")
+                out.info("Uploading image to the storage service:")
                 with image.raw_device() as raw:
                     with open(raw, 'rb') as f:
                         remote = kamaki.upload(
@@ -345,42 +401,43 @@ def image_creator():
                             "(1/3)  Calculating block hashes",
                             "(2/3)  Uploading missing blocks")
 
-                out.output("(3/3)  Uploading md5sum file ...", False)
+                out.info("(3/3)  Uploading md5sum file ...", False)
                 md5sumstr = '%s %s\n' % (checksum,
                                          os.path.basename(options.upload))
                 kamaki.upload(StringIO.StringIO(md5sumstr),
                               size=len(md5sumstr),
                               remote_path="%s.%s" % (options.upload, 'md5sum'))
                 out.success('done')
-                out.output()
+                out.info()
 
             if options.register:
                 img_type = 'public' if options.public else 'private'
-                out.output('Registering %s image with the compute service ...'
-                           % img_type, False)
+                out.info('Registering %s image with the compute service ...'
+                         % img_type, False)
                 result = kamaki.register(options.register, remote,
-                                         metadata, options.public)
+                                         image.meta, options.public)
                 out.success('done')
-                out.output("Uploading metadata file ...", False)
-                metastring = unicode(json.dumps(result, ensure_ascii=False))
+                out.info("Uploading metadata file ...", False)
+                metastring = unicode(json.dumps(result, ensure_ascii=False,
+                                                indent=4))
                 kamaki.upload(StringIO.StringIO(metastring),
                               size=len(metastring),
                               remote_path="%s.%s" % (options.upload, 'meta'))
                 out.success('done')
                 if options.public:
-                    out.output("Sharing md5sum file ...", False)
+                    out.info("Sharing md5sum file ...", False)
                     kamaki.share("%s.md5sum" % options.upload)
                     out.success('done')
-                    out.output("Sharing metadata file ...", False)
+                    out.info("Sharing metadata file ...", False)
                     kamaki.share("%s.meta" % options.upload)
                     out.success('done')
-
-                out.output()
+                out.result(json.dumps(result, indent=4, ensure_ascii=False))
+                out.info()
         except ClientError as e:
             raise FatalError("Service client: %d %s" % (e.status, e.message))
 
     finally:
-        out.output('cleaning up ...')
+        out.info('cleaning up ...')
         disk.cleanup()
 
     out.success("snf-image-creator exited without errors")
@@ -389,11 +446,11 @@ def image_creator():
 
 
 def main():
+    """Main entry point"""
     try:
         sys.exit(image_creator())
     except FatalError as e:
-        colored = sys.stderr.isatty()
-        SimpleOutput(colored).error(e)
+        SimpleOutput(colored=sys.stderr.isatty()).error(e)
         sys.exit(1)
 
 if __name__ == '__main__':
diff --git a/image_creator/os_type/__init__.py b/image_creator/os_type/__init__.py
index ea8a1938fb8aeddc3df8340fd4a7ebb730deae22..b4195dd8126c7aeab6e034396b430a85df7191ab 100644
--- a/image_creator/os_type/__init__.py
+++ b/image_creator/os_type/__init__.py
@@ -26,6 +26,17 @@ import re
 from collections import namedtuple
 from functools import wraps
 
+OSTYPE_ORDER = {
+    "windows": 8,
+    "linux": 7,
+    "freebsd": 6,
+    "netbsd": 5,
+    "openbsd": 4,
+    "hurd": 3,
+    "dos": 2,
+    "minix": 1
+}
+
 
 def os_cls(distro, osfamily):
     """Given the distro name and the osfamily, return the appropriate OSBase
@@ -77,7 +88,7 @@ def sysprep(message, enabled=True, **kwargs):
         @wraps(method)
         def inner(self, print_message=True):
             if print_message:
-                self.out.output(message)
+                self.out.info(message)
             return method(self)
 
         return inner
@@ -87,11 +98,11 @@ def sysprep(message, enabled=True, **kwargs):
 class SysprepParam(object):
     """This class represents a system preparation parameter"""
 
-    def __init__(self, type, default, description, **kwargs):
-
-        assert hasattr(self, "_check_%s" % type), "Invalid type: %s" % type
+    def __init__(self, name, type, default, description, **kwargs):
 
-        self.type = type
+        self.name = name
+        self.is_list = type.startswith('list:')
+        self.type = type.split(':', 1)[1] if self.is_list else type
         self.default = default
         self.description = description
         self.value = default
@@ -99,18 +110,29 @@ class SysprepParam(object):
         self.check = kwargs['check'] if 'check' in kwargs else lambda x: x
         self.hidden = kwargs['hidden'] if 'hidden' in kwargs else False
 
+        assert hasattr(self, "_check_%s" % self.type), \
+            "Invalid type: %s" % self.type
+
     def set_value(self, value):
         """Update the value of the parameter"""
 
         check_type = getattr(self, "_check_%s" % self.type)
-        try:
-            self.value = self.check(check_type(value))
-        except ValueError as e:
-            self.error = e.message
-            return False
+
+        tmp = []
+
+        for item in value if self.is_list else [value]:
+            try:
+                tmp.append(self.check(check_type(item)))
+            except ValueError as e:
+                self.error = e.message
+                return False
+
+        self.value = tmp if self.is_list else tmp[0]
+
         return True
 
-    def _check_posint(self, value):
+    @staticmethod
+    def _check_posint(value):
         """Check if the value is a positive integer"""
         try:
             value = int(value)
@@ -122,11 +144,13 @@ class SysprepParam(object):
 
         return value
 
-    def _check_string(self, value):
+    @staticmethod
+    def _check_string(value):
         """Check if a value is a string"""
         return str(value)
 
-    def _check_file(self, value):
+    @staticmethod
+    def _check_file(value):
         """Check if the value is a valid filename"""
 
         value = str(value)
@@ -146,7 +170,8 @@ class SysprepParam(object):
 
         raise ValueError("Invalid filename")
 
-    def _check_dir(self, value):
+    @staticmethod
+    def _check_dir(value):
         """Check if the value is a valid directory"""
 
         value = str(value)
@@ -174,7 +199,7 @@ def add_sysprep_param(name, type, default, descr, **kwargs):
                 self.sysprep_params = {}
 
             self.sysprep_params[name] = \
-                SysprepParam(type, default, descr, **extra)
+                SysprepParam(name, type, default, descr, **extra)
             init(self, *args, **kwargs)
         return inner
     return wrapper
@@ -212,11 +237,24 @@ class OSBase(object):
                     self.out.warn("Ignoring invalid `%s' parameter." % key)
                     continue
                 param = self.sysprep_params[key]
+                if param.is_list:
+                    def split_in_comma(val):
+                        tmp = val.split(',')
+                        prev = ""
+                        for i in xrange(len(tmp)):
+                            item = prev + tmp[i]
+                            if item.endswith('\\'):
+                                prev = item[:-1]
+                                continue
+                            prev = ""
+                            yield item
+                    val = list(split_in_comma(val))
+
                 if not param.set_value(val):
                     raise FatalError("Invalid value for sysprep parameter: "
                                      "`%s'. Reason: %s" % (key, param.error))
 
-        self.meta = {}
+        self.meta = self.image.meta
         self.shrinked = False
 
         # This will host the error if mount fails
@@ -268,21 +306,21 @@ class OSBase(object):
         if self.image.is_unsupported():
             return
 
-        self.out.output('Running OS inspection:')
+        self.out.info('Running OS inspection:')
         with self.mount(readonly=True, silent=True):
             self._do_inspect()
-        self.out.output()
+        self.out.info()
 
     def collect_metadata(self):
         """Collect metadata about the OS"""
 
-        self.out.output('Collecting image metadata ...', False)
+        self.out.info('Collecting image metadata ...', False)
 
         with self.mount(readonly=True, silent=True):
             self._do_collect_metadata()
 
         self.out.success('done')
-        self.out.output()
+        self.out.info()
 
     def list_syspreps(self):
         """Returns a list of sysprep objects"""
@@ -337,8 +375,17 @@ class OSBase(object):
 
         return self._sysprep_tasks[obj.__name__]
 
+    def print_metadata(self):
+        """Print the image metadata"""
+
+        self.out.info("Detected image metadata:")
+
+        col_width = max(len(key) for key in self.meta) + 2
+        for key, val in self.meta.items():
+            self.out.info("%s %s" % (key.ljust(col_width), val))
+
     def print_syspreps(self):
-        """Print enabled and disabled system preparation operations."""
+        """Print enabled and disabled system preparation operations"""
 
         syspreps = self.list_syspreps()
         enabled = [s for s in syspreps if self.sysprep_enabled(s)]
@@ -349,53 +396,57 @@ class OSBase(object):
         wrapper.initial_indent = '\t'
         wrapper.width = 72
 
-        self.out.output("Enabled system preparation operations:")
+        self.out.info("Enabled system preparation operations:")
         if len(enabled) == 0:
-            self.out.output("(none)")
+            self.out.info("(none)")
         else:
             for sysprep in enabled:
                 name = sysprep.__name__.replace('_', '-')[1:]
                 descr = wrapper.fill(textwrap.dedent(sysprep.__doc__))
-                self.out.output('    %s:\n%s\n' % (name, descr))
+                self.out.info('    %s:\n%s\n' % (name, descr))
 
-        self.out.output("Disabled system preparation operations:")
+        self.out.info("Disabled system preparation operations:")
         if len(disabled) == 0:
-            self.out.output("(none)")
+            self.out.info("(none)")
         else:
             for sysprep in disabled:
                 name = sysprep.__name__.replace('_', '-')[1:]
                 descr = wrapper.fill(textwrap.dedent(sysprep.__doc__))
-                self.out.output('    %s:\n%s\n' % (name, descr))
+                self.out.info('    %s:\n%s\n' % (name, descr))
 
     def print_sysprep_params(self):
         """Print the system preparation parameter the user may use"""
 
-        self.out.output("System preparation parameters:")
-        self.out.output()
+        self.out.info("System preparation parameters:")
+        self.out.info()
 
         public_params = [(n, p) for n, p in self.sysprep_params.items()
                          if not p.hidden]
         if len(public_params) == 0:
-            self.out.output("(none)")
+            self.out.info("(none)")
             return
 
         wrapper = textwrap.TextWrapper()
         wrapper.subsequent_indent = "             "
-        wrapper.width = 72
+        wrapper.width = 80
 
         for name, param in public_params:
             if param.hidden:
                 continue
-            self.out.output("NAME:        %s" % name)
-            self.out.output("VALUE:       %s" % param.value)
-            self.out.output(
-                wrapper.fill("DESCRIPTION: %s" % param.description))
-            self.out.output()
+            self.out.info("NAME:".ljust(13) + name)
+            self.out.info(wrapper.fill("DESCRIPTION:".ljust(13) +
+                                       "%s" % param.description))
+            self.out.info("TYPE:".ljust(13) + "%s%s" %
+                          ("list:" if param.is_list else "", param.type))
+            self.out.info("VALUE:".ljust(13) +
+                          ("\n".ljust(14).join(param.value) if param.is_list
+                           else param.value))
+            self.out.info()
 
     def do_sysprep(self):
         """Prepare system for image creation."""
 
-        self.out.output('Preparing system for image creation:')
+        self.out.info('Preparing system for image creation:')
 
         if self.image.is_unsupported():
             self.out.warn(
@@ -407,7 +458,7 @@ class OSBase(object):
         cnt = 0
 
         def exec_sysprep(cnt, size, task):
-            self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
+            self.out.info(('(%d/%d)' % (cnt, size)).ljust(7), False)
             task()
             del self._sysprep_tasks[task.__name__]
 
@@ -420,7 +471,7 @@ class OSBase(object):
             cnt += 1
             exec_sysprep(cnt, size, task)
 
-        self.out.output()
+        self.out.info()
 
     @sysprep('Shrinking image (may take a while)', nomount=True)
     def _shrink(self):
@@ -436,7 +487,7 @@ class OSBase(object):
         """Returns a context manager for mounting an image"""
 
         parent = self
-        output = lambda msg='', nl=True: None if silent else self.out.output
+        output = lambda msg='', nl=True: None if silent else self.out.info
         success = lambda msg='', nl=True: None if silent else self.out.success
         warn = lambda msg='', nl=True: None if silent else self.out.warn
 
@@ -517,6 +568,8 @@ class OSBase(object):
           http://libguestfs.org/guestfs.3.html#guestfs_readdir
 
         * exclude: Exclude all files that follow this pattern.
+
+        * include: Only include files that follow this pattern.
         """
         if not self.image.g.is_dir(directory):
             self.out.warn("Directory: `%s' does not exist!" % directory)
@@ -531,6 +584,7 @@ class OSBase(object):
         kwargs['maxdepth'] = maxdepth
 
         exclude = None if 'exclude' not in kwargs else kwargs['exclude']
+        include = None if 'include' not in kwargs else kwargs['include']
         ftype = None if 'ftype' not in kwargs else kwargs['ftype']
         has_ftype = lambda x, y: y is None and True or x['ftyp'] == y
 
@@ -543,6 +597,9 @@ class OSBase(object):
             if exclude and re.match(exclude, full_path):
                 continue
 
+            if include and not re.match(include, full_path):
+                continue
+
             if has_ftype(f, 'd'):
                 self._foreach_file(full_path, action, **kwargs)
 
@@ -552,7 +609,6 @@ class OSBase(object):
     def _do_inspect(self):
         """helper method for inspect"""
         self.out.warn("No inspection method available")
-        pass
 
     def _do_collect_metadata(self):
         """helper method for collect_metadata"""
@@ -564,12 +620,17 @@ class OSBase(object):
             self.out.warn("Unable to identify the partition number from root "
                           "partition: %s" % self.root)
 
-        self.meta['OSFAMILY'] = self.image.g.inspect_get_type(self.root)
-        self.meta['OS'] = self.image.g.inspect_get_distro(self.root)
-        if self.meta['OS'] == "unknown":
-            self.meta['OS'] = self.meta['OSFAMILY']
-        self.meta['DESCRIPTION'] = \
-            self.image.g.inspect_get_product_name(self.root)
+        osfamily = self.image.g.inspect_get_type(self.root)
+        distro = self.image.g.inspect_get_distro(self.root)
+        name = self.image.g.inspect_get_product_name(self.root)
+
+        self.meta['OSFAMILY'] = osfamily
+        self.meta['OS'] = distro if distro != "unknown" else osfamily
+        self.meta['DESCRIPTION'] = name
+        try:
+            self.meta['SORTORDER'] = 1000000 * OSTYPE_ORDER[osfamily]
+        except KeyError:
+            self.meta['SORTORDER'] = 0
 
     def _do_mount(self, readonly):
         """helper method for mount"""
diff --git a/image_creator/os_type/archlinux.py b/image_creator/os_type/archlinux.py
new file mode 100644
index 0000000000000000000000000000000000000000..aac47a2d6dd894da9a3e3b7deff52666f5752480
--- /dev/null
+++ b/image_creator/os_type/archlinux.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GRNET S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module hosts OS-specific code for Arch Linux."""
+
+from image_creator.os_type.linux import Linux
+
+import re
+
+
+class Archlinux(Linux):
+    """OS class for Arch Linux"""
+
+    def _do_collect_metadata(self):
+        """Collect metadata about the OS"""
+        super(Archlinux, self)._do_collect_metadata()
+
+        local_be = '/var/lib/pacman/local'
+
+        if not self.image.g.is_dir(local_be):
+            self.out.warn("Directory: `%s' does not exist!" % local_be)
+            return
+
+        kernel_regexp = re.compile(r'linux-(lts-)?(\d+[\.\d+]*-\d+)')
+        for f in self.image.g.readdir(local_be):
+            match = kernel_regexp.match(f['name'])
+            if match:
+                lts = match.group(1) is not None
+                version = match.group(2)
+                self.meta['KERNEL'] = "%s%s" % (version, " LTS" if lts else "")
+
+# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/bsd.py b/image_creator/os_type/bsd.py
new file mode 100644
index 0000000000000000000000000000000000000000..b472325641af0bd4fe67087ad05f67495e25c3f8
--- /dev/null
+++ b/image_creator/os_type/bsd.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2014 GRNET S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module hosts OS-specific code for *BSD."""
+
+from image_creator.os_type.unix import Unix, sysprep
+
+import re
+
+
+class Bsd(Unix):
+    """OS class for *BSD Unix-like operating system"""
+
+    @sysprep("Cleaning up passwords & locking all user accounts")
+    def _cleanup_password(self):
+        """Remove all passwords and lock all user accounts"""
+
+        master_passwd = []
+
+        for line in self.image.g.cat('/etc/master.passwd').splitlines():
+
+            # Check for empty or comment lines
+            if len(line.split('#')[0]) == 0:
+                master_passwd.append(line)
+                continue
+
+            fields = line.split(':')
+            if fields[1] not in ('*', '!'):
+                fields[1] = '!'
+
+            master_passwd.append(":".join(fields))
+
+        self.image.g.write(
+            '/etc/master.passwd', "\n".join(master_passwd) + '\n')
+
+        # Make sure no one can login on the system
+        self.image.g.rm_rf('/etc/spwd.db')
+
+    def _check_enabled_sshd(self):
+        """Check if the ssh daemon is enabled at boot"""
+        return False
+
+    def _do_collect_metadata(self):
+        """Collect metadata about the OS"""
+        super(Bsd, self)._do_collect_metadata()
+
+        users = self._get_passworded_users()
+
+        self.meta["USERS"] = " ".join(users)
+
+        # The original product name key is long and ugly
+        self.meta['DESCRIPTION'] = \
+            self.meta['DESCRIPTION'].split('#')[0].strip()
+
+        # Delete the USERS metadata if empty
+        if not len(self.meta['USERS']):
+            self.out.warn("No passworded users found!")
+            del self.meta['USERS']
+
+        major = self.image.g.inspect_get_major_version(self.root)
+        minor = self.image.g.inspect_get_minor_version(self.root)
+
+        self.meta['KERNEL'] = "%sBSD %d.%d" % \
+            (self.__class__.__name__[:-3], major, minor)
+        self.meta['SORTORDER'] += 100 * major + minor
+
+        # Check if ssh is enabled
+        sshd_enabled = self._check_enabled_sshd()
+
+        if sshd_enabled:
+            ssh = []
+            opts = self.ssh_connection_options(users)
+            for user in opts['users']:
+                ssh.append("ssh:port=%d,user=%s" % (opts['port'], user))
+
+            if 'REMOTE_CONNECTION' not in self.meta:
+                self.meta['REMOTE_CONNECTION'] = ""
+            else:
+                self.meta['REMOTE_CONNECTION'] += " "
+
+            if len(users):
+                self.meta['REMOTE_CONNECTION'] += " ".join(ssh)
+            else:
+                self.meta['REMOTE_CONNECTION'] += "ssh:port=%d" % opts['port']
+        else:
+            self.out.warn("OpenSSH Daemon is not configured to run on boot")
+
+    def _get_passworded_users(self):
+        """Returns a list of non-locked user accounts"""
+        users = []
+        regexp = re.compile(
+            '^([^:]+):((?:![^:]+)|(?:[^!*][^:]+)|):(?:[^:]*:){7}(?:[^:]*)'
+        )
+
+        for line in self.image.g.cat('/etc/master.passwd').splitlines():
+            line = line.split('#')[0]
+            match = regexp.match(line)
+            if not match:
+                continue
+
+            user, passwd = match.groups()
+            if len(passwd) > 0 and passwd[0] == '!':
+                self.out.warn("Ignoring locked %s account." % user)
+            else:
+                # Put root in the beginning.
+                if user == 'root':
+                    users.insert(0, user)
+                else:
+                    users.append(user)
+
+        return users
+
+    def _do_mount(self, readonly):
+        """Mount partitions in the correct order"""
+
+        critical_mpoints = ('/', '/etc', '/root', '/home', '/var')
+
+        mopts1 = "ufstype=44bsd,%s" % ('ro' if readonly else 'rw')
+        mopts2 = "ufstype=ufs2,%s" % ('ro' if readonly else 'rw')
+        for mp, dev in self._mountpoints():
+            try:
+                try:
+                    self.image.g.mount_vfs(mopts2, 'ufs', dev, mp)
+                except RuntimeError:
+                    self.image.g.mount_vfs(mopts1, 'ufs', dev, mp)
+            except RuntimeError as msg:
+                if mp in critical_mpoints:
+                    self._mount_error = str(msg)
+                    return False
+                else:
+                    self._mount_warnings.append('%s (ignored)' % msg)
+
+        return True
+
+# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/freebsd.py b/image_creator/os_type/freebsd.py
index 08c734f12420c04d7a856332d689fc9f6ce42c7e..07237244f326497502239a580022a49731c54dfc 100644
--- a/image_creator/os_type/freebsd.py
+++ b/image_creator/os_type/freebsd.py
@@ -17,57 +17,42 @@
 
 """This module hosts OS-specific code for FreeBSD."""
 
-from image_creator.os_type.unix import Unix, sysprep
+from image_creator.os_type.bsd import Bsd
 
 import re
 
 
-class Freebsd(Unix):
+class Freebsd(Bsd):
     """OS class for FreeBSD Unix-like operating system"""
 
-    @sysprep("Cleaning up passwords & locking all user accounts")
-    def _cleanup_password(self):
-        """Remove all passwords and lock all user accounts"""
+    def _check_enabled_sshd(self):
+        """Check if the sshd is enabled at boot"""
 
-        master_passwd = []
+        sshd_enabled = False
+        sshd_service = re.compile(r'^sshd_enable=.+$')
 
-        for line in self.image.g.cat('/etc/master.passwd').splitlines():
-
-            # Check for empty or comment lines
-            if len(line.split('#')[0]) == 0:
-                master_passwd.append(line)
+        # Freebsd has a checkyesno() functions that tests the service variable
+        # against all those different values in a case insensitive manner!!!
+        sshd_yes = re.compile(r"^sshd_enable=(['\"]?)(YES|TRUE|ON|1)\1$",
+                              re.IGNORECASE)
+        for rc_conf in ('/etc/rc.conf', '/etc/rc.conf.local'):
+            if not self.image.g.is_file(rc_conf):
                 continue
 
-            fields = line.split(':')
-            if fields[1] not in ('*', '!'):
-                fields[1] = '!'
-
-            master_passwd.append(":".join(fields))
-
-        self.image.g.write(
-            '/etc/master.passwd', "\n".join(master_passwd) + '\n')
-
-        # Make sure no one can login on the system
-        self.image.g.rm_rf('/etc/spwd.db')
-
-    def _do_collect_metadata(self):
-        """Collect metadata about the OS"""
-        super(Freebsd, self)._do_collect_metadata()
-        self.meta["USERS"] = " ".join(self._get_passworded_users())
+            for line in self.image.g.cat(rc_conf).splitlines():
+                line = line.split('#')[0].strip()
+                # Be paranoid. Don't stop examining lines after a match. This
+                # is a shell variable and can be overwritten many times. Only
+                # the last match counts.
+                if sshd_service.match(line):
+                    sshd_enabled = sshd_yes.match(line) is not None
 
-        # The original product name key is long and ugly
-        self.meta['DESCRIPTION'] = \
-            self.meta['DESCRIPTION'].split('#')[0].strip()
-
-        # Delete the USERS metadata if empty
-        if not len(self.meta['USERS']):
-            self.out.warn("No passworded users found!")
-            del self.meta['USERS']
+        return sshd_enabled
 
     def _do_inspect(self):
         """Run various diagnostics to check if media is supported"""
 
-        self.out.output('Checking partition table type...', False)
+        self.out.info('Checking partition table type...', False)
         ptype = self.image.g.part_get_parttype(self.image.guestfs_device)
         if ptype != 'gpt':
             self.out.warn("partition table type is: `%s'" % ptype)
@@ -76,27 +61,6 @@ class Freebsd(Unix):
         else:
             self.out.success(ptype)
 
-    def _get_passworded_users(self):
-        """Returns a list of non-locked user accounts"""
-        users = []
-        regexp = re.compile(
-            '^([^:]+):((?:![^:]+)|(?:[^!*][^:]+)|):(?:[^:]*:){7}(?:[^:]*)'
-        )
-
-        for line in self.image.g.cat('/etc/master.passwd').splitlines():
-            line = line.split('#')[0]
-            match = regexp.match(line)
-            if not match:
-                continue
-
-            user, passwd = match.groups()
-            if len(passwd) > 0 and passwd[0] == '!':
-                self.out.warn("Ignoring locked %s account." % user)
-            else:
-                users.append(user)
-
-        return users
-
     def _do_mount(self, readonly):
         """Mount partitions in the correct order"""
 
diff --git a/image_creator/os_type/linux.py b/image_creator/os_type/linux.py
index a9ad62178feddc96d15a6fe5078c88c6fe290bfd..293dcf383bc6ae9647951870d88974581ac13181 100644
--- a/image_creator/os_type/linux.py
+++ b/image_creator/os_type/linux.py
@@ -21,6 +21,40 @@ from image_creator.os_type.unix import Unix, sysprep
 
 import re
 import time
+import pkg_resources
+
+X2GO_DESKTOPSESSIONS = {
+    'CINNAMON': 'cinnamon',
+    'KDE': 'startkde',
+    'GNOME': 'gnome-session',
+    'MATE': 'mate-session',
+    'XFCE': 'xfce4-session',
+    'LXDE': 'startlxde',
+    'TRINITY': 'starttrinity',
+    'UNITY': 'unity',
+}
+
+X2GO_EXECUTABLE = "x2goruncommand"
+
+DISTRO_ORDER = {
+    "ubuntu": 80,
+    "linuxmint": 75,
+    "debian": 70,
+    "rhel": 60,
+    "fedora": 58,
+    "centos": 55,
+    "scientificlinux": 50,
+    "sles": 45,
+    "opensuse": 44,
+    "archlinux": 40,
+    "gentoo": 35,
+    "slackware": 30,
+    "oraclelinux": 28,
+    "mageia": 20,
+    "mandriva": 19,
+    "cirros": 15,
+    "pardus": 10
+}
 
 
 class Linux(Unix):
@@ -286,13 +320,13 @@ class Linux(Unix):
     def _do_inspect(self):
         """Run various diagnostics to check if media is supported"""
 
-        self.out.output(
+        self.out.info(
             'Checking if the media contains logical volumes (LVM)...', False)
 
         has_lvm = True if len(self.image.g.lvs()) else False
 
         if has_lvm:
-            self.out.output()
+            self.out.info()
             self.image.set_unsupported('The media contains logical volumes')
         else:
             self.out.success('no')
@@ -300,13 +334,123 @@ class Linux(Unix):
     def _do_collect_metadata(self):
         """Collect metadata about the OS"""
         super(Linux, self)._do_collect_metadata()
-        self.meta["USERS"] = " ".join(self._get_passworded_users())
+        users = self._get_passworded_users()
+        self.meta["USERS"] = " ".join(users)
 
         # Delete the USERS metadata if empty
         if not len(self.meta['USERS']):
             self.out.warn("No passworded users found!")
             del self.meta['USERS']
 
+        kernels = []
+        for f in self.image.g.ls('/boot'):
+            if f.startswith('config-'):
+                kernels.append(f[7:])
+
+        if len(kernels):
+            kernels.sort(key=pkg_resources.parse_version)
+            self.meta['KERNEL'] = kernels[-1]
+
+        distro = self.image.g.inspect_get_distro(self.root)
+        major = self.image.g.inspect_get_major_version(self.root)
+        if major > 99:
+            major = 99
+        minor = self.image.g.inspect_get_minor_version(self.root)
+        if minor > 99:
+            minor = 99
+        try:
+            self.meta['SORTORDER'] += \
+                10000 * DISTRO_ORDER[distro] + 100 * major + minor
+        except KeyError:
+            pass
+
+        if self.is_enabled('sshd'):
+            ssh = []
+            opts = self.ssh_connection_options(users)
+            for user in opts['users']:
+                ssh.append("ssh:port=%d,user=%s" % (opts['port'], user))
+
+            if 'REMOTE_CONNECTION' not in self.meta:
+                self.meta['REMOTE_CONNECTION'] = ""
+            else:
+                self.meta['REMOTE_CONNECTION'] += " "
+
+            if len(ssh):
+                self.meta['REMOTE_CONNECTION'] += " ".join(ssh)
+            else:
+                self.meta['REMOTE_CONNECTION'] += "ssh:port=%d" % opts['port']
+
+            # Check if x2go is installed
+            x2go_installed = False
+            desktops = set()
+            for path in ('/bin', '/usr/bin', '/usr/local/bin'):
+                if self.image.g.is_file("%s/%s" % (path, X2GO_EXECUTABLE)):
+                    x2go_installed = True
+                for name, exe in X2GO_DESKTOPSESSIONS.items():
+                    if self.image.g.is_file("%s/%s" % (path, exe)):
+                        desktops.add(name)
+
+            if x2go_installed:
+                self.meta['REMOTE_CONNECTION'] += " "
+                if len(desktops) == 0:
+                    self.meta['REMOTE_CONNECTION'] += "x2go"
+                else:
+                    self.meta['REMOTE_CONNECTION'] += \
+                        " ".join(["x2go:session=%s" % d for d in desktops])
+        else:
+            self.out.warn("OpenSSH Daemon is not configured to run on boot")
+
+    def is_enabled(self, service):
+        """Check if a service is enabled to run on boot"""
+
+        systemd_services = '/etc/systemd/system/multi-user.target.wants'
+        exec_start = re.compile(r'^\s*ExecStart=.+bin/%s\s?' % service)
+        if self.image.g.is_dir(systemd_services):
+            for entry in self.image.g.readdir(systemd_services):
+                if entry['ftyp'] not in ('l', 'f'):
+                    continue
+                service_file = "%s/%s" % (systemd_services, entry['name'])
+                for line in self.image.g.cat(service_file).splitlines():
+                    if exec_start.search(line):
+                        return True
+
+        found = set()
+
+        def check_file(path):
+            regexp = re.compile(r"[/=\s'\"]%s('\")?\s" % service)
+            for line in self.image.g.cat(path).splitlines():
+                line = line.split('#', 1)[0].strip()
+                if len(line) == 0:
+                    continue
+                if regexp.search(line):
+                    found.add(path)
+                    return
+
+        # Check upstart config files under /etc/init
+        # Only examine *.conf files
+        if self.image.g.is_dir('/etc/init'):
+            self._foreach_file('/etc/init', check_file, maxdepth=1,
+                               include=r'.+\.conf$')
+            if len(found):
+                return True
+
+        # Check scripts under /etc/rc[1-5].d/ and /etc/rc.d/rc[1-5].d/
+        for conf in ["/etc/%src%d.d" % (d, i) for i in xrange(1, 6)
+                     for d in ('', 'rc.d/')]:
+            try:
+                for entry in self.image.g.readdir(conf):
+                    if entry['ftyp'] not in ('l', 'f'):
+                        continue
+                    check_file("%s/%s" % (conf, entry['name']))
+
+                    if len(found):
+                        return True
+
+            except RuntimeError:
+                continue
+
+        return False
+
     def _get_passworded_users(self):
         """Returns a list of non-locked user accounts"""
         users = []
diff --git a/image_creator/os_type/netbsd.py b/image_creator/os_type/netbsd.py
index 002c5c838a2aa15ddc9e8d1663724b76c5a41730..427eab2f928440edcb0702cc650f218377e79ee2 100644
--- a/image_creator/os_type/netbsd.py
+++ b/image_creator/os_type/netbsd.py
@@ -17,11 +17,31 @@
 
 """This module hosts OS-specific code for NetBSD."""
 
-from image_creator.os_type.unix import Unix
+import re
 
+from image_creator.os_type.bsd import Bsd
 
-class Netbsd(Unix):
+
+class Netbsd(Bsd):
     """OS class for NetBSD"""
-    pass
+
+    def _check_enabled_sshd(self):
+        """Check if the ssh daemon is enabled at boot"""
+
+        sshd_enabled = False
+        sshd_service = re.compile(r'\bsshd=')
+        sshd_yes = re.compile(r"\bsshd=(['\"]?)(YES|TRUE|ON|1)\1\b")
+
+        for rc_conf in ('/etc/defaults/rc.conf', '/etc/rc.conf'):
+            if not self.image.g.is_file(rc_conf):
+                self.out.warn("File: `%s' does not exist!" % rc_conf)
+                continue
+
+            for line in self.image.g.cat(rc_conf).splitlines():
+                line = line.split('#')[0].strip()
+                if sshd_service.match(line):
+                    sshd_enabled = len(sshd_yes.findall(line)) > 0
+
+        return sshd_enabled
 
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/openbsd.py b/image_creator/os_type/openbsd.py
new file mode 100644
index 0000000000000000000000000000000000000000..982813977dbc595e28a181a798c431dfad46b7f2
--- /dev/null
+++ b/image_creator/os_type/openbsd.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2014 GRNET S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module hosts OS-specific code for OpenBSD."""
+
+import re
+
+from image_creator.os_type.bsd import Bsd
+
+
+class Openbsd(Bsd):
+    """OS class for OpenBSD"""
+
+    def _check_enabled_sshd(self):
+        """Check if the ssh daemon is enabled at boot"""
+
+        sshd_enabled = True
+        sshd_service = re.compile(r'^sshd_flags=')
+        sshd_no = re.compile(r"^sshd_flags=(['\"]?)NO\1$")
+
+        for rc_conf in ('/etc/rc.conf', '/etc/rc.conf.local'):
+            if not self.image.g.is_file(rc_conf):
+                self.out.warn("File: `%s' does not exist!" % rc_conf)
+                continue
+
+            for line in self.image.g.cat(rc_conf).splitlines():
+                line = line.split('#')[0].strip()
+                if sshd_service.match(line):
+                    sshd_enabled = sshd_no.match(line) is None
+
+        return sshd_enabled
+
+# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/slackware.py b/image_creator/os_type/slackware.py
index 78127975ddd1b0d9c796fbbc422b10bcbe4c5cf1..bf284765395d0273ecda7a622fe61b9377a51c18 100644
--- a/image_creator/os_type/slackware.py
+++ b/image_creator/os_type/slackware.py
@@ -32,4 +32,16 @@ class Slackware(Linux):
         self._foreach_file('/var/log', self.image.g.truncate, ftype='r',
                            exclude='/var/log/packages')
 
+    def is_enabled(self, service):
+        """Check if a service is enabled to start on boot"""
+
+        name = '/etc/rc.d/%s' % service
+        # In slackware a service will be executed during boot if the
+        # execute bit is set for the root
+        if self.image.g.is_file(name):
+            return self.image.g.stat(name)['mode'] & 0400
+
+        self.out.warn('Service %s not found on the media' % service)
+        return False
+
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/unix.py b/image_creator/os_type/unix.py
index 27fa2a8b2bf8feedb365e92f5ff8f66f5d7cbd94..4999f7dd72523920f6eb4858d1f9dec1f6d35277 100644
--- a/image_creator/os_type/unix.py
+++ b/image_creator/os_type/unix.py
@@ -15,24 +15,101 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-"""This module hosts OS-specific code common to all Unix-like OSs."""
+"""This module hosts OS-specific code common to all Unix-like OSes."""
 
-from image_creator.os_type import OSBase, sysprep
+from image_creator.os_type import OSBase, sysprep, add_sysprep_param
+
+# Credits go to Wmconfig (https://www.arrishq.net/) for the biggest part of
+# this collection
+DESKTOPSESSIONS = {
+    # Put the most popular GUIs first
+    ('startkde',): 'KDE',
+    ('gnome-session', 'gnomesession'): 'GNOME',
+    ('unity',): 'Unity',
+    ('xfce4-session', 'startxfce4', 'xfwm4'): 'Xfce',
+    ('startlxde', 'lxsession'): 'LXDE',
+    ('mate-session',): 'MATE',
+    ('cinnamon', 'gnome-session-cinnamon', 'cinnamon-session'): 'Cinnamon',
+    ('enlightenment',): 'Enlightenment',
+
+    ('aewm',): 'aewm',
+    ('afterstep',): 'AfterStep',
+    ('amiwm',): 'amiwm',
+    ('awesome',): 'awesome',
+    ('blackbox',): 'Blackbox',
+    ('ctwm',): 'CTWM',
+    ('dwm',): 'dwm',
+    ('epiwm',): 'EPIwm',
+    ('fluxbox', 'startfluxbox'): 'Fluxbox',
+    ('flwm',): 'flvm',
+    ('fvwm',): 'FVWM',
+    ('fvwm2',): 'FVWM',
+    ('fvwm95',): 'FVWM95',
+    ('golem',): 'Golem',
+    ('i3',): 'i3',
+    ('icewm', 'icewm-session'): 'IceVM',
+    ('ion', 'ion2', 'ion3'): 'Ion',
+    ('jwm',): "JWM",
+    ('kahakai',): 'Kahakai',
+    ('larswm',): 'larswm',
+    ('mlvwm',): 'MLVWM',
+    ('mwm',): 'Motif Window Manager',
+    ('olvwm',): 'OLVWM',
+    ('olwm',): 'OLWM',
+    ('openbox', 'openbox-session'): 'Openbox',
+    ('particleman', ): 'ParticleMan',
+    ('pekwm',): 'PeKWM',
+    ('pwm', 'pwm2', 'pwm3'): 'PWM',
+    ('qvwm',): 'qvwm',
+    ('rasor-session',): 'Razor-qt',
+    ('ratpoison',): 'Ratpoison',
+    ('sapphire',): 'Sapphire',
+    ('sawfish',): 'Sawfish',
+    ('sithwm',): 'SithWM',
+    ('startede',): 'EDE',
+    ('startstump',): 'StumpWM',
+    ('starttrinity',): 'TDE',
+    ('twm',): 'twm',
+    ('uwm',): 'UDE',
+    ('w9wm',): 'w9wm',
+    ('wmaker',): 'WindowMaker',
+    ('wmx',): 'wmx',
+    ('wmi', 'wmii'): 'wmii',
+    ('windowlab',): 'WindowLab',
+    ('xmonad',): 'xmonad',
+}
+X11_EXECUTABLE = 'startx'
+
+SENSITIVE_USERDATA = [
+    '.history',
+    '.sh_history',
+    '.bash_history',
+    '.zsh_history',
+    '.gnupg',
+    '.ssh',
+    '.kamakirc',
+    '.kamaki.history',
+    '.kamaki.log'
+]
+
+
+def check_sensitive_userdata(value):
+    "Do not allow a zero string"
+
+    if len(value) == 0:
+        raise ValueError("Filename cannot be empty")
+
+    return value
 
 
 class Unix(OSBase):
     """OS class for Unix"""
-    sensitive_userdata = [
-        '.history',
-        '.sh_history',
-        '.bash_history',
-        '.zsh_history',
-        '.gnupg',
-        '.ssh',
-        '.kamakirc',
-        '.kamaki.history',
-        '.kamaki.log'
-    ]
+    @add_sysprep_param(
+        'sensitive_userdata', 'list:string', SENSITIVE_USERDATA,
+        'Files in the home directory of each user that should be removed',
+        check=check_sensitive_userdata)
+    def __init__(self, image, **kwargs):
+        super(Unix, self).__init__(image, **kwargs)
 
     def _mountpoints(self):
         """Return mountpoints in the correct order.
@@ -70,6 +147,64 @@ class Unix(OSBase):
 
         return True
 
+    def _do_collect_metadata(self):
+        super(Unix, self)._do_collect_metadata()
+
+        bin_prefixes = ('', '/usr', '/usr/local')
+        gui = False
+
+        paths = ['%s/bin/%s' % (p, X11_EXECUTABLE) for p in bin_prefixes]
+        for path in paths:
+            if self.image.g.is_file(path):
+                gui = True
+                break
+
+        if not gui:
+            self.meta['GUI'] = "No GUI"
+        else:
+            self.meta['GUI'] = 'Unknown'
+
+        desktop = []
+        for exe, session in DESKTOPSESSIONS.items():
+            paths = ["%s/bin/%s" % (p, e) for p in bin_prefixes for e in exe]
+            for path in paths:
+                if self.image.g.is_file(path):
+                    desktop.append(session)
+                    break
+        if gui and len(desktop) != 0:
+            self.meta['GUI'] = " | ".join(desktop)
+
+    def ssh_connection_options(self, users):
+        """Returns a list of valid ssh connection options"""
+
+        def sshd_config():
+            """Read /etc/ssh/sshd_config and return it as a dictionary"""
+            config = {}
+            fname = '/etc/ssh/sshd_config'
+
+            if not self.image.g.is_file(fname):
+                return {}
+
+            for line in self.image.g.cat(fname).splitlines():
+                line = line.split('#')[0].strip()
+                if not len(line):
+                    continue
+                line = line.split()
+                config[line[0]] = line[1:]
+            return config
+
+        config = sshd_config()
+        try:
+            port = int(config['Port'][0])
+        except:
+            port = 22
+
+        if 'PermitRootLogin' in config and config['PermitRootLogin'] == 'no':
+            if 'root' in users:
+                users.remove('root')
+
+        return {'port': port, 'users': users}
+
     @sysprep('Removing files under /var/cache')
     def _cleanup_cache(self):
         """Remove all regular files under /var/cache"""
@@ -111,8 +246,9 @@ class Unix(OSBase):
         else:
             self.out.warn("Sensitive data won't be scrubbed (not supported)")
 
+        sensitive_userdata = self.sysprep_params['sensitive_userdata'].value
         for homedir in homedirs:
-            for data in self.sensitive_userdata:
+            for data in sensitive_userdata:
                 fname = "%s/%s" % (homedir, data)
                 if self.image.g.is_file(fname):
                     action(fname)
diff --git a/image_creator/os_type/unknown.py b/image_creator/os_type/unknown.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c42fcdb61c70300842af5b88a41c9e0e5f015f1
--- /dev/null
+++ b/image_creator/os_type/unknown.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2014 GRNET S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module hosts code to handle unknown OSes."""
+
+from image_creator.os_type.unsupported import Unsupported
+
+
+class Unknown(Unsupported):
+    """OS class for Unknown OSes"""
+    def __init__(self, image, **kwargs):
+        super(Unsupported, self).__init__(image, **kwargs)
+
+        self.image.set_unsupported("Unknown Operating System")
+
+# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/unsupported.py b/image_creator/os_type/unsupported.py
index 6b8d9cba54ae306096f61ba267b6b19e3aed20d2..41ef24eff69ba72870b877d5c36220b901d05bb9 100644
--- a/image_creator/os_type/unsupported.py
+++ b/image_creator/os_type/unsupported.py
@@ -15,13 +15,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-"""This module hosts code to handle unknown OSs."""
+"""This module hosts code to handle unknown OSes."""
 
 from image_creator.os_type import OSBase
 
 
 class Unsupported(OSBase):
-    """OS class for unsupported OSs"""
+    """OS class for unsupported OSes"""
     def __init__(self, image, **kwargs):
         super(Unsupported, self).__init__(image, **kwargs)
 
@@ -31,7 +31,7 @@ class Unsupported(OSBase):
 
     def _do_mount(self, readonly):
         """Mount partitions in correct order"""
-        self._mount_error = "not supported on this media"
+        self._mount_error = "not supported for this media"
         return False
 
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/os_type/windows/__init__.py b/image_creator/os_type/windows/__init__.py
index b73ec7fdc51c684c3fefecf13d8bc3c9c3c2df37..fccbd2d73092367e11506b62d744f49eb644176e 100644
--- a/image_creator/os_type/windows/__init__.py
+++ b/image_creator/os_type/windows/__init__.py
@@ -16,7 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """This package hosts OS-specific code common for the various Microsoft
-Windows OSs."""
+Windows OSes."""
 
 from image_creator.os_type import OSBase, sysprep, add_sysprep_param
 from image_creator.util import FatalError
@@ -271,7 +271,7 @@ class Windows(OSBase):
         self.registry = Registry(self.image)
 
         with self.mount(readonly=True, silent=True):
-            self.out.output("Checking media state ...", False)
+            self.out.info("Checking media state ...", False)
 
             # Enumerate the windows users
             (self.usernames,
@@ -453,7 +453,7 @@ class Windows(OSBase):
             self.out.warn("Not enough available space to shrink the image!")
             return
 
-        self.out.output("\tReclaiming %dMB ..." % querymax)
+        self.out.info("\tReclaiming %dMB ..." % querymax)
 
         cmd = (
             r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
@@ -473,14 +473,14 @@ class Windows(OSBase):
 
         for line in stdout.splitlines():
             if line.find("%d" % querymax) >= 0:
-                self.out.output(" %s" % line)
+                self.out.info(" %s" % line)
 
         self.shrinked = True
 
     def do_sysprep(self):
         """Prepare system for image creation."""
 
-        self.out.output('Preparing system for image creation:')
+        self.out.info('Preparing system for image creation:')
 
         # Check if winexe is installed
         if not WinEXE.is_installed():
@@ -506,7 +506,7 @@ class Windows(OSBase):
         timeout = self.sysprep_params['boot_timeout'].value
         shutdown_timeout = self.sysprep_params['shutdown_timeout'].value
 
-        self.out.output("Preparing media for boot ...", False)
+        self.out.info("Preparing media for boot ...", False)
 
         with self.mount(readonly=False, silent=True):
 
@@ -514,12 +514,31 @@ class Windows(OSBase):
                 self._add_cleanup('sysprep', self.registry.reset_account,
                                   self.vm.admin.rid, False)
 
-            if self.registry.update_uac(0):
-                self._add_cleanup('sysprep', self.registry.update_uac, 1)
+            old = self.registry.update_uac(0)
+            if old != 0:
+                self._add_cleanup('sysprep', self.registry.update_uac, old)
 
-            if self.registry.update_uac_remote_setting(1):
+            old = self.registry.update_uac_remote_setting(1)
+            if old != 1:
                 self._add_cleanup('sysprep',
-                                  self.registry.update_uac_remote_setting, 0)
+                                  self.registry.update_uac_remote_setting, old)
+
+            def if_not_sysprepped(task, *args):
+                """Only perform this if the image is not sysprepped"""
+                if not self.sysprepped:
+                    task(*args)
+
+            # The next 2 registry values get completely removed by Microsoft
+            # Sysprep. They should not be reverted if Sysprep gets executed.
+            old = self.registry.update_noautoupdate(1)
+            if old != 1:
+                self._add_cleanup('sysprep', if_not_sysprepped,
+                                  self.registry.update_noautoupdate, old)
+
+            old = self.registry.update_auoptions(1)
+            if old != 1:
+                self._add_cleanup('sysprep', if_not_sysprepped,
+                                  self.registry.update_auoptions, old)
 
             # disable the firewalls
             self._add_cleanup('sysprep', self.registry.update_firewalls,
@@ -541,13 +560,13 @@ class Windows(OSBase):
         self.image.disable_guestfs()
         booted = False
         try:
-            self.out.output("Starting windows VM ...", False)
+            self.out.info("Starting windows VM ...", False)
             self.vm.start()
             try:
                 self.out.success("started (console on VNC display: %d)" %
                                  self.vm.display)
 
-                self.out.output("Waiting for OS to boot ...", False)
+                self.out.info("Waiting for OS to boot ...", False)
                 if not self.vm.wait_on_serial(timeout):
                     raise FatalError("Windows VM booting timed out!")
                 self.out.success('done')
@@ -558,17 +577,17 @@ class Windows(OSBase):
                 # conditions
                 time.sleep(5)
 
-                self.out.output("Checking connectivity to the VM ...", False)
+                self.out.info("Checking connectivity to the VM ...", False)
                 self._check_connectivity()
                 # self.out.success('done')
 
-                # self.out.output("Disabling automatic logon ...", False)
+                # self.out.info("Disabling automatic logon ...", False)
                 self._disable_autologon()
                 self.out.success('done')
 
                 self._exec_sysprep_tasks()
 
-                self.out.output("Waiting for windows to shut down ...", False)
+                self.out.info("Waiting for windows to shut down ...", False)
                 (_, stderr, rc) = self.vm.wait(shutdown_timeout)
                 if rc != 0 or "terminating on signal" in stderr:
                     raise FatalError("Windows VM died unexpectedly!\n\n"
@@ -582,7 +601,7 @@ class Windows(OSBase):
         finally:
             self.image.enable_guestfs()
 
-            self.out.output("Reverting media boot preparations ...", False)
+            self.out.info("Reverting media boot preparations ...", False)
             with self.mount(readonly=False, silent=True, fatal=False):
 
                 if not self.ismounted:
@@ -621,11 +640,11 @@ class Windows(OSBase):
         cnt = 0
         for task in enabled:
             cnt += 1
-            self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
+            self.out.info(('(%d/%d)' % (cnt, size)).ljust(7), False)
             task()
             del self._sysprep_tasks[task.__name__]
 
-        self.out.output("Sending shut down command ...", False)
+        self.out.info("Sending shut down command ...", False)
         if not self.sysprepped:
             self._shutdown()
         self.out.success("done")
@@ -697,8 +716,33 @@ class Windows(OSBase):
         super(Windows, self)._do_collect_metadata()
 
         # We only care for active users
-        self.meta["USERS"] = \
-            " ".join([self.usernames[a] for a in self.active_users])
+        active = [self.usernames[a] for a in self.active_users]
+        self.meta["USERS"] = " ".join(active)
+
+        # Get RDP settings
+        settings = self.registry.get_rdp_settings()
+
+        if settings['disabled']:
+            self.out.warn("RDP is disabled on the image")
+        else:
+            if 'REMOTE_CONNECTION' not in self.meta:
+                self.meta['REMOTE_CONNECTION'] = ""
+            else:
+                self.meta['REMOTE_CONNECTION'] += " "
+
+            port = settings['port']
+            if len(active):
+                rdp = ["rdp:port=%d,user=%s" % (port, user) for user in active]
+                self.meta['REMOTE_CONNECTION'] += " ".join(rdp)
+            else:
+                self.meta['REMOTE_CONNECTION'] += "rdp:port=%d" % port
+
+        major = self.image.g.inspect_get_major_version(self.root)
+        minor = self.image.g.inspect_get_minor_version(self.root)
+
+        self.meta["KERNEL"] = "Windows NT %d.%d" % (major, minor)
+        self.meta['SORTORDER'] += (100 * major + minor) * 100
+        self.meta['GUI'] = 'Windows'
 
     def _check_connectivity(self):
         """Check if winexe works on the Windows VM"""
@@ -725,10 +769,10 @@ class Windows(OSBase):
                 log.file.write("STDERR:\n%s\n" % stderr)
             finally:
                 log.close()
-            self.out.output("failed! See: `%s' for the full output" % log.name)
+            self.out.info("failed! See: `%s' for the full output" % log.name)
             if i < retries - 1:
                 wait = timeout.pop()
-                self.out.output("retrying in %d seconds ..." % wait, False)
+                self.out.info("retrying in %d seconds ..." % wait, False)
                 time.sleep(wait)
 
         raise FatalError("Connection to the Windows VM failed after %d retries"
@@ -814,8 +858,8 @@ class Windows(OSBase):
             if len(drvs) == 0:
                 del collection[drv_type]
 
-        self.out.output('Found %d valid driver%s' %
-                        (num, "s" if num != 1 else ""))
+        self.out.info('Found %d valid driver%s' %
+                      (num, "s" if num != 1 else ""))
         return collection
 
     def install_virtio_drivers(self, upgrade=True):
@@ -827,7 +871,7 @@ class Windows(OSBase):
         if not dirname:
             raise FatalError('No directory hosting the VirtIO drivers defined')
 
-        self.out.output('Installing VirtIO drivers:')
+        self.out.info('Installing VirtIO drivers:')
 
         valid_drvs = self._fetch_virtio_drivers(dirname)
         if not len(valid_drvs):
@@ -845,8 +889,8 @@ class Windows(OSBase):
                 else:
                     self._cleanup('virtio')
 
-        self.out.output("VirtIO drivers were successfully installed")
-        self.out.output()
+        self.out.success("VirtIO drivers were successfully installed")
+        self.out.info()
 
     def _upload_virtio_drivers(self, dirname, drvs, delete_old=True):
         """Upload the VirtIO drivers and installation scripts to the media.
@@ -862,8 +906,19 @@ class Windows(OSBase):
                               self.vm.admin.rid,
                               self.registry.reset_account(self.vm.admin.rid))
 
-            if self.registry.update_uac(0):
-                self._add_cleanup('virtio', self.registry.update_uac, 1)
+            old = self.registry.update_uac(0)
+            if old != 0:
+                self._add_cleanup('virtio', self.registry.update_uac, old)
+
+            old = self.registry.update_noautoupdate(1)
+            if old != 1:
+                self._add_cleanup('virtio',
+                                  self.registry.update_noautoupdate, old)
+
+            old = self.registry.update_auoptions(1)
+            if old != 1:
+                self._add_cleanup('virtio',
+                                  self.registry.update_auoptions, old)
 
             # We disable this with powershell scripts
             self.registry.enable_autologon(self.vm.admin.name)
@@ -953,7 +1008,7 @@ class Windows(OSBase):
             timeout = self.sysprep_params['boot_timeout'].value
             shutdown_timeout = self.sysprep_params['shutdown_timeout'].value
             virtio_timeout = self.sysprep_params['virtio_timeout'].value
-            self.out.output("Starting Windows VM ...", False)
+            self.out.info("Starting Windows VM ...", False)
             booted = False
             try:
                 if old_windows:
@@ -965,16 +1020,16 @@ class Windows(OSBase):
 
                 self.out.success("started (console on VNC display: %d)" %
                                  self.vm.display)
-                self.out.output("Waiting for Windows to boot ...", False)
+                self.out.info("Waiting for Windows to boot ...", False)
                 if not self.vm.wait_on_serial(timeout):
                     raise FatalError("Windows VM booting timed out!")
                 self.out.success('done')
                 booted = True
-                self.out.output("Installing new drivers ...", False)
+                self.out.info("Installing new drivers ...", False)
                 if not self.vm.wait_on_serial(virtio_timeout):
                     raise FatalError("Windows VirtIO installation timed out!")
                 self.out.success('done')
-                self.out.output('Shutting down ...', False)
+                self.out.info('Shutting down ...', False)
                 (_, stderr, rc) = self.vm.wait(shutdown_timeout)
                 if rc != 0 or "terminating on signal" in stderr:
                     raise FatalError("Windows VM died unexpectedly!\n\n"
@@ -996,7 +1051,7 @@ class Windows(OSBase):
             # Hopefully restart in safe mode. Newer windows will not boot from
             # a viostor device unless we initially start them in safe mode
             try:
-                self.out.output('Rebooting Windows VM in safe mode ...', False)
+                self.out.info('Rebooting Windows VM in safe mode ...', False)
                 self.vm.start()
                 (_, stderr, rc) = self.vm.wait(timeout + shutdown_timeout)
                 if rc != 0 or "terminating on signal" in stderr:
diff --git a/image_creator/os_type/windows/registry.py b/image_creator/os_type/windows/registry.py
index bd379937beb7f197a95aa4f71a5d05e3d5d70e14..19d2ce54483a48cab3577d238b686d59cd9ec6d2 100644
--- a/image_creator/os_type/windows/registry.py
+++ b/image_creator/os_type/windows/registry.py
@@ -92,7 +92,7 @@ class Registry(object):
         # OpenHive class needs this since 'self' gets overwritten
         g = self.image.g
 
-        class OpenHive:
+        class OpenHive(object):
             """The OpenHive context manager"""
             def __enter__(self):
                 localfd, self.localpath = tempfile.mkstemp()
@@ -135,6 +135,42 @@ class Registry(object):
 
         return self._current_control_set
 
+    def _update_dword(self, keyname, valuename, data, default, valid):
+        """Updates a registry key dword value to data.
+
+            default: The default value is the key does not exist
+            valid: a range of valid values
+
+        Returns:
+            The old value of the field
+        """
+
+        if data not in valid:
+            raise ValueError("Valid values for parameter are %r" % (valid,))
+
+        hivename, keyname = keyname.split('/', 1)
+        with self.open_hive(hivename, write=True) as hive:
+            key = traverse(hive, keyname)
+
+            value = None
+            for v in hive.node_values(key):
+                if hive.value_key(v) == valuename:
+                    value = v
+                    break
+
+            old = default
+            if value is not None:
+                old = hive.value_dword(value)
+            elif old is None:
+                raise FatalError("Value: `%s' of key: `%s/%s' is missing." %
+                                 (valuename, hivename, keyname))
+
+            if old != data:
+                hive.node_set_value(key, REG_DWORD(valuename, data))
+                hive.commit(None)
+
+        return old
+
     def get_setup_state(self):
         """Returns the stage of Windows Setup the image is in.
         The method will return an int with one of the following values:
@@ -170,6 +206,29 @@ class Registry(object):
 
         raise FatalError("Unknown Windows Setup State: %s" % value)
 
+    def get_rdp_settings(self):
+        """Returns Remote Desktop Settings of the image"""
+
+        settings = {}
+        with self.open_hive('SYSTEM') as hive:
+            path = '%s/Control/Terminal Server' % self.current_control_set
+            term_server = traverse(hive, path)
+
+            disabled = hive.node_get_value(term_server, "fDenyTSConnections")
+            # expecting a little endian dword
+            assert hive.value_type(disabled)[1] == 4
+            settings['disabled'] = hive.value_dword(disabled) != 0
+
+            path += "/WinStations/RDP-Tcp"
+            rdp_tcp = traverse(hive, path)
+
+            port = hive.node_get_value(rdp_tcp, "PortNumber")
+            # expecting a little endian dword
+            assert hive.value_type(port)[1] == 4
+            settings['port'] = hive.value_dword(port)
+
+        return settings
+
     def runonce(self, commands):
         """Add commands to the RunOnce registry key"""
 
@@ -255,33 +314,13 @@ class Registry(object):
         For more info see here: http://support.microsoft.com/kb/951016
 
         Returns:
-            True if the key is changed
-            False if the key is unchanged
+            The old value of the field
         """
 
-        if value not in (0, 1):
-            raise ValueError("Valid values for value parameter are 0 and 1")
+        key = 'SOFTWARE/Microsoft/Windows/CurrentVersion/Policies/System'
+        valuename = 'LocalAccountTokenFilterPolicy'
 
-        with self.open_hive('SOFTWARE', write=True) as hive:
-            path = 'Microsoft/Windows/CurrentVersion/Policies/System'
-            system = traverse(hive, path)
-
-            policy = None
-            for val in hive.node_values(system):
-                if hive.value_key(val) == "LocalAccountTokenFilterPolicy":
-                    policy = val
-
-            if policy is not None:
-                if value == hive.value_dword(policy):
-                    return False
-            elif value == 0:
-                return False
-
-            hive.node_set_value(
-                system, REG_DWORD("LocalAccountTokenFilterPolicy", value))
-            hive.commit(None)
-
-        return True
+        return self._update_dword(key, valuename, value, 0, (0, 1))
 
     def update_uac(self, value):
         """Enable or disable the User Account Control by changing the value of
@@ -291,32 +330,49 @@ class Registry(object):
         value = 0 will disable the UAC
 
         Returns:
-            True if the key is changed
-            False if the key is unchanged
+            The old value of the field
         """
 
-        if value not in (0, 1):
-            raise ValueError("Valid values for value parameter are 0 and 1")
+        key = 'SOFTWARE/Microsoft/Windows/CurrentVersion/Policies/System'
+        valuename = 'EnableLUA'
 
-        with self.open_hive('SOFTWARE', write=True) as hive:
-            path = 'Microsoft/Windows/CurrentVersion/Policies/System'
-            system = traverse(hive, path)
+        return self._update_dword(key, valuename, value, 1, (0, 1))
 
-            enablelua = None
-            for val in hive.node_values(system):
-                if hive.value_key(val) == 'EnableLUA':
-                    enablelua = val
+    def update_noautoupdate(self, value):
+        """Enable or disable Automatic Updates by changing the NoAutoUpdate
+        value of the "Auto Update" registry key.
 
-            if enablelua is not None:
-                if value == hive.value_dword(enablelua):
-                    return False
-            elif value == 1:
-                return False
+        value = 0 will enable Automatic Updates (Default)
+        value = 1 will disable Automatic Updates
 
-            hive.node_set_value(system, REG_DWORD('EnableLUA', value))
-            hive.commit(None)
+        Returns:
+            The old value of the field
+        """
+
+        key = 'SOFTWARE' \
+            '/Microsoft/Windows/CurrentVersion/WindowsUpdate/Auto Update'
+        valuename = 'NoAutoUpdate'
+
+        return self._update_dword(key, valuename, value, 0, (0, 1))
+
+    def update_auoptions(self, value):
+        """Updates the AUOptions value of the "Auto Update" registry key.
+
+        value = 1 will disabled Automatic Updates
+        value = 2 will notify of download and installation
+        value = 3 will automatically download and notify of installation
+        value = 4 will automatically download and schedule installation
+
+        Returns:
+            The old value of the field
+        """
+
+        key = 'SOFTWARE' \
+            '/Microsoft/Windows/CurrentVersion/WindowsUpdate/Auto Update'
+        valuename = 'AUOptions'
 
-        return True
+        return self._update_dword(key, valuename, value, None,
+                                  tuple(range(1, 5)))
 
     def enum_users(self):
         """Returns:
@@ -345,7 +401,7 @@ class Registry(object):
 
             # Check http://pogostick.net/~pnh/ntpasswd/ for more info
             offset = struct.unpack('<I', c_val[0x28:0x2c])[0] + 0x34
-            #size = struct.unpack('<I', c_val[0x2c:0x30])[0]
+            # size = struct.unpack('<I', c_val[0x2c:0x30])[0]
             count = struct.unpack('<I', c_val[0x30:0x34])[0]
 
             # Parse the sid array and get all members
@@ -415,6 +471,7 @@ class Registry(object):
         return parent['old']
 
     def reset_account(self, rid, activate=True):
+        """Reset the password in a user account"""
 
         # This is a hack. I cannot assign a new value to nonlocal variable.
         # This is why I'm using a dict
diff --git a/image_creator/os_type/windows/vm.py b/image_creator/os_type/windows/vm.py
index 6122b8c28fe2aefc883a3ed681d846d887fec107..fc884ec1ab7f6efa1e603d724530c57fb7ee3650 100644
--- a/image_creator/os_type/windows/vm.py
+++ b/image_creator/os_type/windows/vm.py
@@ -150,6 +150,7 @@ class VM(object):
                 return
 
             def handler(signum, frame):
+                """Signal handler"""
                 self.process.terminate()
                 time.sleep(1)
                 if self.isalive():
@@ -197,6 +198,7 @@ class VM(object):
         """Wait for the VM to shutdown by itself"""
 
         def handler(signum, frame):
+            """Signal handler"""
             raise FatalError("VM wait timed-out.")
 
         signal.signal(signal.SIGALRM, handler)
@@ -247,8 +249,6 @@ class VM(object):
                 fname = log.name
                 log.close()
 
-            # self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
-            #                 (command, rc, reason))
             raise FatalError("Command: `%s' failed (rc=%d). See: %s" %
                              (command, rc, fname))
 
diff --git a/image_creator/os_type/windows/winexe.py b/image_creator/os_type/windows/winexe.py
index 37df80df087e5a7b99e0122434ab24ebe9e6cc64..4ffafbf9992dac8bf27f47ebbaf1d3eadcdf5f04 100644
--- a/image_creator/os_type/windows/winexe.py
+++ b/image_creator/os_type/windows/winexe.py
@@ -30,14 +30,16 @@ class WinexeTimeout(FatalError):
     pass
 
 
-class WinEXE:
+class WinEXE(object):
     """Wrapper class for the winexe command"""
 
     @staticmethod
     def is_installed(program='winexe'):
+        """Check if WinEXE is installed on the system"""
         return which(program) is not None
 
     def __init__(self, username, hostname, **kwargs):
+        """Initialize a WinEXE instance"""
         self._user = username
         self._host = hostname
 
@@ -100,6 +102,7 @@ class WinEXE:
                                stderr=subprocess.PIPE)
 
         def handler(signum, frame):
+            """Signal handler"""
             run.terminate()
             time.sleep(1)
             run.poll()
diff --git a/image_creator/output/__init__.py b/image_creator/output/__init__.py
index 4e86a8032c0cbfd1d087249a5523300054b668e6..4ace94406b0716cb71d64aca43837093a92d25ff 100644
--- a/image_creator/output/__init__.py
+++ b/image_creator/output/__init__.py
@@ -36,10 +36,14 @@ class Output(object):
         """Print msg after an action is completed"""
         pass
 
-    def output(self, msg='', new_line=True):
+    def info(self, msg='', new_line=True):
         """Print normal program output"""
         pass
 
+    def result(self, msg='', new_line=True):
+        """Print a result"""
+        pass
+
     def cleanup(self):
         """Cleanup this output class"""
         pass
@@ -51,7 +55,7 @@ class Output(object):
     def _get_progress(self):
         """Returns a new Progress object"""
         progress = self._Progress
-        progress.output = self
+        progress.parent = self
         return progress
 
     Progress = property(_get_progress)
@@ -61,7 +65,7 @@ class Output(object):
         def __init__(self, size, title, bar_type='default'):
             self.size = size
             self.bar_type = bar_type
-            self.output.output("%s ..." % title, False)
+            self.parent.info("%s ..." % title, False)
 
         def goto(self, dest):
             """Move progress to a specific position"""
@@ -73,7 +77,7 @@ class Output(object):
 
         def success(self, result):
             """Print a msg after an action is completed successfully"""
-            self.output.success(result)
+            self.parent.success(result)
 
     def progress_generator(self, message):
         """A python generator for the progress bar class"""
diff --git a/image_creator/output/cli.py b/image_creator/output/cli.py
index d938e945edb5849f3f72ca99975dc3313ec585da..a1bd78f8c9f827fa9d76a188c0be609cfe28d913 100644
--- a/image_creator/output/cli.py
+++ b/image_creator/output/cli.py
@@ -24,64 +24,54 @@ from colors import red, green, yellow
 from progress.bar import Bar
 
 
-def output(msg, new_line, decorate, stream):
+def write(msg, new_line, decorate, stream):
+    """Print a message"""
     nl = "\n" if new_line else ' '
     stream.write(decorate(msg) + nl)
 
 
-def error(msg, new_line, colored, stream):
-    color = red if colored else lambda x: x
-    output("Error: %s" % msg, new_line, color, stream)
-
-
-def warn(msg, new_line, colored, stream):
-    color = yellow if colored else lambda x: x
-    output("Warning: %s" % msg, new_line, color, stream)
-
-
-def success(msg, new_line, colored, stream):
-    color = green if colored else lambda x: x
-    output(msg, new_line, color, stream)
-
+class SilentOutput(Output):
+    """Silent Output class. Only Errors are printed"""
 
-def clear(stream):
-    """Clears the terminal screen."""
-    if stream.isatty():
-        stream.write('\033[H\033[2J')
+    def __init__(self, **kwargs):
+        """Initialize a SilentOutput instance"""
+        self.colored = kwargs['colored'] if 'colored' in kwargs else True
+        self.stdout = kwargs['stdout'] if 'stdout' in kwargs else sys.stdout
+        self.stderr = kwargs['stderr'] if 'stderr' in kwargs else sys.stderr
 
+    def result(self, msg, new_line=True):
+        """Print a result"""
+        write(msg, new_line, lambda x: x, self.stdout)
 
-class SilentOutput(Output):
-    """Silent Output class. Only Errors are printed"""
-    pass
+    def error(self, msg, new_line=True):
+        """Print an error"""
+        color = red if self.colored else lambda x: x
+        write("Error: %s" % msg, new_line, color, self.stderr)
 
 
-class SimpleOutput(Output):
+class SimpleOutput(SilentOutput):
     """Print messages but not progress bars. Progress bars are treated as
     output messages. The user gets informed when the action begins and when it
     ends, but no progress is shown in between."""
-    def __init__(self, colored=True, stream=None):
-        self.colored = colored
-        self.stream = sys.stderr if stream is None else stream
-
-    def error(self, msg, new_line=True):
-        """Print an error"""
-        error(msg, new_line, self.colored, self.stream)
 
     def warn(self, msg, new_line=True):
         """Print a warning"""
-        warn(msg, new_line, self.colored, self.stream)
+        color = yellow if self.colored else lambda x: x
+        write("Warning: %s" % msg, new_line, color, self.stderr)
 
     def success(self, msg, new_line=True):
         """Print msg after an action is completed"""
-        success(msg, new_line, self.colored, self.stream)
+        color = green if self.colored else lambda x: x
+        write(msg, new_line, color, self.stderr)
 
-    def output(self, msg='', new_line=True):
+    def info(self, msg='', new_line=True):
         """Print msg as normal program output"""
-        output(msg, new_line, lambda x: x, self.stream)
+        write(msg, new_line, lambda x: x, self.stderr)
 
     def clear(self):
         """Clear the screen"""
-        clear(self.stream)
+        if self.stderr.isatty():
+            self.stderr.write('\033[H\033[2J')
 
 
 class OutputWthProgress(SimpleOutput):
@@ -114,7 +104,7 @@ class OutputWthProgress(SimpleOutput):
 
         def success(self, result):
             """Print result after progress has finished"""
-            self.output.output("\r%s ...\033[K" % self.title, False)
-            self.output.success(result)
+            self.parent.info("\r%s ...\033[K" % self.title, False)
+            self.parent.success(result)
 
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/output/composite.py b/image_creator/output/composite.py
index cb7820651e61c9c9b57a5fde375c9809fe469575..fe1185ced920f03f0a32e58d22081e65c831f92e 100644
--- a/image_creator/output/composite.py
+++ b/image_creator/output/composite.py
@@ -20,7 +20,7 @@
 from image_creator.output import Output
 
 
-class CompositeOutput(Output):
+class CompositeOutput(Output, list):
     """This class can be used to composite different outputs into a single one
 
     You may create an instance of this class and then add other output
@@ -29,70 +29,69 @@ class CompositeOutput(Output):
     this one.
     """
 
-    def __init__(self, outputs=[]):
+    def __init__(self, outputs=None):
         """Add initial output instances"""
-        self._outputs = outputs
+        super(CompositeOutput, self).__init__()
 
-    def add(self, output):
-        """Add another output instance"""
-        self._outputs.append(output)
-
-    def remove(self, output):
-        """Remove an output instance"""
-        self._outputs.remove(output)
+        if outputs is not None:
+            self.extend(outputs)
 
     def error(self, msg, new_line=True):
         """Call the error method of each of the output instances"""
-        for out in self._outputs:
+        for out in self:
             out.error(msg, new_line)
 
     def warn(self, msg, new_line=True):
         """Call the warn method of each of the output instances"""
-        for out in self._outputs:
+        for out in self:
             out.warn(msg, new_line)
 
     def success(self, msg, new_line=True):
         """Call the success method of each of the output instances"""
-        for out in self._outputs:
+        for out in self:
             out.success(msg, new_line)
 
-    def output(self, msg='', new_line=True):
+    def info(self, msg='', new_line=True):
+        """Call the output method of each of the output instances"""
+        for out in self:
+            out.info(msg, new_line)
+
+    def result(self, msg='', new_line=True):
         """Call the output method of each of the output instances"""
-        for out in self._outputs:
-            out.output(msg, new_line)
+        for out in self:
+            out.result(msg, new_line)
 
     def cleanup(self):
         """Call the cleanup method of each of the output instances"""
-        for out in self._outputs:
+        for out in self:
             out.cleanup()
 
     def clear(self):
         """Call the clear method of each of the output instances"""
-        for out in self._outputs:
+        for out in self:
             out.clear()
 
-    class _Progress(object):
+    class _Progress(list):
         """Class used to composite different Progress objects"""
 
         def __init__(self, size, title, bar_type='default'):
             """Create a progress on each of the added output instances"""
-            self._progresses = []
-            for out in self.output._outputs:
-                self._progresses.append(out.Progress(size, title, bar_type))
+            for out in self.parent:
+                self.append(out.Progress(size, title, bar_type))
 
         def goto(self, dest):
             """Call the goto method of each of the progress instances"""
-            for progress in self._progresses:
+            for progress in self:
                 progress.goto(dest)
 
         def next(self):
             """Call the next method of each of the progress instances"""
-            for progress in self._progresses:
+            for progress in self:
                 progress.next()
 
         def success(self, result):
             """Call the success method of each of the progress instances"""
-            for progress in self._progresses:
+            for progress in self:
                 progress.success(result)
 
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/image_creator/output/dialog.py b/image_creator/output/dialog.py
index be0297ba001bce22ee5c92dce8be94149e4f92f5..c7fbc647d3297c11a4ade1f12e32649c8c8f7417 100644
--- a/image_creator/output/dialog.py
+++ b/image_creator/output/dialog.py
@@ -41,13 +41,17 @@ class GaugeOutput(Output):
         flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
         fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
 
-    def output(self, msg='', new_line=True):
+    def info(self, msg='', new_line=True):
         """Print msg as normal output"""
         self.msg = msg
         self.percent = 0
         self.d.gauge_update(self.percent, self.msg, update_text=True)
         time.sleep(0.4)
 
+    def result(self, msg='', new_line=True):
+        """Print a result"""
+        self.info(msg, new_line)
+
     def success(self, result, new_line=True):
         """Print result after a successful action"""
         self.percent = 100
@@ -61,6 +65,12 @@ class GaugeOutput(Output):
                             update_text=True)
         time.sleep(0.4)
 
+    def error(self, msg, new_line=True):
+        """Print an error"""
+        self.d.gauge_update(self.percent, "%s Error: %s" % (self.msg, msg),
+                            update_text=True)
+        time.sleep(0.4)
+
     def cleanup(self):
         """Cleanup the GaugeOutput instance"""
         self.d.gauge_stop()
@@ -76,25 +86,25 @@ class GaugeOutput(Output):
         }
 
         def __init__(self, size, title, bar_type='default'):
-            self.output.size = size
+            """Initialize a _Progress instance"""
+            self.parent.size = size
             self.bar_type = bar_type
-            self.output.msg = "%s ..." % title
+            self.parent.msg = "%s ..." % title
             self.goto(0)
 
-        def _postfix(self):
-            return self.template[self.bar_type] % self.output.__dict__
-
         def goto(self, dest):
             """Move progress bar to a specific position"""
-            self.output.index = dest
-            self.output.percent = self.output.index * 100 // self.output.size
-            msg = "%s %s" % (self.output.msg, self._postfix())
-            self.output.d.gauge_update(self.output.percent, msg,
+            self.parent.index = dest
+            self.parent.percent = self.parent.index * 100 // self.parent.size
+
+            postfix = self.template[self.bar_type] % self.parent.__dict__
+            msg = "%s %s" % (self.parent.msg, postfix)
+            self.parent.d.gauge_update(self.parent.percent, msg,
                                        update_text=True)
 
         def next(self):
             """Move progress bar one step forward"""
-            self.goto(self.output.index + 1)
+            self.goto(self.parent.index + 1)
 
 
 class InfoBoxOutput(Output):
@@ -107,7 +117,7 @@ class InfoBoxOutput(Output):
         self.height = height
         self.d.infobox(self.msg, title=self.title)
 
-    def output(self, msg='', new_line=True):
+    def info(self, msg='', new_line=True):
         """Print msg as normal output"""
         nl = '\n' if new_line else ''
         self.msg += "%s%s" % (msg, nl)
@@ -120,13 +130,21 @@ class InfoBoxOutput(Output):
         self.d.infobox(display, title=self.title, height=self.height,
                        width=self.width)
 
+    def result(self, msg='', new_line=True):
+        """Print a result"""
+        self.info(msg, new_line)
+
     def success(self, result, new_line=True):
         """Print result after an action is completed successfully"""
-        self.output(result, new_line)
+        self.info(result, new_line)
 
     def warn(self, msg, new_line=True):
         """Print a warning message"""
-        self.output("Warning: %s" % msg, new_line)
+        self.info("Warning: %s" % msg, new_line)
+
+    def error(self, msg, new_line=True):
+        """Print an error message"""
+        self.info("Error: %s" % msg, new_line)
 
     def finalize(self):
         """Finalize the output. After this is called, the InfoboxOutput
diff --git a/image_creator/rsync.py b/image_creator/rsync.py
index c37055e3c6b409f55ee1365426058bd170be3038..651593155d38680fdf3a390059fcb74a26337a44 100644
--- a/image_creator/rsync.py
+++ b/image_creator/rsync.py
@@ -24,7 +24,7 @@ import signal
 from image_creator.util import FatalError
 
 
-class Rsync:
+class Rsync(object):
     """Wrapper class for the rsync command"""
 
     def __init__(self, output):
@@ -76,8 +76,8 @@ class Rsync:
         for i in self._exclude:
             cmd.extend(['--exclude', i])
 
-        self._out.output("Calculating total number of %s files ..." % slabel,
-                         False)
+        self._out.info("Calculating total number of %s files ..." % slabel,
+                       False)
 
         # If you don't specify a destination, rsync will list the source files.
         dry_run = subprocess.Popen(cmd + [src], shell=False,
@@ -110,6 +110,7 @@ class Rsync:
 
         finally:
             def handler(signum, frame):
+                """Signal handler"""
                 run.terminate()
                 time.sleep(1)
                 run.poll()
diff --git a/image_creator/util.py b/image_creator/util.py
index b57f026bd13751bf0a27e6a842b7b9aa0e524be1..3229994abe4b11629bc40009a17860fcafbed110 100644
--- a/image_creator/util.py
+++ b/image_creator/util.py
@@ -35,8 +35,9 @@ class FatalError(Exception):
 def get_command(command):
     """Return a file system binary command"""
     def find_sbin_command(command, exception):
+        """Checks if a command is hosted under one of the sbin directories"""
         search_paths = ['/usr/local/sbin', '/usr/sbin', '/sbin']
-        for fullpath in map(lambda x: "%s/%s" % (x, command), search_paths):
+        for fullpath in ["%s/%s" % (x, command) for x in search_paths]:
             if os.path.exists(fullpath) and os.access(fullpath, os.X_OK):
                 return sh.Command(fullpath)
         raise exception
@@ -139,7 +140,7 @@ class QemuNBD(object):
         """Initialize an instance"""
         self.image = image
         self.device = None
-        self.pattern = re.compile('^nbd\d+$')
+        self.pattern = re.compile(r'^nbd\d+$')
         self.modprobe = get_command('modprobe')
         try:
             self.qemu_nbd = get_command('qemu-nbd')
diff --git a/image_creator/version.py b/image_creator/version.py
index 96ec7ff099569dd36a557783007d3b6b180df418..948005c9c5d979bb79c041b2167aa6edd887a5fc 100644
--- a/image_creator/version.py
+++ b/image_creator/version.py
@@ -1,8 +1,8 @@
 
-__version__ = "0.7.4"
+__version__ = "0.8_653_c89703e"
 __version_vcs_info__ = {
-    'branch': 'hotfix-0.7.4',
-    'revid': '430b262',
-    'revno': 593}
+    'branch': 'master',
+    'revid': 'c89703e',
+    'revno': 653}
 __version_user_email__ = "skalkoto@grnet.gr"
 __version_user_name__ = "Nikos Skalkotos"
diff --git a/setup.py b/setup.py
index d2de6fcdc3e84058b74697c5bb8db5539ac10b05..9bba333e3a952e319e115aaab2bf381da18f922d 100755
--- a/setup.py
+++ b/setup.py
@@ -25,17 +25,33 @@ setup(
     version=image_creator.__version__,
     description='Command line tool for creating images',
     long_description=open('README.md').read(),
-    url='https://code.grnet.gr/projects/snf-image-creator',
-    author="Synnefo development team",
-    author_email="synnefo-devel@googlegroups.com",
-    license='BSD',
+    url='https://github.com/grnet/snf-image',
+    download_url='https://pypi.python.org/pypi/snf_image_creator',
+    author='Synnefo development team',
+    author_email='synnefo-devel@googlegroups.com',
+    maintainer='Synnefo development team',
+    maintainer_email='synnefo-devel@googlegroups.com',
+    license='GNU GPLv3',
     packages=find_packages(),
     include_package_data=True,
-    install_requires=['sh', 'ansicolors', 'progress>=1.0.2'],
+    install_requires=['sh', 'ansicolors', 'progress>=1.0.2', 'kamaki>=0.9'],
+    # Unresolvable dependencies:
+    #   pysendfile|py-sendfile, hivex, guestfs, parted, rsync,
     entry_points={
         'console_scripts': [
                 'snf-mkimage = image_creator.main:main',
                 'snf-image-creator = image_creator.dialog_main:main']
-    }
+    },
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Console',
+        'Environment :: Console :: Curses',
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
+        'Operating System :: POSIX :: Linux',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7'],
+    keywords = 'cloud IaaS OS images'
 )
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
diff --git a/version b/version
index 0a1ffad4b4df766be3d8a23383cfb3d1defb19f7..aec258df73d39d2122706793921981f4a0f672f8 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.7.4
+0.8