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

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_APPLY=0
DO_CLEANUP_OVERLAYS=0
DO_CLEANUP_SNAPSHOTS=0
DO_MIGRATION=0
DO_DUP=0
DO_ROLLBACK=0
DO_SELF_UPDATE=1
DO_REGISTRATION=0
DO_RUN=0
DO_STATUS=0
DO_STATUS_LAST=0
REGISTRATION_ARGS=""
ROLLBACK_SNAPSHOT=0
REBOOT_AFTERWARDS=0
REBOOT_LEVEL="reboot"
REBOOT_LEVEL_PREV=""
REBOOT_METHOD="auto"
declare -A REBOOT_ASSOCS
declare -A REBOOT_PRIO
REBOOT_PRIO[reboot]=3
REBOOT_PRIO[kexec]=2
REBOOT_PRIO[soft-reboot]=1
RUN_CMD=""
RUN_SHELL=0
SETUP_FIPS=0
SETUP_KDUMP=0
SETUP_SELINUX=0
UPDATE_METHOD="up"
USE_TELEMETRICS=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"
NEEDS_RESTARTING_FILE="/run/reboot-needed"
LOCKFILE="/var/run/transactional-update.pid"
STATUS_FILE="/usr/lib/sysimage/tu/transactional-update.status"
EXPERIMENTAL_STATUS=0
BASE_MANIFEST_FILE="/usr/lib/sysimage/tu/base.manifest"
SYSTEM_MANIFEST_FILE="/usr/lib/sysimage/tu/system.manifest"
ZYPPER_AUTO_IMPORT_KEYS=0
ETC_OVERLAY_PATTERN='^[^[:space:]]\+[[:space:]]\+\/etc[[:space:]]\+overlay[[:space:]]\+\([^[:space:]]*,\|\)workdir=\/sysroot\/var\/lib\/overlay\/work-etc[,[:space:]]'
NON_ROOTFS_WHITELIST=("/var/lib/YaST2/cookies" "/var/lib/rpm" "/var/lib/systemd/migrated" "/var/run/zypp.pid")
DRACUT_OPTS=""
TUKIT_OPTS=""

TMPDIR=${TMPDIR:-/tmp}

# Load config
if [ -r ${SYSTEMCONFFILE} ]; then
    . ${SYSTEMCONFFILE}
