#!/bin/bash
#
# transactional-update - apply updates to the system in an atomic way
#
# Author: Thorsten Kukuk <kukuk@suse.com>
# Copyright (C) 2016, 2017, 2018 SUSE Linux GmbH
#
# 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 2 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/>.

export LANG=C

DIR_TO_MOUNT="dev opt var/log"
EXITCODE=0
VERBOSITY=2
ZYPPER_ARG=""
ZYPPER_NONINTERACTIVE="-y --auto-agree-with-product-licenses"
ZYPPER_ARG_PKGS=()
REWRITE_BOOTLOADER=0
REWRITE_GRUB_CFG=0
REWRITE_INITRD=0
REBUILD_KDUMP_INITRD=0
DO_CLEANUP=0
DO_MIGRATION=0
DO_DUP=0
DO_ROLLBACK=0
DO_SELF_UPDATE=1
DO_REGISTRATION=0
REGISTRATION_ARGS=""
ROLLBACK_SNAPSHOT=0
REBOOT_AFTERWARDS=0
REBOOT_METHOD="auto"
RUN_SHELL=0
USE_SALT_GRAINS=0
CONFFILE="/etc/transactional-update.conf"
SYSTEMCONFFILE="/usr/etc/transactional-update.conf"
LOGFILE="/var/log/transactional-update.log"
STATE_FILE="/var/lib/misc/transactional-update.state"
NEW_SNAPSHOT_FLAG="/var/lib/overlay/transactional-update.newsnapshot"
SELF_PATH="`dirname $0`"
PACKAGE_UPDATES=0
ZYPPER_AUTO_IMPORT_KEYS=0
HAS_SEPARATE_VAR=0
SNAPSHOT_ID=""
BACKUP_SNAPSHOT_ID=""
SNAPPER_NO_DBUS=""

# Load config
if [ -r ${SYSTEMCONFFILE} ]; then
    . ${SYSTEMCONFFILE}
fi
if [ -r ${CONFFILE} ]; then
    . ${CONFFILE}
fi

# Log stderr to log file
exec 4>&2 2> >(tee -a "${LOGFILE}")

self_update() {
    if [ ${DO_SELF_UPDATE} == 0 ]; then
	return
    fi

    log_info "Checking for newer version."
    if zypper --non-interactive info transactional-update | grep -q '^Status *: out-of-date'; then
	log_info "New version found - updating..."
	TA_UPDATE_TMPFILE="`mktemp -d /tmp/transactional-update.XXXXXXXXXX`"
	if [ $? -ne 0 ]; then
	    log_error "ERROR: Couldn't create temporary directory for self-update."
	    quit 1
	fi
	export TA_UPDATE_TMPFILE
	cd "${TA_UPDATE_TMPFILE}"
	zypper --non-interactive --pkg-cache-dir "${TA_UPDATE_TMPFILE}" download transactional-update
	find . -name transactional-update*.rpm -exec rpm2cpio {} \; | cpio -idmv 2>/dev/null
	if [ $? -ne 0 ]; then
	    log_error "ERROR: Couldn't extract the update."
	    quit 1
	fi
	exec "${TA_UPDATE_TMPFILE}/usr/sbin/transactional-update" "$@"
    fi
}

usage() {
    echo "Syntax: transactional-update [option...] [general-command...] [package-command]"
    echo "        transactional-update [option...] standalone-command"
    echo ""
    echo "Applies package updates to a new snapshot without touching the running"
    echo "system."
    echo ""
    echo "General Commands:"
    echo "cleanup            Mark unused snapshots for snapper removal"
    echo "grub.cfg           Regenerate grub.cfg"
    echo "bootloader         Reinstall the bootloader"
    echo "initrd             Regenerate initrd"
    echo "kdump              Regenerate kdump initrd"
    echo "shell              Open rw shell in new snapshot before exiting"
    echo "reboot             Reboot after update"
    echo ""
    echo "Package Commands:"
    echo "dup                Call 'zypper dup --no-allow-vendor-change'"
    echo "up                 Call 'zypper up'"
    echo "patch              Call 'zypper patch'"
    echo "migration          Updates systems registered via SCC / SMT"
    echo "pkg install ...    Install individual packages"
    echo "pkg remove ...     Remove individual packages"
    echo "pkg update ...     Updates individual packages"
    echo ""
    echo "Standalone Commands:"
    echo "rollback <number>  Set given snapshot as default snapshot"
    echo ""
    echo "Options:"
    echo "--no-selfupdate    Skip checking for newer version"
    echo "--quiet            Don't print warnings and infos to stdout"
    echo "--help             Display this help and exit"
    echo "--version          Display version and exit"
    exit $1
}

print_version() {
    echo "transactional-update 2.5"
    exit 0
}

log_info() {
    echo `date "+%Y-%m-%d %H:%M"` "$@" >> ${LOGFILE}
    if [ ${VERBOSITY} -ge 2 ]; then
	echo "$@"
    fi
}

log_error() {
    echo `date "+%Y-%m-%d %H:%M"` "$@" >> ${LOGFILE}
    echo "$@" 1>&2
}

