#!/bin/bash
# $Id$
#
# Relax-and-Recover
#
#    Relax-and-Recover 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 3 of the License, or
#    (at your option) any later version.

#    Relax-and-Recover 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 Relax-and-Recover; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# Authors:
# Schlomo Schapiro <rear at schlomo.schapiro dot org> [GSS]
# Gratien D'haese  <gdha at sourceforge dot net> [GD]
# Jeroen Hoekx <jeroen.hoekx at hamok dot be> [JH]
# Dag Wieers <dag at wieers dot com> [DAG]
# Johannes Meixner <jsmeix at suse dot de> [jsmeix]

# Usually functions belong to the library scripts (i.e. $SHARE_DIR/lib/*.sh),
# but these 2 are exceptions on the rule because the complementary global functions
# to get and apply bash flags and options commands are needed from the very beginning
# (the usual functions are sourced at "Include functions" below):
function get_bash_flags_and_options_commands () {
    # Output bash commands that set the currently set flags
    # in reverse ordering to have "set ... xtrace" (i.e. "set +/- x") first
    # so that this flag is set first via apply_bash_flags_and_options_commands
    # which results better matching logging output in debugscripts mode:
    set +o | tac | tr '\n' ';'
    # Output bash commands that set the currently set options:
    shopt -p | tr '\n' ';'
}
# This function exists only to provide a syntactically matching counterpart of get_bash_flags_and_options_commands:
function apply_bash_flags_and_options_commands () {
    # Must be called with $1 that is the output of a previous get_bash_flags_and_options_commands:
    eval "$1"
}
# At the very beginning save initial bash flags and options in a global readonly variable
# so that exactly the initial bash flags and options can be restored if needed via
#   apply_bash_flags_and_options_commands "$INITIAL_BASH_FLAGS_AND_OPTIONS_COMMANDS"
readonly INITIAL_BASH_FLAGS_AND_OPTIONS_COMMANDS="$( get_bash_flags_and_options_commands )"

# Enable as needed for development of fail-safe code:
# set -ue -o pipefail
# set -xue -o pipefail
# set -xvue -o pipefail
# Cf. the Relax-and-Recover Coding Style
# https://github.com/rear/rear/wiki/Coding-Style
# that reads (excerpt - dated 18. Nov. 2015):
# "TODO Use set -eu to die from unset variables and unhandled errors"

