#! /bin/bash

# This script creates a backup of all of the config files (in the debian
# packaging sense) on a Cumulus Linux switch.  It also backups all other
# files in /etc, so the only config files backed up name or list are those
# not located in the /etc directory.

# before sourcing
declare -r prefix=config-backup

# Source the file with the common variables and functions
. /usr/lib/cumulus/config-util-common

umask 077 # don't want to have sensitive data accessible in archives
mkdir -p $TMPDIR $rundir
chmod 700 $TMPDIR $rundir

declare nonetc_cfiles archive tag description exclude
declare -r exclude_dir=/var/lib/config-backup/config-backup-exclude.d
declare -i permanent=0
declare -g include_file_dir=""
declare -r cfg_file=/etc/cumulus/config_backup.conf

# parsing /etc/cumulus/config_backup.conf to check if any
# files/directories are mentioned
parse_cfg_file() {
    if [[ -e $cfg_file ]]
    then
        include_line=$(grep -E "^(\s*)include_file_dirs(\s*):" $cfg_file | tail -n 1)
        include_file_dir=""

        if [ ! -z "$include_line" ]; then

            files=$(echo $include_line | cut -d ":" -f 2- )

            for file in $files; do
                trimmed_file=$(echo $file | awk '{$1=$1};1')
                if [ -e "$trimmed_file" ]; then
                    include_file_dir=$(echo $include_file_dir $trimmed_file)
                else
                    echo $trimmed_file does not exist and cannot be backed up. Pls remove this file from $cfg_file
                fi
            done
        fi
    fi
}

# cleanup any temp files we create, etc., then fall through so we exit
# with the value we expect
# We never cleanup the archive, either in TMPDIR or archivedir,
# since we can't know their state for sure.
cleanup()
{
    debug Cleaning up
    rm -f $lockfile $control_base 2>/dev/null
    rm -fr $TMPDIR 2>/dev/null
}

main()
{
    pname=${0##*/}

    options "$@"

    checkperms

    # functions that cd must cd back afterwards, or pushd/popd
    cd $TMPDIR || error Unable to create or chdir to $TMPDIR

    runlock

    trap cleanup EXIT INT TERM

    read_config
    mkdir -p $archivedir $archivedir/.Descriptions $archivedir/.Tags \
        $archivedir/.Numbers
    chmod a-w $archivedir || exit

    verbose getting backup system info
    get_configfiles
    create_control

    cleanbackups

    verbose Starting the backup
    do_backup

    debug Completed
    return 0
}

usagemsg='Usage: [-dhpqv] [-D description] [-t type] [-X pattern]'

usage()
{
    [ -n "$1" ] && warn "Unsupported option(s): $1"
    echo "$pname: $usagemsg"
    exit 1
}

help()
{
    echo -e "$usagemsg"
    cat << EOF
    -h: Display this help message
    -d: Debugging output enabled
    -D description: Description of backup
    -p: Mark this backup as permenant; never removed
    -q: Quiet mode, no messages except errors
    -t type (e.g. pre, post, pre-restore)
    -v: Verbose status dmessages
    -X pattern: eXclude files matching pattern.  May be repeated
EOF
}


options()
{
    declare dohelp=0 badopts Option
    # we'll report errors ourselves
    while getopts ":dD:hpqt:vX:" Option; do
       case $Option in
       h) dohelp=1 ;;
       d) debug_en=1 ;;
       D) description="$OPTARG" ;;
       p) permanent=1;;
       q) quiet_en=1 ;;
       t) tag="$OPTARG" ;;
       v) verbose_en=1 ;;
       X) exclude="$exclude --exclude=$OPTARG" ;;
       *|?) badopts="$badopts -$OPTARG" ;;
       esac
    done
    shift $((OPTIND - 1))

    [ $# -ne 0 ] && warn Extra arguments \""$*"\" ignored
    [ -n "$badopts" ] && usage "$badopts"
    [ $dohelp -eq 1 ] && { help ; exit 0 ; }

}