bashlock() {
    if [ "$#" -ne 1 ]; then
	echo 'Usage: bashlock [LOCKFILENAME]' 1>&2
	return 2
    fi
    LOCKFILE="$1"

    echo "$$" >"$LOCKFILE.$$"
    if ! ln "$LOCKFILE.$$" "$LOCKFILE" 2>/dev/null; then
	PID=`head -1 "$LOCKFILE"`
	if [ -z "$PID" ]; then
	    rm -f "$LOCKFILE"
	else
	   kill -0 "$PID" 2>/dev/null || rm -f "$LOCKFILE"
	fi

	if ! ln "$LOCKFILE.$$" "$LOCKFILE" 2>/dev/null; then
	    rm -f "$LOCKFILE.$$"
	    return 1
	fi
    fi

    rm -f "$LOCKFILE.$$"
    trap 'rm -f "$LOCKFILE"' EXIT

    return 0
}

save_state_file() {
    echo "LAST_WORKING_SNAPSHOTS=\"${LAST_WORKING_SNAPSHOTS}\"" > ${STATE_FILE}
    echo "UNUSED_SNAPSHOTS=\"${UNUSED_SNAPSHOTS}\"" >>  ${STATE_FILE}

    if [ $1 -ne 0 -a ${HAS_SEPARATE_VAR} -eq 0 ]; then
	# If /var/lib/misc is not a seperate partition / subvolume, copy the
	# state file into the new snapshot as it will contain an outdated
	# version from before taking the snapshot otherwise.
	grep -q var.lib.misc /proc/mounts
	if [ $? -ne 0 ]; then
	    cp -a ${STATE_FILE} "/.snapshots/$1/snapshot${STATE_FILE}"
	fi
    fi
}

rebuild_kdump_initrd() {
    local MOUNT_DIR=$1

    test -f /usr/lib/systemd/system/kdump.service || return
    systemctl is-enabled --quiet kdump.service
    if [ $? = 0 -a -x ${MOUNT_DIR}/usr/sbin/tu-rebuild-kdump-initrd ]; then
	chroot ${MOUNT_DIR} /usr/sbin/tu-rebuild-kdump-initrd |& tee -a ${LOGFILE}
    fi
}

# Only called in error case; reverts everything to previous state.
quit() {
    if [ -n "${SNAPSHOT_ID}" ] ; then
	log_error "Removing snapshot #${SNAPSHOT_ID}..."
	snapper ${SNAPPER_NO_DBUS} delete ${SNAPSHOT_ID} |& tee -a ${LOGFILE}
    fi
    if [ -n "${BACKUP_SNAPSHOT_ID}" ] ; then
	log_error "Removing snapshot #${BACKUP_SNAPSHOT_ID}..."
	snapper ${SNAPPER_NO_DBUS} delete ${BACKUP_SNAPSHOT_ID} |& tee -a ${LOGFILE}
    fi
    if [ $USE_SALT_GRAINS -eq 1 ]; then
	if [ -f /etc/salt/grains ]; then
	    grep -q tx_update_failed /etc/salt/grains
	    if [ $? -ne 0 ]; then
		# Add variable to existing salt grains
		echo "tx_update_failed: true" >> /etc/salt/grains
	    else
		# modify variable in existing salt grains
		sed -i -e 's|tx_update_failed:.*|tx_update_failed: true|g' /etc/salt/grains
	    fi
	else
	    echo "tx_update_failed: true" > /etc/salt/grains
	fi
    fi
    log_info "transactional-update finished"
    exit $1
}

reboot_via_salt() {
    log_info "transactional-update finished - created salt grains"
    if [ -f /etc/salt/grains ]; then
	grep -q tx_update_reboot_needed /etc/salt/grains
	if [ $? -ne 0 ]; then
	    # Add variable to existing salt grains
	    echo "tx_update_reboot_needed: true" >> /etc/salt/grains
	else
	    # modify variable in existing salt grains
	    sed -i -e 's|tx_update_reboot_needed:.*|tx_update_reboot_needed: true|g' /etc/salt/grains
	fi
    else
	echo "tx_update_reboot_needed: true" > /etc/salt/grains
    fi
    # Reset tx_update_failed if exist
    sed -i -e 's|tx_update_failed:.*|tx_update_failed: false|g' /etc/salt/grains
    exit 0
}

reboot_via_rebootmgr() {
    /usr/sbin/rebootmgrctl is-active --quiet
    if [ $? -eq 0 ]; then
	# rebootmgrctl is running
	/usr/sbin/rebootmgrctl reboot
	log_info "transactional-update finished - informed rebootmgr"
	exit 0
    fi
}

reboot_via_systemd() {
    log_info "transactional-update finished - rebooting machine"
    sync
    systemctl reboot |& tee -a ${LOGFILE}
    exit 0
}

reboot_autodetect() {
    if [ -x /usr/sbin/rebootmgrctl ]; then
	reboot_via_rebootmgr
    fi
    # If rebootmgr is inactive try systemd
    reboot_via_systemd
}

add_unique_id() {
    local NEW_ID="$1"

    for snap in ${LAST_WORKING_SNAPSHOTS}; do
	if [ ${snap} -eq ${NEW_ID} ]; then
	    return
	fi
    done
    LAST_WORKING_SNAPSHOTS="${NEW_ID} ${LAST_WORKING_SNAPSHOTS}"
}

