...making Linux just a little more fun!

Development Builds Layered on Top of a Stable System by Means of Unionfs

By Dirk Wallenstein

Intro

Unionfs is a filesystem type by which you can merge multiple directories into one mountpoint. The source directories (branches) are stacked onto each other with filesystem entries in the top layer hiding/overriding entries in the layers below. In this article I'd like to show you how to set up a layered /usr directory, which allows you to push and pop a development directory onto/from your distribution's /usr directory. That way you don't ever modify your stable system, and can still test your work in a real context. Further I'll show you how to put boot options into place, to choose between the stable system and the development system at bootup. That way you can replace core system applications that are started by init.

Disclaimer

The steps shown here will deeply interfere with your system's filesystem hierarchy and boot process. You might render your system unusable or even lose all your data. I cannot be held responsible for that. Reader beware; back up your system to a safe location.

Getting unionfs support

Alas, no filesystem unification implementation has made it into the vanilla kernels, and so you have to patch and compile a custom kernel to get the support. If you haven't done that before, there is a distribution specific kernel compilation series on howtoforge.com. They run under the title "How To Compile A Kernel - The XXX Way" where XXX is Fedora, Ubuntu, etc.

You can get the patches for unionfs and further documentation at the unionfs project. Once you have applied the patch, there is very focused documentation below /linux/Documentation/filesystems/unionfs/. I think concepts.txt and usage.txt in particular give you all the information you need to understand and operate a unionfs, and they aren't too long. You'll need to activate unionfs support by checking "File systems -> Layered filesystems -> Union file system" (UNION_FS) and optionally "Unionfs extended attribute" (UNION_FS_XATTR) next to it, in the kernel configuration. Now, compile and install the new kernel.

A New Filesystem Structure

Unionfs hides everything below the mountpoint (like every other filesystem type), so we have to move the stable branch (the /usr dir of the distribution) to another directory, and hide away this fact, by using a bind mount back onto /usr. As you can guess, this will rumble a bit (i.e., you might miss some commands on the way), and is thus best applied from a live distribution of your choice. In this example I'll use the following branches:

        /usr                    # the mountpoint for your unionfs
        /usr_stable             # your distro's stable branch (formerly known as /usr)
        /mnt/devel/usr          # your development branch

So, let's assume you are running a live distro and your normal root filesystem is mounted on /mnt/hda1.

1. Rename the stable branch:

            $ mv /mnt/hda1/usr /mnt/hda1/usr_stable