# Create the control information for the backup
create_control()
{
    {
    date +"Backup created on: %F %T"
    echo "Backup created on host: $(hostname)"
    echo "Platform: $(platform-detect)"
    echo "OS-Version: $(sed -n -e 's/\~.*//' -e s/VERSION_ID=//p /etc/os-release)"
    echo "Serial Number: $(decode-syseeprom -e)"
    echo "Backup_version=$config_version"
    echo "=== System EEPROM"
    decode-syseeprom
    echo "=== System EEPROM End"
    echo "=== Package List"
    get_installed_pkgs
    cat $pkglist
    echo "=== Package List End"
    } > $control_base
}

# cleanup archive files, printing reason if verbose enabled
# Also remove the related flag and info files
# The reason is required, and is argument 1
# The expectation is that we are in the archive directory where
# the files are located (/run or archivedir)
# This won't handle filenames with spaces because of the way
# the list is builtup but we shouldn't ever see those, unless
# something went wrong
cleanupfiles()
{
    declare msg="$1"
    shift
    declare flist=$@
    declare flist2="$@"

    verbose "$msg" $flist

    rm -f $flist
    [ -d .Descriptions ] && # remove matching descriptions, if any
        ( cd .Descriptions && rm -f "$flist" ) 2>/dev/null
    [ -d .Tags ] && # remove matching tags, if any
        ( cd .Tags && rm -f "$flist" ) 2>/dev/null
    [ -d .Numbers ] && # remove matching numbers, if any
        ( cd .Numbers && rm -f "$flist" ) 2>/dev/null
}


# clean up old archive files (never oldest or newest)
# if more than $maxbackups.
# Gets directory to check as an argument
# Never removes oldest or newest archive
# from oldest to newest until fewer than maxbackups
# This is called prior to creating the backup; the newest at
# this point will be from the previous run.
cleanarchives()
{
    local dir=$1

    debug Check number of archives in $dir

    [ -d "$dir" ] || return
    # run rest in a subshell to avoid changing user environment
    # and directory for rest of script
    (
    declare list file numfiles
    cd "$dir" || return
    list=$TMPDIR/tmp-${$}-files
    # We want C sorting, regardless of customer language
    # Never consider permanent (-p) archives for removal
    LANG=C LC_ALL=C /bin/ls ${cfgbackup}* 2>/dev/null | 
        grep -v -- '-perm' > $list 
    read numfiles file <<< $(wc -l $list)

    if [ $numfiles -ge $maxbackups ]; then
        declare oldest newest files
        declare -i nfiles=$numfiles
        oldest=$(head -1 $list)
        newest=$(tail -1 $list)
        while read file ; do
            [ "$file" = "$oldest" -o "$file" = "$newest" ] &&
                continue
            files="$file $files"
            (( nfiles-- ))
            [ $nfiles -lt $maxbackups ] && break
        done < $list
        [ -n "$files" ] && {
            cleanupfiles "$dir had $numfiles configuration backups, removing:"\
                $files
        }
    else
        debug $dir only had $numfiles configuration backups, no cleanup
    fi
    )
}


# check if still too much space used; check both TMPDIR
# and archivedir, since we may leave backups in TMPDIR
# when the root filesystem is getting full
# Never removes oldest or newest archive; otherwise removes
# from oldest to newest until enough space
# This is called prior to creating the backup; the newest at
# this point will be from the previous run.
cleanspace()
{
    local dir=$1

    debug Check disk space of archives in $dir
    [ -d "$dir" ] || return
    # run rest in a subshell to avoid changing user environment
    # and directory for rest of script
    (
    declare -i bsize blocks total totalmb maxblocks
    cd "$dir" || return
    bsize=$(stat -c %B . 2>/dev/null)
    [ -z "$bsize" ] && bsize=512
    total=0
    for f in ${cfgbackup}*; do
        blocks=$(stat -c %b "$f" 2>/dev/null)
        (( total += blocks ))
    done
    (( maxblocks = ( maxmbbackups * 1024 * 1024 ) / bsize ))
    if [ $total -ge $maxblocks ]; then
        declare oldest newest list file files

        (( totalmb = (total * bsize) / (1024*1024) ))
        debug ${totalmb}MB of backups in $dir, try cleaning up
        list=$TMPDIR/tmp-${$}-files
        # We want C sorting, regardless of customer language
        # Never consider permanent (-p) archives for removal
        LANG=C LC_ALL=C /bin/ls ${cfgbackup}* 2>/dev/null | 
            grep -v -- '-perm' > $list 
        oldest=$(head -1 $list)
        newest=$(tail -1 $list)
        while read file ; do
            [ "$file" = "$oldest" -o "$file" = "$newest" ] &&
                continue
            files="$file $files"
            blocks=$(stat -c %b $file 2>/dev/null)
            (( total -= blocks ))
            [ $total -lt $maxblocks ] && break
        done < $list
        [ -n "$files" ] && {
            cleanupfiles "$dir has ${totalmb}MB in backups, removing:" $files
        }
    else
        (( totalmb = (total * bsize) / (1024*1024) ))
        debug ${totalmb}MB of backups in $dir, no space cleanup
    fi
    )
}