check_registration_on_next_reboot() {
    if [ ${HAS_SEPARATE_VAR} -eq 0 ]; then
	# /var own subvolume, not mounted during update, so touch the
	# file in the running system.
	test -d  /var/lib/rollback || mkdir -p /var/lib/rollback
	touch /var/lib/rollback/check-registration
    else
	# check if /var/lib/rollback is own subvolume
	grep -q "[[:space:]]/var/lib/rollback[[:space:]]" /proc/mounts
	if [ $? -eq 0 ]; then
	    # Extra subvolume, not mounted in chroot, touch outside.
	    touch /var/lib/rollback/check-registration
	else
	    # no subvolumes,
	    test -d ${MOUNT_DIR}/var/lib/rollback || mkdir -p ${MOUNT_DIR}/var/lib/rollback
	    touch ${MOUNT_DIR}/var/lib/rollback/check-registration
	fi
    fi
}

# Sync /etc mount contents to target directory
sync_etc() {
    if findmnt -t overlay /etc >/dev/null; then
	log_info "Copying /etc state into snapshot $1"
	rsync --archive --xattrs --filter='-x trusted.overlay.*' --acls --delete --quiet /etc "$1"
	if [ $? -ne 0 ]; then
	    log_error "ERROR: copying of /etc into snapshot $1 failed!"
	    quit 1
	fi
    fi
}

ORIG_ARGS=("$@")