2. Create the mountpoint. You can create fallback links within the mountpoint in case either of the unionfs or the bind mount fails, and a sentinel file which you can check for existence in scripts:

            $ mkdir /mnt/hda1/usr
            $ cd /mnt/hda1/usr
            $ ln -sn -t . ../usr_stable/*
            $ touch YOU_SHOULD_NEVER_SEE_THIS_FILE

3. Then create a bind mount for the stable system.

            $ echo "/usr_stable  /usr none  bind" >> /mnt/hda1/etc/fstab

Now you should be able to boot into your stable system, and everything should be like before.

Boot Script:

The next step is to create a script that checks for a chosen identification string on the kernel command line, and if given, releases the bind mount again and puts the union mount into its place. A call to this script must be inserted into the boot process right after the corresponding init scripts have processed /etc/fstab and mounted all the given filesystems. Where and how to insert this script varies among the different distributions; if you don't know, you'll have to consult the documentation of your distribution for this. To find the corresponding scripts, you can grep for 'mount' or 'fstab' in the /etc/init.d/ scripts.

            $ grep -Hw -e mount -e fstab /etc/init.d/*
Good candidates are localmount, bootmisc or similar. Look for the '-a' flag to mount. In case of Gentoo the right place to insert the call is here (in this example the script is called unionmount-usr):
    --- /etc/init.d/localmount.orgy 2009-02-15 10:26:22.000000000 +0100
    +++ /etc/init.d/localmount      2009-02-15 10:33:02.000000000 +0100
    @@ -23,6 +23,9 @@ start()
            mount -at "${types}"
            eend $? "Some local filesystem failed to mount"

    +       # conditionally mount development branch through unionfs
    +       /etc/init.d/unionmount-usr
    +
            # Always return 0 - some local mounts may not be critical for boot
            return 0
     }

Note that for the approach given here to work (unmounting the default bind mount), you can't have another bind mount on /usr_stable (or indirectly on /usr) unless you unmount and remount them too.

Now, the script - this is just a very basic version to illustrate the necessary steps. You might, for example, remount the bind mount if the union mount fails. In general, I like to have consistency checker script at the end of the boot process, where you can check for different vital things that you don't want to miss. Besides checking iptables rules and such, you could also check the sentinel file for existence there.

    #!/bin/bash
    # check the kernel command line (/proc/cmdline) for the id-String and if given release the bind mount on /usr.
    # and put the union mount in its place.
    UNION_WANTED_FLAG="UniteWithDevel"      # this is what would be given on the kernel cmdline
    MOUNTPOINT="/usr"
    STABLE_BRANCH="/usr_stable"
    DEVEL_BRANCH="/mnt/devel/usr"
    ERROR_LOG_FILE="/var/log/develUnionFailed.log"

    errorOut() {
        MSG="ERROR ${0} $(date) : ${@}"
        echo "${MSG}" >&2
        echo "${MSG}" >> "${ERROR_LOG_FILE}" 
        exit 88
    }

    wantUnionMount() {
        grep "${UNION_WANTED_FLAG}" /proc/cmdline &>/dev/null
    }

    wantUnionMount && {
        echo "trying to unite..."
        umount "${MOUNTPOINT}" || errorOut "umounting failed"
        mount -t unionfs -o "dirs=${DEVEL_BRANCH}=rw:${STABLE_BRANCH}=rw" none ${MOUNTPOINT} || errorOut "mounting unionfs failed"
    }

Final Step

Now the final step is to create a boot menu entry which contains the ${UNION_WANTED_FLAG} from above. You can put any string on the kernel command line (well, maybe any byte value, I guess all ascii-chars, but at least all ascii-alphanum chars) and if the kernel doesn't know it, it seems to be silently ignored but still appears in '/proc/cmdline' (and in `dmesg | grep 'Kernel command line') So, in the case of the example above, just create a boot menu entry with "UniteWithDevel" on the kernel command line and you can boot right into your development work, giving you the possibility to replace core system applications and daemons that get started by the init process (e.g.: ntp, iptables, sysklogd, etc). E.g:

        kernel /boot/unionkern-2.6.27.10/unionkern-2.6.27.10 root=/dev/hdc9 vga=0x31B UniteWithDevel 

Some Unionfs notes:

Union Mount

There's also an alternative implementation called union mount. For that, see "Unifying filesystems with union mounts". As far as I know, the corresponding patches are not yet considered stable at this time. Unionfs seems like it will never be supported, but the union mount patches will be. See "Unionfs and related patches pre-merge review". Nevertheless, unionfs is widely used (see here) and I didn't have any problems with it (using xserver and xlib). So until the union mount patches go mainline, unionfs seems to be a good opportunity. Obviously union mount would make fs-entries below the mountpoint accessible which would eventually obviate some of the steps above, making a test run much easier (e.g., you wouldn't have to move /usr.)

I have used the term union mount at different places in this text and scripts, because it seems to describe what happens. That does not relate to the implementation that goes by that name. Everything except this paragraph is based on unionfs and not "union mount".

Populate

If you have a binary (i.e., pre-compiled) distribution, there are certainly a lot of points to consider. Depending on how much "core" your application wants, you have to build the application with the right flags in case other apps make use of those features (--enable-this, --enable-that and friends, in case it is based on autotools.) And of course, you have to use the same paths that the installed package uses, to hide all the files in the stable branch. Actually you would probably only need to hide entry points for other applications, like binaries, headers, libs and such. If there are single differences, you can try to hide them by using symlinks at the topmost layer.

So, with binary distributions, it is not that easy - but it is very easy with Gentoo, as Gentoo is about building apps from source in the first place. For example: to make little modifications to the xserver and try it out, it would only require something similar to this:

        $ ebuild /usr/portage/x11-base/xorg-server/xorg-server-1.5.3-r2.ebuild compile
            #... now modify the sources like so:
            $ cd /var/tmp/portage/x11-base/xorg-server-1.5.3-r2/work/xorg-server-1.5.3
            $ sed -i "s/\"(II)\"/\"(I am Bob and I almost completely rewrote the xserver)\"/g" os/log.c
            #... and restart the build process
            $ make
        $ ebuild /usr/portage/x11-base/xorg-server/xorg-server-1.5.3-r2.ebuild install
        $ cp -a /var/tmp/portage/x11-base/xorg-server-1.5.3-r2/image/usr/* /mnt/devel/usr/

The result would be exactly the same xserver like the one that is in the stable branch (if it was of version 1.5.3-r2) but it would show condign respect to your accomplishments in the servers logfile.

Another more useful example: As a non-native English speaker, I sometimes miss words from English movies. I like using "kaffeine" to watch movies, but the smallest step to skip backward is 20 seconds, which is far too long in response to "Come again?" So, with the steps from the previous example adapted to kaffeine and a bit of source-code browsing resulted in the following patch, which allows rewinding by 5 secs:

    --- kaffeine/src/player-parts/xine-part/xine_part.cpp.orgy      2008-11-10 19:24:18.000000000 +0100
    +++ kaffeine/src/player-parts/xine-part/xine_part.cpp   2008-11-10 19:24:23.000000000 +0100
    @@ -511,7 +511,7 @@ void XinePart::slotPosPlusSmall()

     void XinePart::slotPosMinusSmall()
     {
    -       slotJumpIncrement( -20 );
    +       slotJumpIncrement( -5 );
     }

     void XinePart::slotPosPlusMedium()

The point is that you can easily modify sources and test the results without ever messing up your stable branch.

Your fully functional stable system is just one reboot away.

As a side note: You can, of course, also use this mechanism to try out different versions of an application. Just put a read-write mounted blank branch on top of your read-only mounted stable-branch, and use your distribution's package management system to install another, possibly unstable, version of that application. If, in the course of that, you want to manage different top level branches, you might also consider using "/mnt/devel/usr" as a symlink, and let the unionmount-usr script above resolve that symlink to the branch you really want, by adding a line like:

    DEVEL_BRANCH=$(readlink -f "${DEVEL_BRANCH}") 

That way you can choose which branch is mounted at bootup by simply redirecting the symlink.

Sources

Now, you've probably guessed that I'm quite satisfied with Gentoo - once it's combined with a unification filesystem. Once you have set up and configured a Gentoo system, and know how to use portage (Gentoo's package manager), updating applications is a no-brainer most of the time. (Big kudos to all the Gentoo developers who make this possible.)

If you are interested, you can also get an initial binary distribution from Gentoo and then still use the source-based package manager afterwards on top of the binary packages. See Gentoo Linux and wikipedia:Gentoo.

Then there is Sabayon which is based on Gentoo and also includes a binary package manager but only supports x86 and x86-64 architectures. See Sabayon Linux and wikipedia:sabayon.

Can it get any better?

Initially I wanted to share my musings about whether it would be possible to bring the demonstrated "Populate" approach to binary distribution as a distro-independent application. When I wrote the last paragraph I took the opportunity to finally have a more thorough look at Sabayon Linux. The impression I got is that you can have a more-or-less semi-annually updated binary base system while still having the opportunity to use all the versatility that Gentoo offers (trying the latest features of bleeding edge apps, always have the newest versions of exposed and endangered apps, and of course easily modifying selected apps). So, if you don't want to go "all source" you can still try Sabayon Linux as a compromise.

If you have any interest in browsing, modifying, and testing open source programs, I strongly recommend giving Gentoo or Sabayon a try, getting accustomed to the Gentoo build system, and optionally trying a unified /usr-directory.

Again, I am very pleased to have the opportunity to develop and test a program while having a stable system just a reboot away. This makes development very pleasant.

And all the difficulties about dependencies and different build systems are covered by portage. This makes tweaking selected apps very convenient.


Talkback: Discuss this article with The Answer Gang


[BIO]

I`m a Linux user since 2003 and and after I did my first Linux steps with SuSE, I very soon chose source-based distributions (LFS and later Gentoo) as they offer a convenient way to see and alter any bit of the system that`s actually running.


Copyright © 2009, Dirk Wallenstein. Released under the Open Publication License unless otherwise noted in the body of the article. Linux Gazette is not produced, sponsored, or endorsed by its prior host, SSC, Inc.

Published in Issue 160 of Linux Gazette, March 2009

Tux