Gracefully Handling conffiles

When upgrading a package, dpkg will not remove a conffile (a configuration file for which dpkg should preserve user changes) if it is not present in the newer version. There are two principal reasons for this; the first is that the conffile could've been dropped by accident and the next version could restore it, users wouldn't want their changes thrown away. The second is to allow packages to transition files from a dpkg-maintained conffile to a file maintained by the package's maintainer scripts, usually with a tool like debconf or ucf.

This means if you ever rename or remove a conffile, as a package maintainer you must take certain steps yourself to behave correctly. This wiki page attempts to document recipies for you to use.

Removing a conffile

If you completely remove a configuration file, you should make sure it's also removed from the disk. However if the user has modified it, then you may wish to preserve the user's modifications somehow in case they wish to refer to them.

This can be done your preinst script when given the install or upgrade argument with a package version known to have the conffile that has been removed.

# Remove a no-longer used conffile
rm_conffile() {
    CONFFILE="$1"

    if [ -e "$CONFFILE" ]; then
        md5sum="`md5sum \"$CONFFILE\" | sed -e \"s/ .*//\"`"
        old_md5sum="`sed -n -e \"/^Conffiles:/,/^[^ ]/{\\\\' $CONFFILE'{s/.* //;p}}\" /var/lib/dpkg/status`"
        if [ "$md5sum" != "$old_md5sum" ]; then
            echo "Obsolete conffile $CONFFILE has been modified by you."
            echo "Saving as $CONFFILE.dpkg-bak ..."
            mv -f "$CONFFILE" "$CONFFILE".dpkg-bak
        else
            echo "Removing obsolete conffile $CONFFILE ..."
            rm -f "$CONFFILE"
        fi
    fi
}

case "$1" in
install|upgrade)
    if dpkg --compare-versions "$2" le "$LASTVERSION"; then
        rm_conffile "/etc/pkg/conf.1"
        rm_conffile "/etc/pkg/conf.2"
    fi
esac

The function first checks to make sure the conffile actually exists on the disk, if the user has removed it already there's no point carrying on as everything's fine. We then check to see whether the user has modified it from the package's version or not. We obtain the current md5sum using the tool and compare that to one taken from the dpkg status file, this is available and contains the old information during preinst so this is why we do this job here.

If the user has not modified the conffile, we simply remove it from the disk. If they have, we move it to a filename that can be easily identified by the user and ignored by policy-compliant tools such as run-parts.

In the main body of the preinst script, we check the arguments of the script and make sure that the version we're upgrading is a version that contained the conffile we need to remove. The reason we check for install as well is that we need to cope with being reinstalled when the last version was removed and not purged.

Moving a conffile

If a conffile is moved from one location to another, you need to make sure you move across any changes the user has made. This may seem a simple change to the removal script at first, however that will result in the user being prompted by dpkg to approve the conffile edits they have made. If you change the conffile format, that may be desirable, however often you can do that kind of checking yourself.

Here's a recipie for moving a conffile without triggering the dpkg question.

First we check in the preinst script whether the user has removed the conffile or not. This code looks rather similar to the code for removal, but with a few differences.

# Prepare to move a conffile without triggering a dpkg question
prep_mv_conffile() {
    CONFFILE="$1"

    if [ -e "$CONFFILE" ]; then
        md5sum="`md5sum \"$CONFFILE\" | sed -e \"s/ .*//\"`"
        old_md5sum="`sed -n -e \"/^Conffiles:/,/^[^ ]/{\\\\' $CONFFILE'{s/.* //;p}}\" /var/lib/dpkg/status`"
        if [ "$md5sum" = "$old_md5sum" ]; then
            rm -f "$CONFFILE"
        fi
    fi
}

case "$1" in
install|upgrade)
    if dpkg --compare-versions "$2" le "$LASTVERSION"; then
        prep_mv_conffile "/etc/pkg_conf.1"
        prep_mv_conffile "/etc/pkg_conf.2"
    fi
esac

Much like removal, the function first checks to make sure the conffile exists on the disk and whether the user has changed it or not. If the user has not changed it, we simply remove the old version from the disk; this will tell us that we don't need to move their changes over. We handle the case of a user-changed conffile in postinst in a minute...

In the main body of the preinst script, we again check the arguments of the script and make sure that the version we're upgrading is a version that contained the conffile we need to move. Again, the reason we check for install as well is that we need to cope with being reinstalled when the last version was removed and not purged.

If we moved the conffile in the preinst script, dpkg would unpack the new package and see that the conffile has changed and ask the user what to do about it. We want to avoid that, so we let dpkg unpack the package without conflict. We can now handle moving the user-changed conffile into place in the postinst script.

# Move a conffile without triggering a dpkg question
mv_conffile() {
    OLDCONFFILE="$1"
    NEWCONFFILE="$2"

    if [ -e "$OLDCONFFILE" ]; then
        echo "Preserving user changes to $NEWCONFFILE ..."
        mv -f "$NEWCONFFILE" "$NEWCONFFILE".dpkg-new
        mv -f "$OLDCONFFILE" "$NEWCONFFILE"
    fi
}

case "$1" in
configure)
    if dpkg --compare-versions "$2" le "$LASTVERSION"; then
        mv_conffile "/etc/pkg_conf.1" "/etc/pkg/conf.1"
        mv_conffile "/etc/pkg_conf.2" "/etc/pkg/conf.2"
    fi
esac

This function takes two arguments, the old and new filenames of the conffile. We check to make sure the old one still exists on the disk, if the user had not modified it the preinst script would have removed it for us. If the file still exists, we know that there were changes to it we want to preserve. We move the new conffile (as unpacked by dpkg) to a file the user can compare it with if they wish and move the old conffile into place with the new filename.

In the main body of the postinst script, we check the arguments of the script and make sure that the version we last configured (ie. upgrading) is a version that contained the conffile we need to move. If so, we call the function passing both the old and new filenames.

dpkg: ConffileHandling (last edited 2005-11-22 15:34:38 by ScottJamesRemnant)