# If no option is given, assume "up"
if [ $# -eq 0 ]; then
    ZYPPER_ARG="up"
fi

while [ 1 ]; do
    if [ $# -eq 0 ]; then
	break
    fi

    case "$1" in
	cleanup)
	    DO_CLEANUP=1
	    shift
	    ;;
	dup)
	    DO_DUP=1
	    ZYPPER_ARG="dup --no-allow-vendor-change"
	    shift
	    ;;
        up|patch)
	    ZYPPER_ARG=$1
	    shift
	    ;;
	ptf|pkg|package)
	    shift
	    if [ $# -eq 0 ]; then
		usage 1
	    fi
	    case "$1" in
		install|in)
		    ZYPPER_ARG="install"
		    shift
		    ;;
		remove|rm)
		    ZYPPER_ARG="remove"
		    shift
		    ;;
		update|up)
		    ZYPPER_ARG="up"
		    shift
		    ;;
		*)
		    usage 1;
		    ;;
	    esac

	    if [ $# -eq 0 ]; then
		usage 1
	    fi

	    while [ 1 ]; do
		if [ $# -eq 0 ]; then
		    break;
		else
		    ZYPPER_ARG_PKGS+=("$1");
		    shift
		fi
	    done
            # Interactively run installing PTFs
            ZYPPER_NONINTERACTIVE=""
	    ;;
	migration)
	    DO_MIGRATION=1
	    ZYPPER_NONINTERACTIVE=""
	    ZYPPER_ARG="migration --no-snapshots"
	    shift
	    ;;
	bootloader)
	    REWRITE_BOOTLOADER=1
	    REWRITE_GRUB_CFG=1
	    shift
	    ;;
	grub.cfg)
	    REWRITE_GRUB_CFG=1
	    shift
	    ;;
	shell)
	    RUN_SHELL=1
	    shift
	    ;;
	initrd)
	    REWRITE_INITRD=1
	    REBUILD_KDUMP_INITRD=1
	    shift
	    ;;
	kdump)
	    REBUILD_KDUMP_INITRD=1
	    shift
	    ;;
	reboot)
	    REBOOT_AFTERWARDS=1
	    shift
	    ;;
	rollback)
	    DO_ROLLBACK=1
	    DO_SELF_UPDATE=0
	    shift
	    if [ $# -eq 1 ]; then
		ROLLBACK_SNAPSHOT=$1
		shift
	    fi
	    ;;
	salt)
	    REBOOT_AFTERWARDS=1
	    USE_SALT_GRAINS=1
	    REBOOT_METHOD="salt"
	    shift
	    ;;
	--no-selfupdate)
	    DO_SELF_UPDATE=0
	    shift
	    ;;
	--quiet)
	    VERBOSITY=1
	    shift
	    ;;
	register)
	    DO_REGISTRATION=1
	    shift

	    # Collect arguments for Registration
	    if [ $# -eq 0 ]; then
		usage 1
	    fi

	    while [ 1 ]; do
		if [ $# -eq 0 ]; then
		    break;
		else
		    REGISTRATION_ARGS="${REGISTRATION_ARGS} $1";
		    shift
		fi
	    done
	    ;;
	-h|--help)
	    usage 0
	    ;;
	--version)
	    print_version
	    ;;
	*)
	    if [ $# -ge 1 ]; then
		usage 1;
	    fi
	    ;;
    esac
done

if [ $# -ne 0 ]; then
    usage 1
fi

# Check if this is a self-updated transactional-update; if it isn't lock and
# check for update
if [ -z "${TA_UPDATE_TMPFILE}" ]; then
    bashlock "/var/run/transactional-update.pid"
    if [ $? -ne 0 ]; then
	echo "Couldn't get lock, is another instance already running?"
	exit 1
    fi
    self_update "${ORIG_ARGS[@]}"
else # Set exit handler to clean up artifacts of the self-update
    trap 'rm -f "$LOCKFILE" && rm -rf "${TA_UPDATE_TMPFILE}" && unset TA_UPDATE_TMPFILE' EXIT
fi

# Load old state file
if [ -f ${STATE_FILE} ]; then
    . ${STATE_FILE}
fi

log_info "transactional-update 2.5 started"
log_info "Options: ${ORIG_ARGS[@]}"

if [ "`stat -f -c %T /`" != "btrfs" ]; then
  log_error "ERROR: not using btrfs as root file system!"
  log_info "transactional-update finished"
  exit 1
fi

if [ ! -d /.snapshots ]; then
  log_error "ERROR: no snapshots for root file system configured!"
  log_info "transactional-update finished"
  exit 1
fi

grep -q "[[:space:]]/var[[:space:]]" /proc/mounts
if [ $? -eq 0 ]; then
    log_info "Separate /var detected."
    DIR_TO_MOUNT="${DIR_TO_MOUNT} var/cache var/lib/alternatives"
    HAS_SEPARATE_VAR=1
else
    grep -q var.cache /proc/mounts
    if [ $? -ne 0 ]; then
	log_error "WARNING: it looks like your installation isn't recent enough."
    fi
fi

if [ -n "${ZYPPER_ARG}" -a ${ZYPPER_AUTO_IMPORT_KEYS} -eq 1 ]; then
    ZYPPER_ARG="--gpg-auto-import-keys ${ZYPPER_ARG}"
fi

CURRENT_SNAPSHOT_ID=`grep subvol=/@/.snapshots/ /proc/mounts | grep "/ btrfs" | sed -e 's|.*.snapshots/\(.*\)/snapshot.*|\1|g'`
DEFAULT_SNAPSHOT_ID=`btrfs subvolume get-default / | sed -e 's|.*.snapshots/\(.*\)/snapshot|\1|g'`
RO_ROOT=`btrfs property get / ro | sed -e 's|ro=||'`

if [ ${DO_ROLLBACK} -eq 1 ]; then
    NEED_REBOOT_WARNING=1

    if [ ${ROLLBACK_SNAPSHOT} -eq 0 ]; then
	ROLLBACK_SNAPSHOT=${CURRENT_SNAPSHOT_ID}
	NEED_REBOOT_WARNING=0
    fi

    log_info "Rollback to snapshot ${ROLLBACK_SNAPSHOT}..."

    if [ ${RO_ROOT} == "true" ]; then
	BTRFS_ID=`btrfs subvolume list / |grep /.snapshots/${ROLLBACK_SNAPSHOT}/snapshot | awk '{print $2}'`
	if [ -z $BTRFS_ID ]; then
	    log_error "ERROR: couldn't determine btrfs subvolume ID"
	    quit 1
	else
	    btrfs subvolume set-default $BTRFS_ID /.snapshots
	    if [ $? -ne 0 ]; then
		log_error "ERROR: btrfs set-default $BTRFS_ID failed!"
		quit 1
	    fi
	    # Create the trigger to re-register the system as new version after next
	    # reboot.
	    check_registration_on_next_reboot
	fi
	# Remove possible cleanup algo
	snapper ${SNAPPER_NO_DBUS} modify -c '' ${ROLLBACK_SNAPSHOT}
    else
	snapper rollback ${ROLLBACK_SNAPSHOT}
	NEED_REBOOT_WARNING=1
    fi
    if [ ${NEED_REBOOT_WARNING} -eq 1 ]; then
	echo "Please reboot to finish rollback!"
    fi
    exit 0
fi

#
# Cleanup part: make sure old root file systems will be removed after they are no longer active.
#
if [ ${DO_CLEANUP} -eq 1 ]; then
    # If there is a list of working snapshots, go through it and mark any snapshot for deletion, if it is
    # not the currently used one or the active one.
    if [ -n "${LAST_WORKING_SNAPSHOTS}" ]; then
	for snap in ${LAST_WORKING_SNAPSHOTS}; do
	    if [ ${CURRENT_SNAPSHOT_ID} -ne ${snap} ]; then
		log_info "Adding cleanup algorithm to snapshot #${snap}"
		snapper modify -c number ${snap} |& tee -a ${LOGFILE}
		if [ ${PIPESTATUS[0]} -ne 0 ]; then
		    log_error "ERROR: cannot set cleanup algorithm for snapshot #${snap}"
		fi
		# If the old snapshot is read-write, we have already a mandatory snapshot and this one can deleted
		# earlier. If not, mark is as important, so that it will not get deleted too fast.
		if [ ${RO_ROOT} == "true" ]; then
		    log_info "Adding \"important=yes\" to snapshot #${snap}"
		    snapper modify -u "important=yes" ${snap} |& tee -a ${LOGFILE}
		    if [ ${PIPESTATUS[0]} -ne 0 ]; then
			log_error "ERROR: cannot set \"important=yes for snapshot\" #${snap}"
		    fi
		fi
	    else
		NEW_LIST="${snap} ${NEW_LIST}"
	    fi
	done
	LAST_WORKING_SNAPSHOTS="${NEW_LIST}"
	save_state_file 0
    fi

    # Check for aborted transactional-updates (due to power outtage, killed
    # process, forced shutdown or similar uncommon conditions).
    for snap in `snapper list | cut -f 2,8 -d \| | grep transactional-update-in-progress=yes | cut -f 1 -d \|`; do
	UNUSED_SNAPSHOTS="${UNUSED_SNAPSHOTS} ${snap}"
    done

    # Always try to cleanup all snapshots; only the current snapshot and an
    # eventual new default one needs to be kept.
    if [ -n "${UNUSED_SNAPSHOTS}" ]; then
	_new_unused=""
	for snap in ${UNUSED_SNAPSHOTS}; do
	    # Don't mark our current in use snapshot for deletion
	    if [ ${snap} -ne ${CURRENT_SNAPSHOT_ID} ] && \
		[ ${snap} -ne ${DEFAULT_SNAPSHOT_ID} ]; then
		log_info "Mark unused snapshot #${snap} for deletion"
		snapper modify -c number ${snap} |& tee -a ${LOGFILE}
		if [ ${PIPESTATUS[0]} -ne 0 ]; then
		    log_error "ERROR: cannot set cleanup algorithm for snapshot #${snap}"
		    # Is the snapshot still available at all?
		    if snapper list | cut -f 2 -d \| | grep -q ${snap}; then
			# Keep the snapshot in the list
			_new_unused="${snap} ${_new_unused}"
		    fi
		fi
	    elif [ ${snap} -ne ${CURRENT_SNAPSHOT_ID} ]; then
		# This is the snapshot which is currently in use, so keep it in
		# the list. We would probably never clean it up later otherwise.
		_new_unused="${snap} ${_new_unused}"
	    fi
	done
	UNUSED_SNAPSHOTS="${_new_unused}"
	save_state_file 0
    fi
fi

if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \
    -o ${REWRITE_INITRD} -eq 1 -o ${REBUILD_KDUMP_INITRD} -eq 1 \
    -o ${RUN_SHELL} -eq 1 -o ${REWRITE_BOOTLOADER} -eq 1 \
    -o ${DO_REGISTRATION} -eq 1 ]; then

    # If the current snapshot and the default snapshot differ
    # there have been changes that we are now discarding.
    if [ "${DEFAULT_SNAPSHOT_ID}" -ne "${CURRENT_SNAPSHOT_ID}" ]; then
       log_info "WARNING: Default snapshot differs from current snapshot."
       log_info "WARNING: Any changes within the previous snapshot will be discarded!"
    fi

    # Check if there are updates at all. Don't check if we do
    # a registration, as this can change the zypper result.
    if [ -n "${ZYPPER_ARG}" -a -n "${ZYPPER_NONINTERACTIVE}" \
         -a ${DO_REGISTRATION} -eq 0 ]; then
	if [ $DO_DUP -eq 1 ]; then
	    "${SELF_PATH}"/transactional-update-helper disable-optical
	fi

	TMPFILE=`mktemp /tmp/transactional-update.XXXXXXXXXX`
	zypper --xml ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} --dry-run "${ZYPPER_ARG_PKGS[@]}" > ${TMPFILE}
	if [ $? -ne 0 ]; then
	    LOG_MESSAGES=`awk -v RS='<' -v FS='>' '{if ($1 ~ /^(message|description)/) print $2}' ${TMPFILE} | perl -MHTML::Entities -pe 'decode_entities($_);'`
	    log_error "ERROR: Zypper failed with the following message(s):"
	    log_error "${LOG_MESSAGES}"
            rm -f ${TMPFILE}
	    quit 1
	fi
	PACKAGE_UPDATES=`grep "install-summary download-size" ${TMPFILE} | sed -e 's|.*install-summary download-size=\"\(.*\)\" space-usage-diff.*|\1|g'`
	SIZE_OF_UPDATES=`grep "install-summary.*space-usage-diff" ${TMPFILE} | sed -e 's|.*install-summary.*space-usage-diff=\"\(.*\)\">.*|\1|g'`
	rm -f ${TMPFILE}
	if [ ${PACKAGE_UPDATES} -eq 0 -a ${SIZE_OF_UPDATES} -eq 0 ]; then
	    log_info "zypper: nothing to update"
	    log_info "transactional-update finished"
	    if [ $USE_SALT_GRAINS -eq 1 ]; then
		log_info "Updating salt grains"
		if [ -f /etc/salt/grains ]; then
		    # Reset tx_update_failed if it exists. Could have been set due to wrong
		    # repository configuration or another temporary error before.
		    sed -i -e 's|tx_update_failed:.*|tx_update_failed: false|g' /etc/salt/grains
		fi
	    fi
	    exit 0
	fi
    fi

    # Create a backup snapshot of the current system state (i.e. preserve the /etc overlay contents on a read-only
    # system or the general system state on a read-write file system, similar to what zypper / snapper does).
    # Hint: The rw subvolume is not shown in grub2.
    log_info "Creating read-only snapshot of current system state (#${CURRENT_SNAPSHOT_ID})"
    BACKUP_SNAPSHOT_ID=`snapper create -p -t pre -c number -u "important=yes" -d "RO-Clone of #${CURRENT_SNAPSHOT_ID}"`
    BACKUP_SNAPSHOT_DIR=/.snapshots/${BACKUP_SNAPSHOT_ID}/snapshot
    if [ $? -ne 0 ]; then
	SNAPPER_NO_DBUS="--no-dbus"
	BACKUP_SNAPSHOT_ID=`snapper --no-dbus create -p -t pre -c number -u "important=yes" -d "RO-Clone of #${CURRENT_SNAPSHOT_ID}"`
	if [ $? -ne 0 ]; then
	    log_error "ERROR: snapper create failed!"
	    quit 1
	fi
    fi

    # Make the backup snapshot read-write:
    btrfs property set ${BACKUP_SNAPSHOT_DIR} ro false
    if [ $? -ne 0 ]; then
	log_error "ERROR: changing ${BACKUP_SNAPSHOT_DIR} to read-write failed!"
	quit 1
    fi

    # Copy the contents of /etc into the backup snapshot
    sync_etc ${BACKUP_SNAPSHOT_DIR}

    # Create the working snapshot
    SNAPSHOT_ID=`snapper create --type post --pre-number ${BACKUP_SNAPSHOT_ID} -p -u "transactional-update-in-progress=yes" -d "Snapshot Update"`
    if [ $? -ne 0 ]; then
	SNAPPER_NO_DBUS="--no-dbus"
	SNAPSHOT_ID=`snapper --no-dbus create --type post --pre-number ${BACKUP_SNAPSHOT_ID} -p -u "transactional-update-in-progress=yes" -d "Snapshot Update"`
	if [ $? -ne 0 ]; then
	    log_error "ERROR: snapper create failed!"
	    quit 1
	fi
    fi

    SNAPSHOT_DIR=/.snapshots/${SNAPSHOT_ID}/snapshot

    # Make the snapshot read-write:
    btrfs property set  ${SNAPSHOT_DIR} ro false
    if [ $? -ne 0 ]; then
	log_error "ERROR: changing ${SNAPSHOT_DIR} to read-write failed!"
	quit 1
    fi

    # Remember all snapshots we create for update. If transactional-update is
    # run several times before a reboot, we need to clean up the unused
    # snapshots, otherwise we would have a big disk space leak. But don't store
    # it on disk yet, in error case we would delete the snapshot again.
    UNUSED_SNAPSHOTS="${SNAPSHOT_ID} ${UNUSED_SNAPSHOTS}"

    # Check if installed with SLES12
    if [ ${HAS_SEPARATE_VAR} -eq 0 ]; then
	touch ${SNAPSHOT_DIR}/var/tmp/update_snapshot.test
	if [ $? -ne 0 ]; then
	    log_error "ERROR: System installation is too old!"
	    quit 1
	fi
	rm -f ${SNAPSHOT_DIR}/var/tmp/update_snapshot.test
    else
        # Check if the btrfs subvolumes were created correct
        # or with broken storage-ng [bsc#1077240]
        touch ${SNAPSHOT_DIR}/var/update_snapshot.test
        if [ $? -ne 0 ]; then
            log_error "ERROR: System installation is broken!"
            quit 1
        fi
        rm -f ${SNAPSHOT_DIR}/var/update_snapshot.test
    fi

    if [ ${RO_ROOT} == "true" ]; then
	if [ ${RUN_SHELL} -eq 1 ]; then
	    DIR_TO_MOUNT="${DIR_TO_MOUNT} root"
	fi
    fi

    # Check which directories in /boot/grub2 need to be mounted,
    # otherwise grub2 will not boot after a version update.
    DIR_TO_MOUNT="${DIR_TO_MOUNT} $(awk '$2 ~ /^\/boot\/grub2\// { print $2 }' /proc/mounts)"

    # Mount everything we need:
    mount -t proc none ${SNAPSHOT_DIR}/proc
    if [ $? -ne 0 ]; then
        log_error "ERROR: mount of proc failed!"
        quit 1
    fi
    mount -t sysfs sys ${SNAPSHOT_DIR}/sys
    if [ $? -ne 0 ]; then
        log_error "ERROR: mount of sys failed!"
        quit 1
    fi
    if [ -x /usr/sbin/selinuxenabled ]; then
        /usr/sbin/selinuxenabled
        if [ $? -eq 0 ]; then
            mount -t selinuxfs selinux ${SNAPSHOT_DIR}/sys/fs/selinux
            if [ $? -ne 0 ]; then
                log_error "ERROR: mount of sys failed!"
                quit 1
            fi
        fi
    fi
    for directory in $DIR_TO_MOUNT ; do
	# Make sure mount point exists. With /var on an own subvolume, this directory
	# is empty by default and mount points don't exist in chroot environment.
	test -d ${SNAPSHOT_DIR}/$directory || mkdir -p ${SNAPSHOT_DIR}/$directory
	mount -o bind /$directory ${SNAPSHOT_DIR}/$directory
	if [ $? -ne 0 ]; then
	    log_error "ERROR: mount of $directory failed!"
	    quit 1
	fi
    done

    # Copy the contents of /etc into snapshot
    sync_etc ${SNAPSHOT_DIR}

    # If we have a seperate /var, create some files / directories which we
    # will delete again later.
    if [ ${HAS_SEPARATE_VAR} -eq 1 ]; then
	mkdir ${SNAPSHOT_DIR}/var/tmp
	cp /var/lib/zypp/RequestedLocales ${SNAPSHOT_DIR}/var/lib/zypp/
    fi

    # Check if we have /var/lib/rpm, otherwise zypper will
    # create a new rpm database [bsc#1074598]
    if [ ! -e ${SNAPSHOT_DIR}/var/lib/rpm -a \
	-e ${SNAPSHOT_DIR}/usr/lib/sysimage/rpm ]; then
	mkdir -p ${SNAPSHOT_DIR}/var/lib
	ln -sf ../../usr/lib/sysimage/rpm ${SNAPSHOT_DIR}/var/lib/rpm
    fi

    # Do we need to cleanup the /var/cache directory?
    if [ -d ${SNAPSHOT_DIR}/var/cache/zypp ]; then
	VAR_CACHE_CLEANUP=0
    else
	VAR_CACHE_CLEANUP=1
    fi

    # Create bind mounts or else grub2 will fail
    MOUNT_DIR=`mktemp -d`
    mount -o rbind ${SNAPSHOT_DIR} ${MOUNT_DIR}
    mount -o bind,ro /.snapshots ${MOUNT_DIR}/.snapshots

    # Set indicator for RPM pre/post sections to detect whether we run in a
    # transactional update
    export TRANSACTIONAL_UPDATE=true

    if [ ${DO_REGISTRATION} -eq 1 ]; then
	SUSEConnect --root ${MOUNT_DIR}  ${REGISTRATION_ARGS}
    fi

    if [ -n "${ZYPPER_ARG}" ]; then

	log_info "Calling zypper ${ZYPPER_ARG}"
	# All transactional-update zypper commands except migration and pkg
	if [ -n "${ZYPPER_NONINTERACTIVE}" ]; then
	    env DISABLE_RESTART_ON_UPDATE=yes zypper -R ${MOUNT_DIR} ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE}
	    RETVAL=${PIPESTATUS[0]}
	else
	    # transactional-update migration
	    if [ ${DO_MIGRATION} -eq 1 ]; then
		chroot ${MOUNT_DIR} env DISABLE_RESTART_ON_UPDATE=yes zypper ${ZYPPER_ARG} "${ZYPPER_ARG_PKGS[@]}"
		RETVAL=$?
		# Reset registration until reboot. Needed in both cases,
		# whether an error occured or whether we had success.
		test -x /usr/sbin/rollback-reset-registration && /usr/sbin/rollback-reset-registration
		if [ $RETVAL -eq 0 ]; then
		    # Create the trigger to re-register the system as new version after next
		    # reboot.
		    check_registration_on_next_reboot
		fi
	    # transactional-update pkg
	    else
		env DISABLE_RESTART_ON_UPDATE=yes zypper -R ${MOUNT_DIR} ${ZYPPER_ARG} "${ZYPPER_ARG_PKGS[@]}"
		RETVAL=$?
	    fi
	fi

	if [ $RETVAL -eq 0 -o $RETVAL -eq 102 -o $RETVAL -eq 103 -o \( $DO_DUP -eq 0 -a $RETVAL -eq 106 \) ]; then
	    REBUILD_KDUMP_INITRD=1
	    # check if products are updated and we need to re-register
	    # at next boot.
	    diff -qr /etc/products.d ${MOUNT_DIR}/etc/products.d > /dev/null
	    if [ $? -ne 0 ]; then
		check_registration_on_next_reboot
	    fi
	    # Rebuild grub.cfg if /etc/os-release changes, could change grub
	    # menu output, too.
	    cmp -s /etc/os-release ${MOUNT_DIR}/etc/os-release
	    if [ $? -ne 0 ]; then
	        REWRITE_GRUB_CFG=1
	    fi
	else
	    log_error "ERROR: zypper ${ZYPPER_ARG} on ${MOUNT_DIR} failed with exit code ${RETVAL}!"
	    EXITCODE=1
	fi
    fi

    if [ ${REWRITE_INITRD} -eq 1 ]; then
	log_info "Creating new initrd"
	chroot ${MOUNT_DIR} /sbin/mkinitrd
	if [ $? -ne 0 ]; then
	    log_error "ERROR: mkinitrd failed!"
	    EXITCODE=1
	else
	    REBUILD_KDUMP_INITRD=1
	fi
    fi

    if [ ${REBUILD_KDUMP_INITRD} -eq 1 ]; then
	log_info "Trying to rebuild kdump initrd"
	rebuild_kdump_initrd ${MOUNT_DIR}
    fi

    if [ ${REWRITE_GRUB_CFG} -eq 1 ]; then
	log_info "Creating a new grub2 config"
	chroot ${MOUNT_DIR} /usr/sbin/grub2-mkconfig > ${MOUNT_DIR}/boot/grub2/grub.cfg
	if [ $? -ne 0 ]; then
	    log_error "ERROR: grub2-mkconfig failed!"
	    EXITCODE=1;
	fi
    fi

    if [ ${REWRITE_BOOTLOADER} -eq 1 ]; then
	log_info "Writing new bootloader"
	chroot ${MOUNT_DIR} /sbin/pbl --install
	if [ $? -ne 0 ]; then
	    log_error "ERROR: /sbin/pbl --install failed!"
	    EXITCODE=1;
	fi
    fi

    if [ ${RUN_SHELL} -eq 1 ]; then
	echo "Opening chroot in snapshot ${SNAPSHOT_ID}, continue with 'exit'"
	env PS1="transactional update # " chroot ${MOUNT_DIR} bash 2>&4
    fi

    # Unset variable
    unset TRANSACTIONAL_UPDATE

    # Delete temporary data before unmounting everything:
    if [ ${HAS_SEPARATE_VAR} -eq 1 ]; then
	rm -rf ${SNAPSHOT_DIR}/var/tmp
	rm -f ${SNAPSHOT_DIR}/var/lib/zypp/RequestedLocales
    fi

    # Unmount everything we don't need anymore:
    for directory in proc sys $DIR_TO_MOUNT .snapshots ; do
	umount -R ${SNAPSHOT_DIR}/$directory
	if [ $? -ne 0 ]; then
	    log_error "ERROR: umount of $directory failed!"
	    fuser -v ${SNAPSHOT_DIR}/$directory >> ${LOGFILE}
	    lsof ${SNAPSHOT_DIR}/$directory >> ${LOGFILE}
            # Try again after some time
            sleep 30
            umount -R ${SNAPSHOT_DIR}/$directory
            if [ $? -ne 0 ]; then
                log_error "ERROR 2nd try: umount of $directory failed!"
	        EXITCODE=1;
            fi
	fi
    done
    umount -R ${MOUNT_DIR}

    # Cleanup of temporary mount point
    rmdir ${MOUNT_DIR}

    # Cleanup other stuff
    # Cleanup cache directory
    if [ $VAR_CACHE_CLEANUP -eq 1 ]; then
	rm -rf ${SNAPSHOT_DIR}/var/cache/*
    fi
    # systemd-tmpfiles creates directories/files even if /run is no tmpfs:
    rm -rf ${SNAPSHOT_DIR}/run/*
    # WARNING: /var/spool/ can contain changes through RPM!
    rm -rf ${SNAPSHOT_DIR}/var/spool/*

    # Somersault:
    if [ $EXITCODE -eq 0 ]; then
	BTRFS_ID=`btrfs subvolume list / |grep ${SNAPSHOT_DIR}$ | awk '{print $2}'`
	if [ -z "$BTRFS_ID" ]; then
	    log_error "ERROR: couldn't determine btrfs subvolume ID"
	    EXITCODE=1
	else
	    btrfs subvolume set-default $BTRFS_ID ${SNAPSHOT_DIR}
	    if [ $? -ne 0 ]; then
		log_error "ERROR: btrfs set-default $BTRFS_ID failed!"
		EXITCODE=1;
	    else
		# Save the old snapshot or else it will get lost.
		add_unique_id ${CURRENT_SNAPSHOT_ID}
		save_state_file ${SNAPSHOT_ID}
		# Create flag file for overlay purging
		if [ ${RO_ROOT} = "true" ]; then
		    echo "EXPECTED_SNAPSHOT_ID=${SNAPSHOT_ID}" > "${NEW_SNAPSHOT_FLAG}"
		fi
		# Reset in-progress flag
		snapper ${SNAPPER_NO_DBUS} modify -u "transactional-update-in-progress=" ${SNAPSHOT_ID}
	    fi
	fi
    fi

    # Make the snapshot ro flag identical to current root:
    btrfs property set  ${SNAPSHOT_DIR} ro ${RO_ROOT}
    if [ $? -ne 0 ]; then
	log_error "ERROR: changing ${SNAPSHOT_DIR} to ro=${RO_ROOT} failed!"
	EXITCODE=1
    fi
    if [ -n ${BACKUP_SNAPSHOT_DIR} ]; then
	btrfs property set ${BACKUP_SNAPSHOT_DIR} ro ${RO_ROOT}
	if [ $? -ne 0 ]; then
	    log_error "ERROR: changing ${BACKUP_SNAPSHOT_DIR} to ro=${RO_ROOT} failed!"
	    EXITCODE=1
	fi
    fi

    if [ ${EXITCODE} -ne 0 ]; then
	quit ${EXITCODE}
    elif [ $REBOOT_AFTERWARDS -eq 0 ]; then
	echo "Please reboot your machine to activate the changes and avoid data loss."
    fi
fi

if [ ${EXITCODE} -eq 0 ]; then
    if [ $REBOOT_AFTERWARDS -eq 1 ]; then
	case "$REBOOT_METHOD" in
	    auto)
		reboot_autodetect
		;;
	    salt)
		reboot_via_salt
		;;
	    rebootmgr)
		reboot_via_rebootmgr
		;;
	    systemd)
		reboot_via_systemd
		;;
	    *)
	        log_info "Unsupported reboot method, falling back to 'auto'; please"
	        log_info "check your configuration in ${CONFFILE}."
	        reboot_autodetect
	        ;;
	esac
	echo "The system couldn't be rebooted using method '${REBOOT_METHOD}'. Please reboot the system"
	echo "manually."
    fi
fi

log_info "transactional-update finished"

exit $EXITCODE