fi
for snippet in "${SYSTEMCONFFILE}.d"/*; do
    if [ -f "${snippet}" ]; then
	. "${snippet}"
    fi
done
if [ -r ${CONFFILE} ]; then
    . ${CONFFILE}
fi

# Initialize internal variables
ETC_IS_OVERLAY=0
HAS_SEPARATE_VAR=0
FORCE_NONINTERACTIVE=""
SNAPSHOT_ID=""
SNAPSHOT_DIR=""
BASE_SNAPSHOT_ID=""
TMPFILE=""
APPLYWORKDIR=""
DO_CALLEXT=0
LAST_BOOTED=""

declare -A ROLES=()
REPOS=()
declare -A BUILDTIME=()

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 ${TMPDIR}/transactional-update.XXXXXXXXXX`"
	if [ $? -ne 0 ]; then
	    log_error "ERROR: Couldn't create temporary directory for self-update."
	    quit 1
	fi
	pushd "${TA_UPDATE_TMPFILE}" >/dev/null
	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
	# Reset CWD before restart
	popd >/dev/null
	if ! "${TA_UPDATE_TMPFILE}/usr/sbin/transactional-update" --version >/dev/null; then
	    log_error "Cannot execute updated transactional-update - skipping"
	    rm -rf "${TA_UPDATE_TMPFILE}"
	    return
	fi
	export TA_UPDATE_TMPFILE
	exec "${TA_UPDATE_TMPFILE}/usr/sbin/transactional-update" "$@" 1>&${origstdout} 2>&${origstderr}
    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 "apply                      Switch into default snapshot without reboot"
    echo "cleanup                    Run both cleanup-snapshots and cleanup-overlays"
    echo "cleanup-snapshots          Mark unused snapshots for snapper removal"
    echo "cleanup-overlays           Remove unused overlay layers"
    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 "run <cmd>                  Run a command in a new snapshot"
    echo "setup-fips                 Install and enable FIPS pattern package"
    echo "setup-kdump [--crashkernel=<low>,<high>]  Configure and enable kdump"
    echo "setup-selinux              Install targeted SELinux policy and enable it"
    echo ""
    echo "Package Commands:"
    echo "Defaults: (i) interactive command; (n) non-interactive command"
    echo "dist-upgrade, dup          Call 'zypper dup' (n)"
    echo "update, up                 Call 'zypper up' (n)"
    echo "patch                      Call 'zypper patch' (n)"
    echo "migration                  Updates systems registered via SCC / SMT (i)"
    echo "pkg install|in ...         Install individual packages (i)"
    echo "pkg remove|rm ...          Remove individual packages (i)"
    echo "pkg update|up ...          Updates individual packages (i)"
    echo "register ...               Register system via SUSEConnect (implies -d)"
    echo ""
    echo "Standalone Commands:"
    echo "rollback [<number>]        Set the current or given snapshot as default snapshot"
    echo "rollback last              Set the last working snapshot as default snapshot"
    echo "status                     Status and history report [experimental - enabled: ${EXPERIMENTAL_STATUS}]"
    echo ""
    echo "Options:"
    echo "--interactive, -i          Use interactive mode for package command"
    echo "--non-interactive, -n      Use non-interactive mode for package command"
    echo "--continue [<number>], -c  Use latest or given snapshot as base"
    echo "--no-selfupdate            Skip checking for newer version"
    echo "--drop-if-no-change, -d    Drop the snapshot if there is no change"
    echo "--quiet                    Don't print warnings and infos to stdout"
    echo "--help, -h                 Display this help and exit"
    echo "--version                  Display version and exit"
    exit $1
}

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

log_to_file() {
    echo -e `date "+%Y-%m-%d %H:%M"` "$@" >> ${LOGFILE}
}

log_to_stdout() {
    if [ ${VERBOSITY} -ge 2 ]; then
	echo -e "$@"
    fi
}

log_info() {
    log_to_stdout "$@"
    log_to_file "$@"
}

log_error() {
    log_to_file "$@"
    echo -e "$@" 1>&2
}

bashlock() {
    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 [ "${LAST_BOOTED}" != "${BOOTED_SNAPSHOT_ID}" ]; then
	declare -A REBOOT_ASSOCS
    fi
    echo "LAST_BOOTED=\"${BOOTED_SNAPSHOT_ID}\"" >> ${STATE_FILE}

    if [ -n "${SNAPSHOT_ID}" ]; then
	REBOOT_ASSOCS[${SNAPSHOT_ID}]=${REBOOT_LEVEL}
    fi
    for key in "${!REBOOT_ASSOCS[@]}"; do
	echo "REBOOT_ASSOCS[${key}]=${REBOOT_ASSOCS[${key}]}" >> ${STATE_FILE}
    done

    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() {
    if tukit -q call "$1" systemctl is-enabled --quiet kdump.service; then
	tukit ${TUKIT_OPTS} call "$1" /sbin/mkdumprd |& tee -a ${LOGFILE} 1>&${origstdout}
	return ${PIPESTATUS[0]}
    elif [ ${SETUP_KDUMP} -ge 1 ]; then
	log_info "INFO: Requested rebuild of kdump initrd, but kdump is not enabled."
	log_info "      Did you mean 'setup-kdump'?"
    fi
    return 0;
}

# Only called in error case; reverts everything to previous state.
quit() {
    teardown

    # Reset reboot level
    if [ "${REBOOT_LEVEL_PREV}" == "none" ]; then
	rm -f "${NEEDS_RESTARTING_FILE}"
    else
	echo -n "${REBOOT_LEVEL_PREV}" > "${NEEDS_RESTARTING_FILE}"
    fi

    if [ -n "${SNAPSHOT_ID}" ] ; then
	log_error "Removing snapshot #${SNAPSHOT_ID}..."
	tukit ${TUKIT_OPTS} abort ${SNAPSHOT_ID} |& tee -a ${LOGFILE}
    fi
    log_info "transactional-update finished"
    exit $1
}

# Called on exit (both on success and failure); cleans up temporary files,
# mount points and variables
teardown() {
    # Cleanup temporary files
    rm -f ${TMPFILE}

    if [ -d "${APPLYWORKDIR}" ]; then
	if mountpoint --quiet "${APPLYWORKDIR}/mount"; then
	    umount --recursive "${APPLYWORKDIR}/mount"
	fi
	rmdir "${APPLYWORKDIR}/mount"
	rmdir "${APPLYWORKDIR}"
    fi

    # The following commands only make sense if snapshot dir is set already
    if [ "${SNAPSHOT_DIR}" = "" ]; then
	return
    fi

    # systemd-tmpfiles creates directories/files even if /run is no tmpfs:
    rm -rf ${SNAPSHOT_DIR}/run/*
}

do_apply() {
    if [ -n "${SNAPSHOT_ID}" -o -n "${ROLLBACK_SNAPSHOT}" -o "${DEFAULT_SNAPSHOT_ID}" != "${CURRENT_SNAPSHOT_ID}" ]; then
	NEW_DEFAULT_SNAPSHOT_ID=`btrfs subvolume get-default / | sed -e 's|.*.snapshots/\(.*\)/snapshot|\1|g'`
	if ! APPLYWORKDIR="$(mktemp -d /tmp/transactional-update.apply.XXXXXXXX)"; then
	    log_error "ERROR during apply: Temporary directory template failed."
	fi
	if ! mkdir "${APPLYWORKDIR}/mount"; then
	    log_error "ERROR during apply: Couldn't create mount directory."
	fi

	sourcedevice="$(findmnt --target /usr --raw --noheadings --output SOURCE --nofsroot | tail -n 1)"
	# Mount new snapshot as base
	if ! mount "${sourcedevice}" "${APPLYWORKDIR}/mount"; then
	    log_error "ERROR during apply: Mounting ${sourcedevice} to ${APPLYWORKDIR}/mount failed."
	    quit 2
	fi
	# Mount /etc overlay
	if [ "$(findmnt --noheadings --nofsroot --output FSTYPE /etc | tail -n 1)" == "overlay" ]; then
	    if ! mount overlay -t overlay -o "$(findmnt --tab-file "${APPLYWORKDIR}/mount/etc/fstab" --noheadings --nofsroot --output OPTIONS /etc | sed 's/\/sysroot//g' | sed 's/:\/etc,/:\/.snapshots\/'${NEW_DEFAULT_SNAPSHOT_ID}'\/snapshot\/etc,/g')" "${APPLYWORKDIR}/mount/etc"; then
		log_error "ERROR during apply: Mounting /etc overlay failed."
		quit 2
	    fi
	fi
	# Find and mount potential submounts
	VISIBLEMOUNTS=(/usr /etc /boot)
	for line in $(findmnt --raw --noheadings --nofsroot --output TARGET -d forward | grep -E "^/(usr|etc|boot)"); do
	    for i in ${!VISIBLEMOUNTS[@]}; do
		if [[ $line == ${VISIBLEMOUNTS[$i]} ]]; then
		    while [ $i -lt ${#VISIBLEMOUNTS[@]} ]; do
			if [[ ${VISIBLEMOUNTS[$i]} == ${line}* ]]; then
			    unset -v VISIBLEMOUNTS[$i]
			    VISIBLEMOUNTS=("${VISIBLEMOUNTS[@]}")
			else
			    ((i++))
			fi
		    done
		    break
		fi
	    done
	    VISIBLEMOUNTS+=(${line})
	done
	for dir in ${VISIBLEMOUNTS[@]}; do
	    if [[ $dir =~ ^/(usr|etc|boot)$ ]]; then
		continue;
	    fi
	    dir="$(echo -e ${dir})"
	    if ! mount --bind "${dir}" "${APPLYWORKDIR}/mount/${dir}"; then
		log_error "ERROR during apply: Bind-mounting ${dir} failed."
		quit 2
	    fi
	done
	if ! mount --make-rprivate "${APPLYWORKDIR}/mount"; then
	    log_error "ERROR during apply: make-rprivate failed."
	    quit 2
	fi

	log_info ""
	log_info "Using default snapshot ${NEW_DEFAULT_SNAPSHOT_ID} to replace running system..."

	# Mount new snapshot into running system
	for rbinddir in usr etc boot; do
	    log_info "Applying /${rbinddir}..."
	    if ! mount --rbind "${APPLYWORKDIR}/mount/${rbinddir}" "/${rbinddir}"; then
		log_error "ERROR during apply: Mounting /${rbinddir} failed!"
		for dir in ${rbinddir_done}; do
		    umount "/${dir}"
		done
		quit 2
	    fi
	    rbinddir_done="${rbinddir_done} ${rbinddir}"
	done

	umount --lazy "${APPLYWORKDIR}/mount"
	rmdir "${APPLYWORKDIR}/mount"
	rmdir "${APPLYWORKDIR}"
	if [ -e "${NEEDS_RESTARTING_FILE}" ]; then
	    grep -q soft-reboot "${NEEDS_RESTARTING_FILE}" && rm "${NEEDS_RESTARTING_FILE}"
	fi

	log_info "Executing systemctl daemon-reexec..."
	systemctl daemon-reexec
	log_info "Executing create_dirs_from_rpmdb..."
	create_dirs_from_rpmdb
	log_info "Executing systemd-tmpfiles --create..."
	systemd-tmpfiles --create

	log_info "=> Applied default snapshot as new base for running system!"
	log_info "   Running processes will not be restarted automatically."
    else
	log_info ""
	log_info "The default snapshot is active already."
    fi
}

write_needs_restarting() {
    local prio1="${REBOOT_PRIO[${1}]}"
    local prio2=0
    local prio3=0
    if [ -n "${2}" ]; then
	prio2="${REBOOT_PRIO[${2}]}"
    fi
    if [ -e "${NEEDS_RESTARTING_FILE}" ]; then
	prio3="${REBOOT_PRIO[$(cat "${NEEDS_RESTARTING_FILE}")]}"
    fi

    if [ "${prio1}" -ge "${prio2}" ] && [ "${prio1}" -gt "${prio3}" ]; then
	echo -n "${1}" > "${NEEDS_RESTARTING_FILE}"
    elif [ "${prio2}" -ge "${prio1}" ] && [ "${prio2}" -gt "${prio3}" ]; then
	echo -n "${2}" > "${NEEDS_RESTARTING_FILE}"
    fi
}

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() {
    local VARDIR="/var/lib/rollback"
    # If VARDIR is part of the root file system (usually on rw systems), then
    # create the file in the new snapshot
    if [ "$(findmnt --noheadings --output TARGET --target "${VARDIR}")" = "/" ]; then
        VARDIR="${SNAPSHOT_DIR}${VARDIR}"
    fi
    test -d "${VARDIR}" || mkdir -p "${VARDIR}"
    touch "${VARDIR}/check-registration"
}

# Return the optimized list of mount points
root_mount_points() {
    local prev=0
    for mount in $(findmnt --noheadings --submounts --target / --output TARGET --raw | sort | tail -n +2); do
	if [ "${prev}/" != "${mount:0:$((${#prev}+1))}" ]; then
	    echo "${mount}"
	    prev="${mount}"
	fi
    done
}

# Call tukit callext with the correct parameters
callext() {
    tukit ${TUKIT_OPTS} --quiet callext "${SNAPSHOT_ID}" "$@"
}

# Helper function to grep perl regular expressions
re_findall() {
    grep --only-matching --perl-regexp "$1" "${2:--}"
}

# XXX bsc#1170709 can impact here, but is not expected tha the base
# product change.
# Get the baseproduct name
baseproduct() {
    local file="/etc/products.d/baseproduct"
    callext rpm --root {} --quiet --query --file "$file" || log_to_file "File $file not owned: check bsc#1170709"

    re_findall '(?<=<name>).*?(?=</name>)' "${SNAPSHOT_DIR}${file}"
}

# XXX bsc#1170709 will impact a lot here. We need a more consistent
# proposal.
# Read all the roles from control.xml and store in a global variable
load_roles() {
    local file="/etc/YaST2/control.xml"
    callext rpm --root {} --quiet --query --file "$file" || log_to_file "File $file not owned: check bsc#1170709"

    # TODO: detect the microos_selinux pattern

    # Parsing control.xml cannot be done directly by grep, as the
    # logic of default_patterns can be a bit complex in the case of
    # MicroOS.  We can have some general default_patterns, and roles
    # without default_patterns.
    local xpath="//*[local-name()='productDefines']"
    xpath+="/*[local-name()='software']"
    xpath+="/*[local-name()='default_patterns']/text()"

    local default_patterns=$(xmllint --xpath "$xpath" "${SNAPSHOT_DIR}${file}")

    xpath="//*[local-name()='system_role']"
    xpath+="/*[local-name()='id']/text()"
    local role patterns
    while read role; do
	xpath="//*[local-name()='system_role'][*[local-name()='id']='"$role"']"
	xpath+="/*[local-name()='software']"
	xpath+="/*[local-name()='default_patterns']/text()"
	patterns=$(xmllint --xpath "$xpath" "${SNAPSHOT_DIR}${file}" 2> /dev/null)
	ROLES["$role"]="${patterns:-$default_patterns}"
    done <<< $(xmllint --xpath "$xpath" "${SNAPSHOT_DIR}${file}")
}

# Read all the repo aliases and the priority, reverse sorted by
# priority
load_repo_alias() {
    local regex='(?<=alias=").*?(?=")'
    regex+='|(?<=priority=").*?(?=")'
    regex+='|(?<=enabled=").*?(?=")'
    regex+='|(?<=<url>).*?(?=</url>)'

    # Get the repositories, remove the ones that have duplicated URL,
    # and sort by priority
    local line
    while read line; do
	line=($line)
	[ "${line[2]}" != "0" ] && REPOS+=("${line[0]} ${line[1]}")
    done <<< $(callext zypper --root {} --xmlout lr | re_findall "$regex" | xargs -n4 | LC_COLLATE=C sort -k4 | uniq -f3 | LC_COLLATE=C sort -k2 -r)
}

# Read all the build time of repo packages and store in a file
load_buildtime_for_repo() {
    local file="/var/cache/zypp/raw/$1/repodata/*primary.xml.gz"
    local regex='(?<=<name>).*(?=</name>)'
    regex+='|(?<=<arch>).*(?=</arch>)'
    regex+='|(?<=<version ).*?(?=/>)'
    regex+='|(?<=<time ).*?(?=/>)'

    local name arch line epoch ver rel file build
    while read name; do
	read arch
	read line; eval "$line"
	read line; eval "$line"
	[ ! -z "$name" ] && echo "${name}-${ver}-${rel}.${arch} $build" >> "$2"
    done <<< $(zcat $file | re_findall "$regex")
}

# Read all the build time of installed packages
load_buildtime() {
    local line
    while read line; do
	line=($line)
	BUILDTIME["${line[0]}"]="${line[1]}"
    done <<< $(callext rpm --root {} --query --all --queryformat='%{NVRA} %{BUILDTIME}\n')
}

# Return the timestamp of a repository from the metadata
timestamp_for_repo() {
    local file="/var/cache/zypp/raw/$1/repodata/repomd.xml"
    re_findall '(?<=<revision>).*?(?=</revision>)' "$file"
}

# Return 0 if Zypper will ignore recommended packages
only_requires() {
    local file="/etc/zypp/zypp.conf"
    callext grep --quiet '^solver.onlyRequires\s*=\s*true' "${SNAPSHOT_DIR}${file}"
    return #?
}

# Return 0 if Zypper allow vendor change
allow_vendor_change() {
    local file="/etc/zypp/zypp.conf"
    callext grep --quiet '^solver.allowVendorChange\s*=\s*true' "${SNAPSHOT_DIR}${file}"
    return #?
}

# Create a libsolv testcase for a role
testcase_for_role() {
    local arch="$(uname -m)"

    echo "system $arch rpm"

    local repo
    for repo in "${REPOS[@]}"; do
	repo=($repo)
	echo "repo ${repo[0]} ${repo[1]} solv /var/cache/zypp/solv/${repo[0]}/solv"
    done

    local flags=""
    only_requires && flags="ignorerecommended"
    allow_vendor_change && flags+=" allowvendorchange"
    [ ! -z "$flags" ] && echo "solverflags $flags"

    echo "job install name product:$(baseproduct)"

    local role
    for role in ${ROLES[$1]}; do
	echo "job install name pattern:$role"
    done
}

# Given a role, calculate the list of all packages and store it in a
# file
load_packages_for_role() {
    local testcase_file="$(mktemp)"

    testcase_for_role "$1" > "$testcase_file"

    # Execute in a subshell because of a testsolv bug, that do not
    # accept full path names
    (
	cd $(dirname "$testcase_file")
	testsolv $(basename "$testcase_file") | re_findall '(?<=  - ).*' | LC_COLLATE=C sort > "$2"
    )

    rm -f "$testcase_file"
}

# Calculate the manifest document using the repository information
base_manifest() {
    local pkg
    while read pkg; do
	grep "^$pkg " "$1" || echo "$pkg"
    done <"$2"
}

# Calculate the manifest document from the current system
system_manifest() {
    local pkg
    while read pkg; do
	if [ "${BUILDTIME["$pkg"]+_}" ]; then
	    echo "$pkg ${BUILDTIME["$pkg"]}"
	else
	    echo "$pkg"
	fi
    done <"$1"
}

# List the installed patterns by the user
installed_patterns() {
    callext zypper --root {} --disable-repositories se --installed-only --sort-by-name --type pattern \
	| re_findall '(?<=i\+ \| ).*?(?= )'
}

# List all the installed patterns (including hiddens one)
all_installed_patterns() {
    callext rpm --root {} --query --provides --whatprovides 'pattern()' \
	| re_findall '(?<=pattern\(\) = ).*' \
	| LC_COLLATE=C sort
}

# List the installed packages by the user
installed_packages() {
    callext zypper --root {} --disable-repositories se --installed-only --sort-by-name --type package \
	| re_findall '(?<=i\+ \| ).*?(?= )'
}

# List all installed packages
all_installed_packages() {
    callext rpm --root {} --query --all --queryformat='%{NAME}\n' \
	| LC_COLLATE=C sort
}

# List the extra patterns installed by the user
added_patterns() {
    # TODO The selinux one should be detected
    local exclude='microos_selinux'
    local inst_patterns="$(installed_patterns | grep --invert-match --extended-regexp "$exclude")"
    local role_patterns="$(echo "${ROLES[$1]}" | tr ' ' '\n' | LC_COLLATE=C sort)"

    LC_COLLATE=C comm -23 <(echo "${inst_patterns}") <(echo "${role_patterns}")
}

# List the patterns from the role that are not installed
removed_patterns() {
    local inst_patterns="$(all_installed_patterns)"
    local role_patterns="$(echo "${ROLES[$1]}" | tr ' ' '\n' | LC_COLLATE=C sort)"

    LC_COLLATE=C comm -23 <(echo "${role_patterns}") <(echo "${inst_patterns}")
}

# List the extra packages installed by the user
added_packages() {
    # TODO What others are installed by YaST?
    # TODO Maybe add them into packages_for_role_extended, or add them
    # in the configuration file
    local exclude='patterns-|kernel-|snapper|grub2|btrfsprogs'
    local inst_packages="$(installed_packages | grep --invert-match --extended-regexp "$exclude")"
    local role_packages="$(cat "$1" | rev | cut -d"-" -f3- | rev | LC_COLLATE=C sort)"

    LC_COLLATE=C comm -23 <(echo "${inst_packages}") <(echo "${role_packages}")
}

# List the packages from the role that are not installed
removed_packages() {
    local exclude='pattern:|product:'
    local inst_packages="$(all_installed_packages)"
    local role_packages="$(cat "$1" | grep --invert-match --extended-regexp "$exclude" | rev | cut -d"-" -f3- | rev | LC_COLLATE=C sort)"

    LC_COLLATE=C comm -23 <(echo "${role_packages}") <(echo "${inst_packages}")
}

# Calculate the Jaccard index for two sets
jaccard() {
    local union_card=$(echo "$1 $2" | tr ' ' '\n' | LC_COLLATE=C sort | uniq | wc -l)
    local intersection_card=$(echo "$1 $2" | tr ' ' '\n' | LC_COLLATE=C sort | uniq -D | uniq | wc -l)
    echo $((100 * $intersection_card / $union_card))
}

# Based on the installed patterns, find the installed role
find_closer_role() {
    local patterns="$(installed_patterns)"
    local max_index=0
    local max_role=
    local index=

    for role in "${!ROLES[@]}"; do
	index="$(jaccard "$patterns" "${ROLES["$role"]}")"
	if [ "$index" -gt "$max_index" ]; then
	    max_index="$index"
	    max_role="$role"
	fi
    done

    echo "$max_role"
}

# Wait for a series of PIDs
wait_all() {
    local pid
    for pid in "$@"; do
	wait "$pid"
    done
}

# Create the status file and all the assets to validate it in the
# future by the user
create_status_file() {
    # Start from a clean state
    rm -fr "$(dirname ${SNAPSHOT_DIR}${STATUS_FILE})"
    mkdir -p "$(dirname ${SNAPSHOT_DIR}${STATUS_FILE})"

    echo "DATE=\"$(date +"%Y-%m-%d %T")\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"
    echo "PRODUCT=\"$(baseproduct)\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"
    grep VERSION_ID "${SNAPSHOT_DIR}/etc/os-release" >> "${SNAPSHOT_DIR}${STATUS_FILE}"

    load_roles
    local role="$(find_closer_role)"
    echo "ROLE=\"$role\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"

    load_repo_alias

    local repo_buildtime_file="$(mktemp)"
    local repo
    local pids=()
    for repo in "${REPOS[@]}"; do
	repo=($repo)
	log_info "Parsing repo ${repo[0]}"
	# Do the load in the background, and store the data in the
	# file.  The OS will take care of synchronizing the file
	# access (or at least seems so)
	load_buildtime_for_repo "${repo[0]}" "$repo_buildtime_file" & pids+=($!)
    done
    load_buildtime
    wait_all "${pids[@]}"

    local packages_for_role_file="$(mktemp)"
    load_packages_for_role "$role" "$packages_for_role_file"

    base_manifest "$repo_buildtime_file" "$packages_for_role_file" > "${SNAPSHOT_DIR}${BASE_MANIFEST_FILE}"
    system_manifest "$packages_for_role_file" > "${SNAPSHOT_DIR}${SYSTEM_MANIFEST_FILE}"

    local base_manifest_digest="$(sha256sum "${SNAPSHOT_DIR}${BASE_MANIFEST_FILE}" | cut -d' ' -f1)"
    local system_manifest_digest="$(sha256sum "${SNAPSHOT_DIR}${SYSTEM_MANIFEST_FILE}" | cut -d' ' -f1)"

    echo "BASE_MANIFEST_DIGEST=\"$base_manifest_digest\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"
    echo "SYSTEM_MANIFEST_DIGEST=\"$system_manifest_digest\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"

    pids=()
    gzip "${SNAPSHOT_DIR}${BASE_MANIFEST_FILE}" & pids+=($!)
    gzip "${SNAPSHOT_DIR}${SYSTEM_MANIFEST_FILE}" & pids+=($!)

    local items=$(added_patterns "$role" | xargs)
    echo "ADDED_PATTERNS=\"$items\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"
    items=$(removed_patterns "$role" | xargs)
    echo "REMOVED_PATTERNS=\"$items\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"

    items=$(added_packages "$packages_for_role_file" | xargs)
    echo "ADDED_PACKAGES=\"$items\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"
    items=$(removed_packages "$packages_for_role_file" | xargs)
    echo "REMOVED_PACKAGES=\"$items\"" >> "${SNAPSHOT_DIR}${STATUS_FILE}"

    wait_all "${pids[@]}"

    rm -f "$repo_buildtime_file" "$packages_for_role_file"
}

# Get the snapshot number
snapshot_num() {
    local regex='(?<=<num>).*?(?=</num>)'
    re_findall "$regex" "${1}info.xml"
}

# Get the snapshot date, in the same format that the status file
snapshot_date() {
    local regex='(?<=<date>).*?(?=</date>)'
    re_findall "$regex" "${1}info.xml"
}

# Get the snapshot description line
snapshot_description() {
    local regex='(?<=<description>).*?(?=</description>)'
    re_findall "$regex" "${1}info.xml"
}

# Show the status of a snapshot
show_snapshot_status() {
    printf "%0.s-" $(seq 1 $(tput cols)); echo

    local ss_id=$(snapshot_num "$1")
    if [ "$CURRENT_SNAPSHOT_ID" -eq "$ss_id" ]; then
	ss_id+="*"
    elif [ "$DEFAULT_SNAPSHOT_ID" -eq "$ss_id" ]; then
	ss_id+="+"
    fi

    local ss_date="$(snapshot_date "$1")"
    local ss_description="$(snapshot_description "$1")"
    echo "Snapshot:          ${ss_id} [$(snapshot_date "$1")]"
    echo "Description:       $(snapshot_description "$1")"
    if [ -f "${1}snapshot${STATUS_FILE}" ]; then
	. "${1}snapshot${STATUS_FILE}"
	echo "Report date:       ${DATE}"
	echo "Version:           ${PRODUCT} ${VERSION_ID}"
	echo "Base OS hash:      ${BASE_MANIFEST_DIGEST}"
	echo "Effective OS hash: ${SYSTEM_MANIFEST_DIGEST}"
	[ ! -z "${ADDED_PATTERNS}" ] && echo "Added patterns:    ${ADDED_PATTERNS}"
	[ ! -z "${REMOVED_PATTERNS}" ] && echo "Removed patterns:  ${REMOVED_PATTERNS}"
	[ ! -z "${ADDED_PACKAGES}" ] && echo "Added packages:    ${ADDED_PACKAGES}"
	[ ! -z "${REMOVED_PACKAGES}" ] && echo "Removed packages:  ${REMOVED_PACKAGES}"
    else
	echo "(Missing status file)"
    fi
}


ORIG_ARGS=("$@")

parse_args() {
    while [ 1 ]; do
	if [ $# -eq 0 ]; then
	    break
	fi

	case "$1" in
	    cleanup)
		DO_CLEANUP_OVERLAYS=1
		DO_CLEANUP_SNAPSHOTS=1
		shift
		;;
	    cleanup-snapshots)
		DO_CLEANUP_SNAPSHOTS=1
		shift
		;;
	    cleanup-overlays)
		DO_CLEANUP_OVERLAYS=1
		shift
		;;
	    dist-upgrade|dup)
		DO_DUP=1
		ZYPPER_ARG="--no-cd dup"
		shift
		TELEM_CLASS="upgrade"
		;;
	    update|up)
		ZYPPER_ARG=up
		shift
		TELEM_CLASS="update"
		;;
	    patch)
		ZYPPER_ARG="--non-interactive-include-reboot-patches patch"
		shift
		TELEM_CLASS="patch"
		;;
	    ptf|pkg|package)
		TELEM_CLASS="package"
		shift
		if [ $# -eq 0 ]; then
		    usage 1
		fi
		# Interactively run installing PTFs
		ZYPPER_NONINTERACTIVE="${FORCE_NONINTERACTIVE:-}"
		case "$1" in
		    install|in)
			ZYPPER_ARG="install"
			shift
			;;
		    remove|rm)
			ZYPPER_ARG="remove"
			if [ -n "${ZYPPER_NONINTERACTIVE}" ]; then
		    ZYPPER_NONINTERACTIVE="-y"
			fi
			shift
			;;
		    update|up)
			ZYPPER_ARG="up"
			shift
			;;
		    *)
			usage 1;
			;;
		esac
		DO_CALLEXT=1

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

		while [ 1 ]; do
		    if [ $# -eq 0 ]; then
			break;
		    else
			ZYPPER_ARG_PKGS+=("$1");
			shift
		    fi
		done
		;;
	    migration)
		TELEM_CLASS="migration"
		__NO_RESET=0
		DO_MIGRATION=1
		ZYPPER_ARG="migration --no-snapshots --no-selfupdate"
		if [ -n "${FORCE_NONINTERACTIVE}" ]; then
		    # non interative option is named different for
		    # different commands ...
		    ZYPPER_NONINTERACTIVE="--non-interactive --auto-agree-with-licenses"
		else
		    ZYPPER_NONINTERACTIVE=""
		fi
		shift
		;;
	    bootloader)
		test -z "$TELEM_CLASS" && TELEM_CLASS="bootloader"
		REWRITE_BOOTLOADER=1
		REWRITE_GRUB_CFG=1
		shift
		;;
	    grub.cfg)
		test -z "$TELEM_CLASS" && TELEM_CLASS="bootloader"
		REWRITE_GRUB_CFG=1
		shift
		;;
	    shell)
		test -z "$TELEM_CLASS" && TELEM_CLASS="shell"
		RUN_SHELL=1
		shift
		if [ "$1" = "-c" ]; then
		    if [ -z "$2" ]; then
			SHELL_CMD="-"
		    else
			SHELL_CMD="$2"
			shift
		    fi
		    shift
		fi
		;;
	    initrd)
		test -z "$TELEM_CLASS" && TELEM_CLASS="initrd"
		REWRITE_INITRD=1
		REBUILD_KDUMP_INITRD=1
		shift
		;;
	    kdump)
		test -z "$TELEM_CLASS" && TELEM_CLASS="kdump"
		REBUILD_KDUMP_INITRD=1
		SETUP_KDUMP=2
		shift
		;;
	    reboot)
		REBOOT_AFTERWARDS=1
		shift
		;;
	    rollback)
		TELEM_CLASS="rollback"
		DO_ROLLBACK=1
		DO_SELF_UPDATE=0
		shift
		if [ $# -eq 1 ]; then
		    ROLLBACK_SNAPSHOT=$1
		    shift
		fi
		;;
	    apply)
		DO_APPLY=1
		shift
		;;
	    run)
		test -z "$TELEM_CLASS" && TELEM_CLASS="shell"
		DO_RUN=1
		shift

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

		RUN_CMD=("$@")
		break
		;;
	    setup-fips)
		test -z "$TELEM_CLASS" && TELEM_CLASS="fips"
		SETUP_FIPS=1
		shift
		;;
	    setup-kdump)
		test -z "$TELEM_CLASS" && TELEM_CLASS="setup-kdump"
		SETUP_KDUMP=1
		shift
		if [[ $1 == --crashkernel* ]]; then
		    if ! [[ $1 =~ ^--crashkernel=[[:digit:]]+,[[:digit:]]+ ]]; then
			echo "Invalid --crashkernel syntax"
			usage 1
		    fi
		    KDUMP_LOW="$(echo "$1" | cut -d '=' -f 2 | cut -d ',' -f 1)"
		    KDUMP_HIGH="$(echo "$1" | cut -d '=' -f 2 | cut -d ',' -f 2)"
		    shift
		fi
		;;
	    setup-selinux)
		test -z "$TELEM_CLASS" && TELEM_CLASS="selinux"
		SETUP_SELINUX=1
		shift
		;;
	    status)
		DO_STATUS=1
		DO_SELF_UPDATE=0
		shift
		while [ 1 ]; do
		    [ $# -eq 0 ] && break
		    case "$1" in
			last)
		    DO_STATUS_LAST=1
		    shift
		    ;;
			*)
		    usage 1;
		    ;;
		    esac
		done
		;;
	    -i|--interactive)
		ZYPPER_NONINTERACTIVE=""
		shift
		;;
	    -n|--non-interactive)
		FORCE_NONINTERACTIVE="${ZYPPER_NONINTERACTIVE}"
		shift
		;;
	    -c|--continue)
		# Check whether we got an optional snapshot number argument
		if [[ $2 =~ ^[0-9]+$ ]]; then
		    BASE_SNAPSHOT_ID="$2"
		    shift
		else
		    BASE_SNAPSHOT_ID="default"
		fi
		shift
		;;
	    --no-selfupdate)
		DO_SELF_UPDATE=0
		shift
		;;
	    -d|--drop-if-no-change)
		TUKIT_OPTS="${TUKIT_OPTS} --discard"
		shift
		;;
	    --quiet)
		VERBOSITY=1
		TUKIT_OPTS="${TUKIT_OPTS} --quiet"
		DRACUT_OPTS="${DRACUT_OPTS} --quiet"
		# ZYPPER_ARG handled below
		shift
		;;
	    register)
		DO_REGISTRATION=1
		shift

		if [ $# -eq 0 ]; then
		    usage 1
		fi
		if ! command -v SUSEConnect >/dev/null; then
		    echo "SUSEConnect does not exist on this system."
		    exit 1
		fi

		REGISTRATION_ARGS="$*";
		shift $#

		# A lot of commands won't change anything; discard snapshot then
		TUKIT_OPTS="${TUKIT_OPTS} --discard"
		;;
	    -h|--help)
		usage 0
		;;
	    --version)
		print_version
		;;
	    *)
		if [ $# -ge 1 ]; then
		    usage 1;
		fi
		;;
	esac
    done
}
parse_args "${ORIG_ARGS[@]}"

# Duplicate stdout before creating custom handlers
exec {origstdout}>&1
exec {origstderr}>&2

# Log stderr to log file in case anything goes wrong within transactional-update
exec 2> >(exec tee -i -a "${LOGFILE}" >&2)
if [ "${VERBOSITY}" -eq 1 ]; then
    exec 1>/dev/null
    if [ -n "${ZYPPER_ARG}" ]; then
	ZYPPER_ARG="--quiet ${ZYPPER_ARG}"
    fi
fi

# Setup FIPS
if [ "${SETUP_FIPS}" -eq 1 ]; then
    if [ -n "${ZYPPER_ARG}" -a "${ZYPPER_ARG}" != "install" ]; then
	log_error "ERROR: Cannot combine 'setup-fips' with zypper command '${ZYPPER_ARG}'"
	exit 1
    fi
    # Check if we need to install packages
    fipspattern="$(rpm -q --whatprovides 'pattern()' --provides | grep '^pattern() = fips$')"
    if [ -z "${fipspattern}" ]; then
	ZYPPER_ARG_PKGS+=("pattern() = fips")
    fi
    if [ ${#ZYPPER_ARG_PKGS[@]} -ne 0 ]; then
	ZYPPER_ARG="install"
    fi
    REWRITE_INITRD=1
    REBUILD_KDUMP_INITRD=1
fi

# Setup SELinux
if [ "${SETUP_SELINUX}" -eq 1 ]; then
    # Setting up SELinux requires several steps:
    # 1. Make sure the policies are installed
    # 2. Adjust /etc/default/grub
    # 3. Adjust /etc/selinux/config
    # 4. Rebuild grub.cfg and initrd

    if [ -n "${ZYPPER_ARG}" -a "${ZYPPER_ARG}" != "install" ]; then
	log_error "ERROR: Cannot combine 'setup-selinux' with zypper command '${ZYPPER_ARG}'"
	exit 1
    fi
    # Check if we need to install packages
    for pkg in selinux-policy-targeted container-selinux; do
	rpm -q --quiet ${pkg} || ZYPPER_ARG_PKGS+=("${pkg}")
    done
    if [ ${#ZYPPER_ARG_PKGS[@]} -ne 0 ]; then
	ZYPPER_ARG="install"
    fi
    REWRITE_INITRD=1
    REBUILD_KDUMP_INITRD=1

    # Make sure /var/lib/selinux exists, else installing the
    # Policy will fail
    test -d /var/lib/selinux || mkdir -p /var/lib/selinux
fi

# Setup kdump
if [ ${SETUP_KDUMP} -eq 1 ]; then
    if [ -n "${ZYPPER_ARG}" -a "${ZYPPER_ARG}" != "install" ]; then
        log_error "ERROR: Cannot combine 'setup-kdump' with zypper command '${ZYPPER_ARG}'"
        exit 1
    fi
    # Check if we need to install packages
    for pkg in kdump; do
        rpm -q --quiet ${pkg} || ZYPPER_ARG_PKGS+=("${pkg}")
    done
    if [ ${#ZYPPER_ARG_PKGS[@]} -ne 0 ]; then
        ZYPPER_ARG="install"
    fi
    REBUILD_KDUMP_INITRD=1
fi

# If no commands were given, use default from config
if [ -z "${ZYPPER_ARG}" -a -z "${TELEM_CLASS}" -a "${REBOOT_AFTERWARDS}" -eq 0 \
	-a "${DO_REGISTRATION}" -eq 0 -a "${DO_CLEANUP_OVERLAYS}" -eq 0 \
	-a "${DO_CLEANUP_SNAPSHOTS}" -eq 0 -a "${DO_APPLY}" -eq 0 ]; then
    parse_args "${UPDATE_METHOD}"
fi

# Prevent running transactional-update inside transactional-update
if [ -n "${TRANSACTIONAL_UPDATE}" ]; then
    log_error "Cannot call transactional-update from within transactional-update environment!"
    exit 1
fi

# Store current reboot level for reset in case of failure and delete flag file
if [ -e "${NEEDS_RESTARTING_FILE}" ]; then
    REBOOT_LEVEL_PREV="$(cat "${NEEDS_RESTARTING_FILE}")"
    rm "${NEEDS_RESTARTING_FILE}"
else
    REBOOT_LEVEL_PREV=none
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
    if [ $? -ne 0 ]; then
	log_error "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
    pushd "${TA_UPDATE_TMPFILE}" >/dev/null
    zypper --non-interactive --pkg-cache-dir "${TA_UPDATE_TMPFILE}" download libtukit4 tukit
    find . -name *.rpm -exec sh -c 'rpm2cpio {} | cpio -idmv 2>/dev/null' \;
    popd >/dev/null
    export LD_LIBRARY_PATH="${TA_UPDATE_TMPFILE}/usr/lib64:${TA_UPDATE_TMPFILE}/usr/lib"
    if "${TA_UPDATE_TMPFILE}"/usr/sbin/tukit --version >/dev/null; then
	# tukit is executable - use new version
	export PATH="${TA_UPDATE_TMPFILE}/usr/sbin:${PATH}"
    elif [ -f /usr/sbin/tukit ]; then
	log_info "WARNING: New tukit version cannot be executed - using the old one."
	unset LD_LIBRARY_PATH
    else
	log_info "WARNING: tukit cannot be executed - falling back to old transactional-update version."
	unset LD_LIBRARY_PATH
	exec transactional-update --no-selfupdate "${ORIG_ARGS[@]}"
    fi
fi

# Clean up in case the application is interrupted
trap 'log_error "Received termination signal..." && quit 1' HUP INT QUIT TERM

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

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

SNAPPER_VERSION=`snapper --version | head -1 | cut -d ' ' -f 2`
if [ -n "${BASE_SNAPSHOT_ID}" -a $(zypper --terse versioncmp $SNAPPER_VERSION 0.8.4) -lt 0 ]; then
    log_error "ERROR: snapper >= 0.8.4 required for --continue option!"
    log_info "transactional-update finished"
    exit 1
fi

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."
    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 "${DO_MIGRATION}" -eq 0 -a ${ZYPPER_AUTO_IMPORT_KEYS} -eq 1 ]; then
    ZYPPER_ARG="--gpg-auto-import-keys ${ZYPPER_ARG}"
fi

BOOTED_SNAPSHOT_ID=`grep subvol=/@/.snapshots/ /proc/mounts | grep "/ btrfs" | sed -e 's|.*.snapshots/\(.*\)/snapshot.*|\1|g'`
CURRENT_SNAPSHOT_ID=`findmnt --target /usr --raw --noheadings --output FSROOT | tail -n 1 | 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 [ -z "${BASE_SNAPSHOT_ID}" ]; then
    BASE_SNAPSHOT_ID="${CURRENT_SNAPSHOT_ID}"
elif [ "${BASE_SNAPSHOT_ID}" = "default" ]; then
    BASE_SNAPSHOT_ID="${DEFAULT_SNAPSHOT_ID}"
fi

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

    if [ "${ROLLBACK_SNAPSHOT}" = "last" ]; then
	if [ -n "${LAST_WORKING_SNAPSHOTS}" ]; then
	    ROLLBACK_SNAPSHOT=${LAST_WORKING_SNAPSHOTS%% *}
	else
	    log_error "No last working snapshot saved; please use 'snapper list' for manual selection"
	fi
    elif [ ${ROLLBACK_SNAPSHOT} -eq 0 -o ${ROLLBACK_SNAPSHOT} -eq ${CURRENT_SNAPSHOT_ID} ]; then
	ROLLBACK_SNAPSHOT=${CURRENT_SNAPSHOT_ID}
	NEED_REBOOT=0
    fi

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

    tukit rollback "${ROLLBACK_SNAPSHOT}"
    if [ $? -ne 0 ]; then
	log_error "ERROR: Rollback to snapshot $ROLLBACK_SNAPSHOT failed!"
	quit 1
    fi
    if [ ${RO_ROOT} == "true" ]; then
	# Create the trigger to re-register the system as new version after next
	# reboot.
	check_registration_on_next_reboot
	# Remove possible cleanup algo and re-add to list
	if ! ( echo "${LAST_WORKING_SNAPSHOTS} ${UNUSED_SNAPSHOTS}" | grep --word-regexp --quiet "${ROLLBACK_SNAPSHOT}" ); then
	    UNUSED_SNAPSHOTS="${UNUSED_SNAPSHOTS} ${ROLLBACK_SNAPSHOT}"
	    save_state_file 0
	fi

	if [ ${NEED_REBOOT} -eq 0 ]; then
	    rm -f "${NEEDS_RESTARTING_FILE}"
	else
	    write_needs_restarting "${REBOOT_LEVEL}" "${REBOOT_ASSOCS[${ROLLBACK_SNAPSHOT}]}"
	fi
    else
	NEED_REBOOT=1
    fi
    if [ ${NEED_REBOOT} -eq 1 ]; then
	if [ ${DO_APPLY} -eq 1 ]; then
	    do_apply
	else
	    log_error "Please reboot to finish rollback!"
	fi
    fi
    log_info "transactional-update finished"
    exit 0
fi

if [ "${DO_STATUS}" -eq 1 ]; then
    if [ "${EXPERIMENTAL_STATUS}" -eq 1 ]; then
        for snapshot in $(ls -d /.snapshots/*/ | cut -d '/' -f 3 | sort --reverse --numeric-sort); do
	    show_snapshot_status "/.snapshots/$snapshot/"
	    [ "${DO_STATUS_LAST}" -eq 1 ] && break
	done
    else
	echo "The status command is disabled by default as it is marked as experimental"
    fi
    exit 0