# Versioning
readonly PRODUCT="Relax-and-Recover"
readonly PROGRAM=${0##*/}
readonly VERSION=1.19-git201610111647
readonly RELEASE_DATE="2016-10-11"

# Used in framework-functions.sh to calculate the time spent executing rear:
readonly STARTTIME=$SECONDS

# Provide the command line options in an array so that other scripts may read them:
readonly CMD_OPTS=( "$@" )

# Allow workflows to set the exit code to a different value (not "readonly"):
EXIT_CODE=0

# Find out if we're running from checkout
REAR_DIR_PREFIX=""
readonly SCRIPT_FILE="$( readlink -f $( type -p "$0" || echo "$0" ) )"
if test "$SCRIPT_FILE" != "$( readlink -f /usr/sbin/$PROGRAM )" ; then
    REAR_DIR_PREFIX=${SCRIPT_FILE%/usr/sbin/$PROGRAM}
fi
readonly REAR_DIR_PREFIX

# Program directories - they must be set here. Everything else is then dynamic.
# Not yet readonly here because they are set via the /etc/rear/rescue.conf file
# in the recovery system that is sourced by the rear command in recover mode
# and CONFIG_DIR can also be changed via '-c' command line option:
SHARE_DIR="/usr/share/rear"
CONFIG_DIR="/etc/rear"
VAR_DIR="/var/lib/rear"
LOG_DIR="$REAR_DIR_PREFIX/var/log/rear"

# Generic global variables that are not meant to be configured by the user
# (i.e. that do not belong into usr/share/rear/conf/default.conf),
# see https://github.com/rear/rear/issues/678
# and https://github.com/rear/rear/issues/708
# Location of the disklayout.conf file created by savelayout
# (no readonly DISKLAYOUT_FILE because it is also set in checklayout-workflow.sh and mkbackuponly-workflow.sh):
DISKLAYOUT_FILE="$VAR_DIR/layout/disklayout.conf"
# Location of the root of the filesystem tree of the target system
# in particular the root of the filesystem tree of the to-be-recovered-system in the recovery system
# i.e. the mountpoint in the recovery system where the filesystem tree of the to-be-recovered-system is mounted:
readonly TARGET_FS_ROOT="/mnt/local"

# Initialize defaults: empty value means "false"/"no":
DEBUG=""
DEBUGSCRIPTS=""
DEBUGSCRIPTS_ARGUMENT="x"
KEEP_BUILD_DIR=""
KERNEL_VERSION=""
RECOVERY_MODE=""
STEPBYSTEP=""
SIMULATE=""
VERBOSE=""
WORKFLOW=""

# Parse options
help_note_text="Use '$PROGRAM --help' or 'man $PROGRAM' for more information."
readonly OPTS="$( getopt -n $PROGRAM -o "c:dDhsSvVr:" -l "help,version,debugscripts:" -- "$@" )"
if test $? -ne 0 ; then
    echo "$help_note_text"
    exit 1
fi
eval set -- "$OPTS"
while true ; do
    case "$1" in
        (-h|--help)
            WORKFLOW="help"
            ;;
        (-V|--version)
            echo -e "$PRODUCT $VERSION / $RELEASE_DATE"
            exit 0
            ;;
        (-v)
            VERBOSE=1
            ;;
        (-c)
            if [[ "$2" == -* ]] ; then
                # When the item that follows '-c' starts with a '-'
                # it is considered to be the next option and not an argument for '-c':
                echo "-c requires an argument."
                echo "$help_note_text"
                exit 1
            fi
            CONFIG_DIR="$2"
            shift
            ;;
        (-d)
            DEBUG=1
            VERBOSE=1
            ;;
        (-D)
            DEBUGSCRIPTS=1
            ;;
        (--debugscripts)
            DEBUG=1
            VERBOSE=1
            DEBUGSCRIPTS=1
            if [[ "$2" == -* ]] ; then
                # When the item that follows '--debugscripts' starts with a '-'
                # it is considered to be the next option and not an argument for '--debugscripts':
                echo "--debugscripts requires an argument."
                echo "$help_note_text"
                exit 1
            fi
            DEBUGSCRIPTS_ARGUMENT="$2"
            shift
            ;;
        (-s)
            SIMULATE=1
            VERBOSE=1
            ;;
        (-S)
            STEPBYSTEP=1
            ;;
        (-r)
            if [[ "$2" == -* ]] ; then
                # When the item that follows '-r' starts with a '-'
                # it is considered to be the next option and not an argument for '-r':
                echo "-r requires an argument."
                echo "$help_note_text"
                exit 1
            fi
            KERNEL_VERSION="$2"
            shift
            ;;
        (--)
            shift
            break
            ;;
        (-*)
            echo "$PROGNAME: unrecognized option '$option'"
            echo "$help_note_text"
            exit 1
            ;;
        (*)
            break
            ;;
    esac
    shift
done

# Set workflow to first command line argument or to "help" as fallback:
if test -z "$WORKFLOW" ; then
    # Usually workflow is now in $1 (after the options and its arguments were shifted above)
    # but when rear is called without a workflow there exists no $1 here so that
    # an empty default value is used to avoid 'set -eu' error exit if $1 is unset:
    if test "${1:-}" ; then
        # Not "$1" to get rid of compound commands:
        WORKFLOW=$1
        shift
    else
        WORKFLOW=help
    fi
fi

# Keep the remaining command line arguments to feed to the workflow:
ARGS=( "$@" )

# The following workflows are always verbose:
case "$WORKFLOW" in
    (validate|dump|shell|recover)
        VERBOSE=1
        ;;
esac

# Mark variables that are not meant to be changed later as constants (i.e. readonly).
# Exceptions here:
# CONFIG_DIR because "rear recover" sets it in /etc/rear/rescue.conf in the recovery system
# WORKFLOW because for the udev workflow WORKFLOW is set to the actual workflow
# KERNEL_VERSION because if empty it is set when sourcing config files below
# VERBOSE because there is a special "VERBOSE=1" setting below:
readonly ARGS
readonly DEBUG DEBUGSCRIPTS DEBUGSCRIPTS_ARGUMENT
readonly SIMULATE STEPBYSTEP

# The udev workflow is a special case because it is only a wrapper workflow
# that calls an actual workflow that is specified as $UDEV_WORKFLOW
# (e.g. the default UDEV_WORKFLOW is "mkrescue" in default.conf)
# and then WORKFLOW is set to the actual workflow in udev-workflow.sh
# so that WORKFLOW cannot be set readonly for the udev workflow:
test "$WORKFLOW" = "udev" || readonly WORKFLOW

# Make sure we have the necessary paths (eg. in cron), /sbin will be the first path to search.
# some needed binaries are in /lib/udev or /usr/lib/udev
for path in /usr/bin /bin /usr/sbin /sbin ; do
    case ":$PATH:" in
        (*:"$path":*)
            ;;
        (*)
            if test -d "$path" ; then
                PATH=$path:$PATH
            fi
            ;;
    esac
done
# No readonly PATH so that it can be later extended if needed
# (e.g. to have third-party backup/restore tools available via PATH):
PATH=$PATH:/lib/udev:/usr/lib/udev

# Are we root?
if test "$( id --user )" != "0" ; then
    echo "ERROR: $PRODUCT needs ROOT privileges!" >&2
    exit 1
fi

# Set some bash options:
# With nullglob set when e.g. for foo*bar no file matches are found, then foo*bar is removed
# (e.g. "ls foo*bar" becomes plain "ls" without "foo*bar: No such file or directory" error).
# The extglob shell option enables several extended pattern matching operators.
shopt -s nullglob extglob
# Save the current default bash flags and options in a global readonly variable
# so that exactly the default bash flags and options can be restored if needed via
#   apply_bash_flags_and_options_commands "$DEFAULT_BASH_FLAGS_AND_OPTIONS_COMMANDS"
readonly DEFAULT_BASH_FLAGS_AND_OPTIONS_COMMANDS="$( get_bash_flags_and_options_commands )"

# Forget all remembered full pathnames of commands:
hash -r

# Make sure that we use only English:
export LC_CTYPE=C LC_ALL=C LANG=C

# Include default config:
source $SHARE_DIR/conf/default.conf

# Include functions:
for script in $SHARE_DIR/lib/*.sh ; do
    source $script
done

# LOCKLESS_WORKFLOWS can run simultaneously with another instance by using a LOGFILE.lockless:
if IsInArray "$WORKFLOW" "${LOCKLESS_WORKFLOWS[@]}" ; then
    LOGFILE="$LOGFILE.lockless"
else
    # When this currently running instance is not one of the LOCKLESS_WORKFLOWS
    # then it cannot run simultaneously with another instance
    # in this case pidof is needed to test what running instances there are:
    if ! has_binary pidof ; then
        echo "ERROR: Required program 'pidof' missing, please check your PATH" >&2
        exit 1
    fi
    # For unknown reasons '-o %PPID' does not work for pidof at least in SLES11
    # so that a manual test is done to find out if another pid != $$ is running:
    for pid in $( pidof -x "$SCRIPT_FILE" ) ; do
        if test "$pid" != $$ ; then
            echo "ERROR: $PROGRAM is already running, not starting again" >&2
            exit 1
        fi
    done
fi

# Keep old log file:
if test -r "$LOGFILE" ; then
    mv -f "$LOGFILE" "$LOGFILE".old 2>&8
fi
mkdir -p $LOG_DIR

exec 2>"$LOGFILE" || echo "ERROR: Could not create $LOGFILE" >&2
# Keep our default $LOGFILE location in a seperate variable REAR_LOGFILE
# in case end-user overruled it in the local.conf file:
readonly REAR_LOGFILE="$LOGFILE"

if test "$WORKFLOW" != "help" ; then
    LogPrint "$PRODUCT $VERSION / $RELEASE_DATE"
    Log "Command line options: $0 ${CMD_OPTS[@]}"
    test "$VERBOSE" && LogPrint "Using log file: $LOGFILE" || true
fi

v=""
verbose=""
# Enable progress subsystem only in verbose mode, set some stuff that others can use:
if test "$VERBOSE" ; then
    source $SHARE_DIR/lib/progresssubsystem.nosh
    v="-v"
    verbose="--verbose"
fi

# Enable debug output of the progress pipe
# (no readonly KEEP_BUILD_DIR because it is also set to 1 in build/default/98_verify_rootfs.sh):
test "$DEBUG" && KEEP_BUILD_DIR=1 || true

# Check if we are in recovery mode:
test -e "/etc/rear-release" && RECOVERY_MODE="y" || true
readonly RECOVERY_MODE

test "$SIMULATE" && LogPrint "Simulation mode activated, Relax-and-Recover base directory: $SHARE_DIR" || true

if test "$DEBUGSCRIPTS_ARGUMENT" ; then
    Debug "Current set of flags is '$-'"
    Debug "The debugscripts flags are '$DEBUGSCRIPTS_ARGUMENT'"
fi

# All workflows need to read the configurations first.
# Combine configuration files:
Debug "Combining configuration files"
# Use this file to manually override the OS detection:
test -r "$CONFIG_DIR/os.conf" && Source "$CONFIG_DIR/os.conf" || true
test -r "$CONFIG_DIR/$WORKFLOW.conf" && Source "$CONFIG_DIR/$WORKFLOW.conf" || true
SetOSVendorAndVersion
# Distribution configuration files:
for config in "$ARCH" "$OS" \
        "$OS_MASTER_VENDOR" "$OS_MASTER_VENDOR_ARCH" "$OS_MASTER_VENDOR_VERSION" "$OS_MASTER_VENDOR_VERSION_ARCH" \
        "$OS_VENDOR" "$OS_VENDOR_ARCH" "$OS_VENDOR_VERSION" "$OS_VENDOR_VERSION_ARCH" ; do
    test -r "$SHARE_DIR/conf/$config.conf" && Source "$SHARE_DIR/conf/$config.conf" || true
done
# User configuration files, last thing is to overwrite variables if we are in the rescue system:
for config in site local rescue ; do
    test -r "$CONFIG_DIR/$config.conf" && Source "$CONFIG_DIR/$config.conf" || true
done
# Now SHARE_DIR CONFIG_DIR VAR_DIR LOG_DIR and KERNEL_VERSION should be set to a fixed value:
readonly SHARE_DIR CONFIG_DIR VAR_DIR LOG_DIR KERNEL_VERSION

SourceStage "init"

# Check for requirements, do we have all required binaries?
# Without the empty string as initial value MISSING_PROGS would be
# an unbound variable that would result an error exit if 'set -eu' is used:
MISSING_PROGS=("")
for f in "${REQUIRED_PROGS[@]}" ; do
    if ! has_binary "$f" ; then
        MISSING_PROGS=( "${MISSING_PROGS[@]}" "$f" )
    fi
done
test "$MISSING_PROGS" && Error "Cannot find required programs: ${MISSING_PROGS[@]}"

readonly VERSION_INFO="
$PRODUCT $VERSION / $RELEASE_DATE

$PRODUCT comes with ABSOLUTELY NO WARRANTY; for details see
the GNU General Public License at: http://www.gnu.org/licenses/gpl.html

Host $( uname -n ) using Backup $BACKUP and Output $OUTPUT
Build date: $( date -R )
"

if test "$WORKFLOW" != "help" ; then
    # Create temporary work area and register removal exit task:
    BUILD_DIR="$( mktemp -d -t rear.XXXXXXXXXXXXXXX || Error "Could not create build area '$BUILD_DIR'" )"
    QuietAddExitTask cleanup_build_area_and_end_program
    Log "Using build area '$BUILD_DIR'"
    ROOTFS_DIR=$BUILD_DIR/rootfs
    TMP_DIR=$BUILD_DIR/tmp
    mkdir -p $v $ROOTFS_DIR >&2 || Error "Could not create $ROOTFS_DIR"
    mkdir -p $v $TMP_DIR >&2 || Error "Could not create $TMP_DIR"
fi

# Check for and run the requested workflow:
if has_binary WORKFLOW_$WORKFLOW ; then
    Log "Running $WORKFLOW workflow"
    # There could be no ARGS[@] which means it would be an unbound variable so that
    # an empty default is used here to avoid an error exit if 'set -eu' is used:
    WORKFLOW_$WORKFLOW "${ARGS[@]:-}"
    Log "Finished running $WORKFLOW workflow"
else
    VERBOSE=1
    LogPrint "ERROR: The specified command '$WORKFLOW' does not exist!"
    EXIT_CODE=1
fi

if test "$REAR_LOGFILE" != "$LOGFILE" ; then
    cat "$REAR_LOGFILE" > "$LOGFILE"
fi

if test $EXIT_CODE -eq 0 ; then
    LogToSyslog "DONE: rc=$EXIT_CODE"
fi

exit $EXIT_CODE

