#!/bin/sh
#
# netcf-transaction.sh: save/restore current network interface configuration
#
# Copyright (C) 2011, 2014 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see
# <http://www.gnu.org/licenses/>.
#
# Description: This script can save the current state of network config,
#              and later revert to that config, or commit the new config
#              (by deleting the snapshot). At boot time, if there are
#              uncommitted changes to the network config, they are
#              reverted (and the discarded changes are archived in
#              /var/lib/netcf/network-rollback-*).

# special exit code that means a command was issued that is invalid for the
# current state (e.g. change-begin when there is already an open transaction)
EINVALID_IN_THIS_STATE=199

sysconfdir="/etc"
localstatedir="/var"

netconfdir="$sysconfdir"/sysconfig/network-scripts
snapshotdir="$localstatedir"/lib/netcf/network-snapshot
rollbackdirbase="$localstatedir"/lib/netcf/network-rollback

# Source function library.
test ! -r "$sysconfdir"/rc.d/init.d/functions ||
    . "$sysconfdir"/rc.d/init.d/functions

# take a snapshot of current network configuration scripts
change_begin ()
{
    if test -e "$snapshotdir"
    then
        echo "There is already an open transaction ($snapshotdir exists)"
        return $EINVALID_IN_THIS_STATE
    fi
    if ! mkdir -p "$snapshotdir"
    then
        echo "failed to create snapshot directory $snapshotdir"
        return 1
    fi
    for f in "$netconfdir"/ifcfg-* "$netconfdir"/route-* \
             "$netconfdir"/rule-*
    do
        test ! -f "$f" && continue
        if ! cp -p "$f" "$snapshotdir"
        then
            echo "failed to copy $f to $snapshotdir"
            return 1
        fi
    done
    date >"$snapshotdir"/date
}

change_commit ()
{
    if test ! -d "$snapshotdir"
    then
        echo "No pending transaction to commit"
        return $EINVALID_IN_THIS_STATE
    fi
    if ! rm -rf "$snapshotdir"
    then
        echo "Failed to remove obsolete snapshot directory $snapshotdir"
    fi
}

# rollback network configuration to last snapshot (if one exists)
change_rollback ()
{
    if test ! -d "$snapshotdir"
    then
        echo "No pending transaction to rollback"
        return $EINVALID_IN_THIS_STATE
    fi

    rollback_ret=0

    # eliminate all but the last 20 rollback saves
    LC_ALL=C ls -d "$rollbackdirbase"-* 2>/dev/null | head -n -20 |\
    while read r
    do
        if ! rm -rf "$r"
        then
            # indicate an error, but continue
            echo "Failed to remove obsolete rollback directory $r"
            rollback_ret=1
        fi
    done

    # save a copy of the current config before rollback "just in case"
    rollbackdir=$rollbackdirbase-$(date +%Y.%m.%d-%H:%M:%S)
    if ! mkdir -p "$rollbackdir"
    then
        echo "failed to create rollback directory $rollbackdir"
        return 1
    fi
    for f in "$netconfdir"/ifcfg-* "$netconfdir"/route-* \
             "$netconfdir"/rule-*
    do
        test ! -f "$f" && continue
        if ! cp -p "$f" "$rollbackdir"
        then
            echo "failed to copy $f to $rollbackdir"
            return 1
        fi
    done

    # There are 4 classes of files, each handled slightly differently to
    # minimize disruption in services:
    # 1) file in both, unmodified - just erase the snapshot copy
    # 2) file in both, modified   - copy the snapshot version over
    #                               the current & erase copy
    # 3) file in current only     - remove the file
    # 4) file in snapshot only    - copy the snapshot file to current
    #
    # We handle the 1st three cases in one loop going through all
    # current config files, and what is left over in snapshotdir is,
    # by definition, case 4.
    #
    # (NB: we can't mv the files, because then the selinux labels
    # don't get reset properly.)

    for f in "$netconfdir"/ifcfg-* "$netconfdir"/route-* \
             "$netconfdir"/rule-*
    do
        test ! -f "$f" && continue

        snapshotf=$snapshotdir/${f##*/}
        if test -f "$snapshotf"
        then
            # Case (1) & (2) (only copies if they're different)
            if ! cmp -s "$snapshotf" "$f"
            then
                if ! cp -pf "$snapshotf" "$f"
                then
                    echo "failed to restore $snapshotf to $f"
                    return 1
                fi
            fi
            if ! rm -f "$snapshotf"
            then
                # indicate an error, but continue
                echo "Failed to remove obsolete snapshot file $snapshotf"
                rollback_ret=1
            fi
        else
            # Case (3)
            if ! rm -f "$f"
            then
                # indicate an error, but continue
                echo "Failed to remove unwanted config file $f"
                rollback_ret=1
            fi
        fi
    done

    # Case (4)
    for f in "$snapshotdir"/ifcfg-* "$snapshotdir"/route-* \
             "$snapshotdir"/rule-*
    do
        test ! -f "$f" && continue
        if ! cp -pf "$f" "$netconfdir"
        then
            echo "failed to restore $f to $netconfdir"
            return 1
        fi
    done

    test -f "$snapshotdir"/date \
     && echo Rolled back to network config state of $(cat "$snapshotdir"/date)

    if ! rm -rf "$snapshotdir"
    then
        # indicate an error, but continue
        echo "Failed to remove obsolete snapshot directory $snapshotdir"
        rollback_ret=1
    fi

    return $rollback_ret
}

# usage [val]
# Display usage string, then exit with VAL (defaults to 2).
usage() {
    echo $"Usage: $0 {change-begin|change-commit|change-rollback|snapshot-dir|start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
    exit ${1-2}
}

# See how we were called.
if test $# != 1; then
    usage
fi

retval=0
case "$1" in
    # commands required in all Fedora initscripts
    start|restart|reload|force-reload|condrestart|try-restart)
        echo -n $"Running $prog $1: "
        change_rollback
        # ignore the "no pending transaction" error
        test "$retval" != "$EINVALID_IN_THIS_STATE" && retval=$?
        echo
        ;;
    stop|status)
        if test -d "$snapshotdir"
        then
            echo $"There is an open transaction"
        else
            echo $"No open transaction"
        fi
        ;;

    --help)
        usage 0
        ;;
    # specific to netcf-transaction
    change-begin)
        change_begin
        retval=$?
        ;;
    change-commit)
        change_commit
        retval=$?
        ;;
    change-rollback)
        change_rollback
        retval=$?
        ;;
    snapshot-dir)
        echo $snapshotdir
        ;;
    *)
        usage
        ;;
esac

exit $retval