fi

#
# Cleanup part: make sure old root file systems will be removed after they are no longer active.
#
if [ ${DO_CLEANUP_SNAPSHOTS} -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, the booted one (still required for health-checker rollbacks) or the
    # active one.
    if [ -n "${LAST_WORKING_SNAPSHOTS}" ]; then
	for snap in ${LAST_WORKING_SNAPSHOTS}; do
	    if [ ${CURRENT_SNAPSHOT_ID} -ne ${snap} -a ${BOOTED_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).
    # As snapper list output differs between versions search for the correct
    # rows first
    for snap in $(snapper list | awk -F '|' '
	    NR==1 { for(i=1; i<=NF; i++) { gsub(/^ +/, "", $i); gsub(/ +$/, "", $i); fields[$i]=i }}
	    NR>2 { if($fields["Userdata"] ~ "transactional-update-in-progress=yes") { print $fields["#"] }}'); do
	UNUSED_SNAPSHOTS="${UNUSED_SNAPSHOTS} ${snap}"
    done

    # Always try to cleanup all snapshots; only the current snapshot, the
    # one which the system was booted from and some parts of the system
    # may still use, 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 ${BOOTED_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 [ -e /.snapshots/${snap} ]; then
			# Keep the snapshot in the list
			_new_unused="${snap} ${_new_unused}"
		    fi
		fi
	    elif [ ${snap} -ne ${CURRENT_SNAPSHOT_ID} -a ${snap} -ne ${BOOTED_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 [ ${DO_CLEANUP_OVERLAYS} -eq 1 ]; then
    # Clean up old unused overlays
    if [ ${RO_ROOT} == "true" ]; then
	snapshots="$(ls /.snapshots/*/snapshot/etc/fstab{,.sys} 2>/dev/null)"
	for overlay in /var/lib/overlay/*; do
	    if [ -e ${overlay} ] && [ -n "${snapshots}" ]; then
		grep -q "${overlay}" ${snapshots}
		if [ $? -eq 1 ]; then
		    log_info "Deleting unused overlay ${overlay}"
		    rm -r "${overlay}"
		fi
	    fi
	done
    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 ${DO_RUN} -eq 1 \
    -o ${REWRITE_BOOTLOADER} -eq 1 -o ${DO_REGISTRATION} -eq 1 ]; then

    if [ -z "${ZYPPER_NONINTERACTIVE}" -a "${DEFAULT_SNAPSHOT_ID}" -ne "${BASE_SNAPSHOT_ID}" ]; then
	log_info "WARNING: You are creating a snapshot from a different base (${BASE_SNAPSHOT_ID}) than the"
	log_info "         current default snapshot (${DEFAULT_SNAPSHOT_ID})."
	if [ "${BASE_SNAPSHOT_ID}" -eq "${CURRENT_SNAPSHOT_ID}" ]; then
	    log_info "         If you want to continue a previous snapshot use the --continue"
	    log_info "         option, otherwise the previous changes will be discarded."
	fi
    fi

    output="`tukit ${TUKIT_OPTS} -c"${BASE_SNAPSHOT_ID}" open |& tee -a ${LOGFILE}`"
    log_info "$output"
    SNAPSHOT_ID=`echo "${output}" | grep -e "^ID:" | cut -d " " -f 2-`
    if [ -z ${SNAPSHOT_ID} ]; then
	quit 1
    fi
    SNAPSHOT_DIR="/.snapshots/${SNAPSHOT_ID}/snapshot"

    # 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}"

    if [ ${DO_REGISTRATION} -eq 1 ]; then
	tukit ${TUKIT_OPTS} callext ${SNAPSHOT_ID} SUSEConnect --root {} ${REGISTRATION_ARGS} |& tee -a ${LOGFILE} 1>&${origstdout}
	REBOOT_LEVEL="soft-reboot"
    fi

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

	log_info "Calling zypper ${ZYPPER_ARG}"
	if [ ${DO_MIGRATION} -eq 1 ]; then
	    # transactional-update migration
	    export DISABLE_RESTART_ON_UPDATE=yes
	    tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" zypper ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} 1>&${origstdout}
	    RETVAL=${PIPESTATUS[0]}
	else
	    if [ ${DO_CALLEXT} -eq 1 ]; then
		zypper_cmd="tukit ${TUKIT_OPTS} callext ${SNAPSHOT_ID} zypper -R {}"
	    else
		zypper_cmd="tukit ${TUKIT_OPTS} call ${SNAPSHOT_ID} zypper"
	    fi
	    # Check if there are updates at all.
	    TMPFILE=`mktemp ${TMPDIR}/transactional-update.XXXXXXXXXX`
	    ${zypper_cmd} --xmlout ${ZYPPER_ARG} -y --auto-agree-with-product-licenses --dry-run "${ZYPPER_ARG_PKGS[@]}" > ${TMPFILE}
	    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'`
	    NUM_OF_UPDATES=`grep "install-summary.*packages-to-change" ${TMPFILE} | sed -e 's|.*install-summary.*packages-to-change=\"\([^"]*\)\".*|\1|g'`
	    INCLUDES_KERNEL_PACKAGES=`grep 'solvable.*type="package"' ${TMPFILE} | grep 'name="kernel-'`
	    # Workaround for broken postuninstall
	    if grep 'solvable.*type="package"' ${TMPFILE} | grep 'name="libfdisk1"' | grep -q -e 'edition-old="2\.38\.1-[78]\.'; then
		log_info "Applying workaround for broken libfdisk1"
		tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" rpm -e --justdb --nodeps libfdisk1
	    fi
	    rm -f ${TMPFILE}
	    if [ "${NUM_OF_UPDATES}" = "0" ] || [ -z "${NUM_OF_UPDATES}" -a "${PACKAGE_UPDATES}" = "0" -a "${SIZE_OF_UPDATES}" = "0" ]; then
		log_info "zypper: nothing to update"
		quit 0
	    fi

	    export DISABLE_RESTART_ON_UPDATE=yes
	    ${zypper_cmd} ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} 1>&${origstdout}
	    RETVAL=${PIPESTATUS[0]}
	    if [ \( $RETVAL -eq 0 -o $RETVAL -eq 102 -o $RETVAL -eq 103 \) -a -n "${INCLUDES_KERNEL_PACKAGES}" ]; then
		${zypper_cmd} -n purge-kernels |& tee -a ${LOGFILE}
	    fi
	fi
	# in case of migration, we need to do a little bit more:
	if [ ${DO_MIGRATION} -eq 1 ]; then
	    # Reset registration until reboot. Needed in both cases,
	    # whether an error occured or whether we had success.
	    test -x /usr/sbin/SUSEConnect && /usr/sbin/SUSEConnect --rollback
	    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
	fi

	# Create the status for the new snapshot, before closing it
	if [ "${EXPERIMENTAL_STATUS}" -eq 1 ]; then
	    create_status_file
	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 ${SNAPSHOT_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 ${SNAPSHOT_DIR}/etc/os-release
	    if [ $? -ne 0 -a -x /usr/sbin/grub2-mkconfig ]; then
	        REWRITE_GRUB_CFG=1
	    fi
	    source <(grep VERSION_ID ${SNAPSHOT_DIR}/etc/os-release)
	else
	    log_error "ERROR: zypper ${ZYPPER_ARG} on ${SNAPSHOT_DIR} failed with exit code ${RETVAL}!"
	    if [ -n "${ZYPPER_NONINTERACTIVE}" ]; then
		log_error "Use '--interactive' for manual problem resolution."
	    fi
	    EXITCODE=1
	fi
	# If zypp-boot-plugin is available and didn't return anything, then soft-reboot is enough
	if rpm --quiet -q zypp-boot-plugin && [ ! -e "${NEEDS_RESTARTING_FILE}" ] ; then
	    REBOOT_LEVEL="soft-reboot"
	fi
    fi

    if [ -d /var/lib/overlay/${SNAPSHOT_ID} ]; then
	ETC_BASE="/var/lib/overlay/${SNAPSHOT_ID}"
    else
	ETC_BASE="${SNAPSHOT_DIR}"
    fi
    if [ ${SETUP_FIPS} -eq 1 ]; then
	# Adjust grub configuration

	# Check if we don't have selinux already enabled.
	grep ^GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub | grep -q -w fips || \
	    tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" sed -i -e 's|\(^GRUB_CMDLINE_LINUX_DEFAULT=.*\)"|\1 fips=1"|g' "/etc/default/grub"
	REWRITE_GRUB_CFG=1
    fi
    if [ ${SETUP_SELINUX} -eq 1 ]; then
	# Adjust grub configuration

	# Check if we don't have selinux already enabled.
	grep ^GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub | grep -q -w security=selinux || \
	    tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" sed -i -e 's|\(^GRUB_CMDLINE_LINUX_DEFAULT=.*\)"|\1 security=selinux selinux=1"|g' "/etc/default/grub"
	REWRITE_GRUB_CFG=1

	if tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" [ ! -e "/etc/selinux/config" ]; then
	    log_error "ERROR: /etc/selinux/config does not exist!"
	    EXITCODE=1
	fi
	# Adjust selinux config
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" sed -i -e 's|^SELINUX=.*|SELINUX=enforcing|g' \
	    -e 's|^SELINUXTYPE=.*|SELINUXTYPE=targeted|g' \
	    "/etc/selinux/config"

	# Move an /.autorelabel file from initial installation to writeable location
	test -f ${SNAPSHOT_DIR}/.autorelabel && mv ${SNAPSHOT_DIR}/.autorelabel ${ETC_BASE}/etc/selinux/.autorelabel
    fi

    if [ ${SETUP_KDUMP} -eq 1 ]; then
	if [ -z "${KDUMP_LOW}" ]; then
	    KDUMP_CALIBRATION="$(tukit -q call "${SNAPSHOT_ID}" kdumptool calibrate)"
	    KDUMP_LOW="$(echo "$KDUMP_CALIBRATION" | grep ^Low: | cut -d ' ' -f 2)"
	    KDUMP_HIGH="$(echo "$KDUMP_CALIBRATION" | grep ^High: | cut -d ' ' -f 2)"
	fi

	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" sed -i -e '/^GRUB_CMDLINE_LINUX_DEFAULT=/s/ *crashkernel[^ "]\+//g' -e 's|\(^GRUB_CMDLINE_LINUX_DEFAULT=.*\)"|\1 crashkernel='${KDUMP_LOW}'M,low crashkernel='${KDUMP_HIGH}'M,high"|g' "/etc/default/grub"
	REWRITE_GRUB_CFG=1

        tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" systemctl enable kdump
    fi

    if [ ${REWRITE_INITRD} -eq 1 ]; then
	log_info "Creating new initrd"
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" dracut ${DRACUT_OPTS} --force --regenerate-all |& tee -a ${LOGFILE} 1>&${origstdout}
	if [ $? -ne 0 ]; then
	    log_error "ERROR: initrd creation failed!"
	    EXITCODE=1
	else
	    REBUILD_KDUMP_INITRD=1
	fi
	REBOOT_LEVEL="kexec"
    fi

    if [ ${REBUILD_KDUMP_INITRD} -eq 1 ]; then
	log_info "Trying to rebuild kdump initrd"
	rebuild_kdump_initrd ${SNAPSHOT_ID}
	if [ $? -ne 0 ]; then
	    log_error "ERROR: kdump initrd creation failed!"
	    EXITCODE=1
	fi
	REBOOT_LEVEL="soft-reboot"
    fi

    if [ ${REWRITE_GRUB_CFG} -eq 1 ]; then
	log_info "Creating a new grub2 config"
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash -c "/usr/sbin/grub2-mkconfig > /boot/grub2/grub.cfg" |& tee -a ${LOGFILE} 1>&${origstdout}
	if [ $? -ne 0 ]; then
	    log_error "ERROR: grub2-mkconfig failed!"
	    EXITCODE=1;
        else
            if [ -x /usr/sbin/selinuxenabled ] && /usr/sbin/selinuxenabled ; then
                chcon --reference /boot/grub2/grub.cfg "${SNAPSHOT_DIR}/boot/grub2/grub.cfg"
            fi
	fi
    fi

    if [ ${REWRITE_BOOTLOADER} -eq 1 ]; then
	log_info "Writing new bootloader"
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" /sbin/pbl --install |& tee -a ${LOGFILE} 1>&${origstdout}
	if [ $? -ne 0 ]; then
	    log_error "ERROR: /sbin/pbl --install failed!"
	    EXITCODE=1;
	fi
    fi

    if [ ${DO_RUN} -eq 1 ]; then
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" "${RUN_CMD[@]}" |& tee -a ${LOGFILE} 1>&${origstdout}
    fi

    if [ ${RUN_SHELL} -eq 1 ]; then
	log_to_stdout "Opening chroot in snapshot ${SNAPSHOT_ID}, continue with 'exit'"
        export PS1="transactional update # "
	tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash 1>&${origstdout} 2>&${origstderr}
    fi

    if [ -x /usr/sbin/selinuxenabled ] && /usr/sbin/selinuxenabled ; then
        chcon --reference /etc "${ETC_BASE}/etc"
        chcon --reference /etc/fstab "${ETC_BASE}/etc/fstab"
    fi

    teardown

    # Somersault:
    if [ $EXITCODE -eq 0 ]; then
	# 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 dracut scripts
	if [ ${RO_ROOT} = "true" ]; then
	    echo "EXPECTED_SNAPSHOT_ID=${SNAPSHOT_ID}" > "${NEW_SNAPSHOT_FLAG}"
	    echo "PREV_SNAPSHOT_ID=${CURRENT_SNAPSHOT_ID}" >> "${NEW_SNAPSHOT_FLAG}"
	fi
	tukit ${TUKIT_OPTS} close "${SNAPSHOT_ID}" |& tee -a ${LOGFILE}
    fi

    # If --drop-if-no-change is used, then the snapshot may not exist any more;
    # the remaining code is not applicable in this case.
    if [ ! -e "${SNAPSHOT_DIR}/etc/fstab" -a ${EXITCODE} -eq 0 ]; then
	SNAPSHOT_ID=
	quit 0
    fi

    # Check for installation artefacts: Packages may have created files in
    # directories outside of the root file system; these files will not be
    # visible in the actual system as they are shadowed by the real mount
    # points, so warn the user
    searchdirs=""
    # Filter out commented lines and swap partition
    for mountdir in $(awk '$1 !~ "^#.*" && $2 ~ "^/.+" { print $2 }' ${SNAPSHOT_DIR}/etc/fstab); do
	searchdirs+="${SNAPSHOT_DIR}${mountdir} "
    done
    if [ -n "${searchdirs}" ]; then
	filelist="$(find ${searchdirs} -cnewer ${LOCKFILE} -not -type d 2>/dev/null | grep -v "${SNAPSHOT_DIR}/etc")"
	# Filter acceptable hits
	whitelist=""
	for wlentry in "${NON_ROOTFS_WHITELIST[@]}"; do
	    whitelist+="${SNAPSHOT_DIR}${wlentry}\|"
	done
	filelist="$(echo "$filelist" | grep -v "^\(${whitelist::-2}\)")"

	if [ -n "$filelist" ]; then
	    log_info
	    log_info "Warning: The following files were changed in the snapshot, but are shadowed by"
	    log_info "other mounts and will not be visible to the system:"
	    log_info "${filelist}"
	fi
    fi

    if [ ${EXITCODE} -ne 0 ]; then
	quit ${EXITCODE}
    fi

    write_needs_restarting "${REBOOT_LEVEL}" "${REBOOT_ASSOCS[${BASE_SNAPSHOT_ID}]}"
    rebootmethod="$(cat "${NEEDS_RESTARTING_FILE}")"
    if ! { [ $REBOOT_AFTERWARDS -eq 1 ] || [ $DO_APPLY -eq 1 -a "${rebootmethod}" == "soft-reboot" ]; }; then
	log_info ""
	log_info "Please reboot your machine to activate the changes and avoid data loss."
    fi

    if [ "${DEFAULT_SNAPSHOT_ID}" -ne "${BASE_SNAPSHOT_ID}" ]; then
	log_info ""
	log_info "WARNING: This snapshot has been created from a different base (${BASE_SNAPSHOT_ID})"
	log_info "         than the previous default snapshot (${DEFAULT_SNAPSHOT_ID}) and does not"
	log_info "         contain the changes from the latter."
	log_info ""
    fi

    log_info "New default snapshot is #${SNAPSHOT_ID} (${SNAPSHOT_DIR})."
fi

if [ ${DO_APPLY} -eq 1 -a ${EXITCODE} -eq 0 ]; then
    do_apply
fi

log_info "transactional-update finished"

if [ ${EXITCODE} -eq 0 ]; then
    if [ $REBOOT_AFTERWARDS -eq 1 -a -e "${NEEDS_RESTARTING_FILE}" ]; then
	trap '-' HUP INT QUIT TERM
	case "$REBOOT_METHOD" in
	    auto)
		tukit reboot auto |& tee -a ${LOGFILE}
		;;
	    kured)
		tukit reboot kured |& tee -a ${LOGFILE}
		;;
	    rebootmgr)
		tukit reboot rebootmgr |& tee -a ${LOGFILE}
		;;
	    notify)
		tukit reboot notify |& tee -a ${LOGFILE}
		;;
	    systemd)
		tukit reboot systemd |& tee -a ${LOGFILE}
		;;
	    kexec)
		tukit reboot kexec |& tee -a ${LOGFILE}
		;;
	    none)
		tukit reboot none |& tee -a ${LOGFILE}
		;;
	    *)
	        log_info "Unsupported reboot method, falling back to 'auto'; please"
	        log_info "check your configuration in ${CONFFILE}."
		tukit reboot auto |& tee -a ${LOGFILE}
	        ;;
	esac
    fi
fi

exit $EXITCODE