# check for too many backups or too much space; check both TMPDIR and
# archivedir, since we may leave backups in TMPDIR when the root
# filesystem is getting full
cleanbackups()
{
    for cdir in $TMPDIR $archivedir; do
        cleanarchives $cdir
        cleanspace $cdir
    done
}

do_backup()
{
    declare patfiles rval dt hn availmb perm
    declare -i num bnum
    [ -d $exclude_dir ] && {
        declare file
        for file in $exclude_dir/*.regex; do
            [ "$file" = "$exclude_dir"/'*.regex' ] && break # no files
            patfiles="$patfiles -X$file"
        done
    }
    dt=$(date +%F-%T)
    dt=${dt//:/.} # tar requires --force-local to avoid remote tape
    hn=$(hostname -s)
    # date before hostname for sorting, so date sort works
    # even if hostname changes
    [ $permanent -eq 1 ] && perm=-perm
    archive="${cfgbackup}-${dt}_${hn}${perm}"
    # no tabs allowed in description or Tag, since that's our
    # column separator on listing
    [ -n "$tag" ] &&
        echo "$tag" | sed 's/\t/ /g' > "$archivedir/.Tags/$archive"
    [ -n "$description" ] &&
        echo "$description" | sed 's/\t/ /g' \
            > "$archivedir/.Descriptions/$archive"
    if [ -s "$archivedir/.Numbers/NextNumber" ]; then
        read num < "$archivedir/.Numbers/NextNumber" 1>/dev/null
        [ -z "$num" ] && num=1 # first time, or removed, etc.
        [ "$num" -lt 1 ] 2>/dev/null && num=1
    else
        num=1
    fi
    echo "$num" > "$archivedir/.Numbers/$archive"
    bnum=num
    (( num++ ))
    echo "$num" > "$archivedir/.Numbers/NextNumber"

    parse_cfg_file
    if [[ ! -z "$include_file_dir" ]];
    then
        echo -e "Files/Directories to be included for backup : $include_file_dir"
    fi

    fullarchive="$TMPDIR/$archive"
    debug creating $archive, using exclude files $patfiles

    # -4 is a compromise between compression and time
    XZ_OPT=-4 tar -Jc -b 64 -f "$fullarchive" --warning=no-file-changed \
        -C / $exclude $control_base $patfiles $pkglist $nonetc_cfiles /etc $include_file_dir >& \
        $TMPDIR/tmp-$$.tar
    rval=$?

    egrep -v \
      'tar: Removing leading `/'"'"' from (member names|hard link targets)' \
      $TMPDIR/tmp-$$.tar
    [ $rval -ne 0 ] && warn configuration backup creation exited with $rval
    [ -s $archive ] ||
        error Failed to create configuration backup "$fullarchive"
    availmb=$(df --block-size=MB --output=avail $archivedir 2>/dev/null |
        sed -n s/MB//p)
    if [ -z "$availmb" ] ; then
        warn Unable to determine free space on $archivedir
        (( rval += 2 ))
    elif [ "$availmb" -lt $minmbfree ] ; then
        warn Only "$availmb"MB free space on $archivedir
        (( rval += 3 ))
    elif ! mv "$fullarchive" "$archivedir/$archive"; then
        (( rval += $? ))
        warn Failed to move configuration backup
        warn Leaving configuration backup in volatile filesystem:"\n  $fullarchive"
    elif [ $quiet_en -ne 1 ]; then
        notify Created configuration backup: "\n$bnum  $archivedir/$archive"
    fi
    return $rval
}

main "$@"


