#!/bin/bash VALIDCOMMANDS="go ls init repos stats diff forget get check" RCFILE=${HOME}/.backup/config DEF_AUTHFILE=${HOME}/.backup/auth REPOFILE=${HOME}/.backup/repos STATFILE=${HOME}/.backup/stats LOGFILE=/var/log/backup.log FULLLOGFILE_BASE=/tmp/full_backup_log RESTIC=/usr/local/bin/restic RCLONE=/usr/local/bin/rclone RSYNC=/usr/local/bin/rsync USMB=/usr/local/bin/usmb #RCLONEOPTS="--cache-chunk-no-memory --buffer-size=10M --progress" RCLONEOPTS="--progress --buffer-size 10M --cache-chunk-no-memory" SPEED="" RESTOREDIRBASE="/mnt/restore" CRONMODE=0 CONNECTIONS="" LOG=/dev/stdout DATE=/bin/date DOALL=0 REPO_LIST="" TESTMODE=0 TELEGRAM=0 VERBOSE=0 function cecho() { local col col="$1" shift 1 echo -e "${col}$*${PLAIN}" } function error() { cecho "$RED" "Error: $*" } function action() { cecho "$CYAN" "$*" } function log() { local now col lowrer now=`${DATE} +%Y/%m/%d-%H:%M:%S` lower=$(echo "$*" | tr '[:upper:]' '[:lower:]') if [[ $lower == *error* ]]; then col="$RED" elif [[ $lower == *fail* ]]; then col="$RED" elif [[ $lower == *success* ]]; then col="$GREEN" elif [[ $lower == *starting* ]]; then col="$BOLD" elif [[ $lower == *created* ]]; then col="$BOLD" elif [[ $lower == *creating* ]]; then col="$BOLD" elif [[ $lower == *finished* ]]; then col="$BOLD" else col="$CYAN" fi if [[ $TELEGRAM -eq 1 ]]; then echo -e "__${now}__ **<${mode}>** \`$*\`" >>${LOG} else echo -e "$CYAN${now} <${mode}> $col$*$PLAIN" >>${LOG} fi } function updatestats() { local repo cmd stamp code repo=$1 cmd=$2 code=$3 stamp=`date +%s` if [[ -f $STATFILE ]]; then sed -i "/${repo}:${cmd}/d" $STATFILE fi echo "${repo}:${cmd}:${code}:${stamp}" >> $STATFILE } function usage-repo() { echo " reponame1;/path/to/files1/;tag1,tag2,...,tagX;repo_url;repo_passfile;repo_excludefile" echo " reponame2;/path/to/files2/;tag1,tag2,...,tagX;repo_url;repo_passfile;repo_excludefile" echo " ..." echo " reponameX;/path/to/filesX/;tag1,tag2,...,tagX;repo_url;repo_passfile;repo_excludefile" echo "" echo " Default password file is: $DEF_AUTHFILE" echo "" echo "Backup method tags:" echo " restic Backup using restic (default repo is Backblaze B2 bucket \$B2_BUCKET_PREFIX-)" echo " rclone Backup using rclone (default repo is Backblaze B2 bucket \$B2_BUCKET_PREFIX-)" echo " rsync Backup using rsync (default repo is \$DEF_RSYNC_USER@\$DEF_RSYNC_SERVER:\$DEF_RSYNC_DIR/)" echo " postgres Dump postgres databases. Instead of local file path, specify database name to backup. If empty, all dbs are backed up." echo " mysql Dump mysql databases. Instead of local file path, specify database name to backup. If empty, all dbs are backed up." echo "" echo "Pre-backup commands to access local files:" echo " nfs Mount \$DEF_NFS_SERVER:\$DEF_NFS_SERVER_BASE/ to " echo " before running. Sharename determined by stripping \$NFS_PREFIX" echo " from start of repo path." echo " usmb Run 'usmb reponame' to mount \$USMB_PREFIX/ before running." echo " Sharename determined by stripping \$NFS_PREFIX from start of ." echo " localmount Run 'mount reponame' to mount before running." echo echo "Pre-backup commands to access repo:" echo " premount=x Run 'mount ' before command and 'umount x' afterwards." } function usage-rc() { echo " export B2_ACCOUNT_ID=xxx # Backblaze B2 credentials" echo " export B2_ACCOUNT_KEY=xxx # Backblaze B2 credentials" echo " export B2_APP_ID=xxx # Backblaze B2 bucket details" echo " export B2_APP_KEY=xxx # Backblaze B2 bucket auth" echo " export B2_BUCKET_PREFIX=xxx # Prefix to append to create unique B2 bucket name" echo "" echo " # Binary paths (optional, below are defualts):" echo " #export RESTIC=/usr/local/bin/restic" echo " #export RCLONE=/usr/local/bin/rclone" echo " #export RSYNC=/usr/local/bin/rsync" echo " #export USMB=/usr/local/bin/usmb" echo "" echo " export RESTIC_KEEPDAYS=31 # Days to keep restic backups for" echo " export RESTICOPTS=\"--one-file-system\" # Extra args to pass to restic" echo "" echo " export PGUSER=postgres # Postgresql username" echo "" echo " export DEF_RSYNC_SERVER=xxx # Server used for repos with 'rsync' tag" echo " export DEF_RSYNC_USER=backups # Username for rsync server" echo " export DEF_RSYNC_DIR=/home/backups/backups # Remote directory on rsync server" echo " export RSYNC_OPTIONS=-Pavz # Options to pass to rsync" echo "" echo " export USMB_PREFIX=/DataVolume/shares # Strip this from repo path to get USMB share name" echo "" echo " export DEF_NFS_SERVER=nfs.yourdomain.com # NFS server where local files for 'nfs' repos are found" echo " export DEF_NFS_SERVER_BASE=/remote/nfs/share/base # Path on local NFS server to prefix to repo names" echo " export NFS_PREFIX=/local/nfs/mountpoint # Where to mount NFS exports locally before backing them up" } function usage() { echo "usage: $0 command reponame" echo "" echo " -a Run on all repos defined as 'auto'" echo " -d dir Restore mode: specify where to put restored files (default: $RESTOREDIRBASE)" echo " -h Show this usage text" echo " -s num Limit speed to 'num' Mbps (default: unlimited)" echo " -x num Use 'num' simultaneous connections (default: 20)" echo " -c Cron mode - log to ${LOGFILE}" echo " -t Test mode - dump what would be done then exit." echo " -T Telegram formatting mode" echo "" echo "Valid commands are:" echo " $VALIDCOMMANDS" echo "" echo "${RCFILE} should look like this:" usage-rc echo "" echo "${REPOFILE} should look like this:" usage-repo echo "" } function getdatapath() { local DATAPATH REPO x REPO="$1" DATAPATH="" for x in ${REPODEFS[@]}; do match="^${REPO}\;" if [[ $x =~ $match ]]; then DATAPATH=`echo $x | sed -e s/${match}//` fi done DATAPATH=`echo $DATAPATH | sed -e s/\;.*//` echo "$DATAPATH" } function getmode() { local mode checktag "$1" rclone if [[ $? -eq 0 ]]; then mode=rclone else checktag "$1" rsync if [[ $? -eq 0 ]]; then mode=rsync else mode=restic fi fi echo "$mode" } function getrepoexcludefile() { # output path local res rv res=`getrepotok $1 6` rv=$? echo "$res" return $rv } function getrepopath() { # output path local res rv mode def repobase res=`getrepotok $1 4` rv=$? if [[ -z $res ]]; then mode=`getmode $1` repobase=`getreponame $1` if [[ $mode == "restic" ]]; then def="b2:${B2_BUCKET_PREFIX}-${repobase}" elif [[ $mode == "rclone" ]]; then def="remote:${B2_BUCKET_PREFIX}-${repobase}" elif [[ $mode == "rsync" ]]; then def="${DEF_RSYNC_USER}@${DEF_RSYNC_SERVER}:${DEF_RSYNC_DIR}/$repobase" else def="(no default repo for mode $mode)" fi res="$def" fi echo "$res" return $rv } function getrepopassfile() { # output path local res rv res=`getrepotok $1 5` rv=$? echo "$res" return $rv } function getrepotags() { # output path local res rv res=`getrepotok $1 3` rv=$? echo "$res" return $rv } function getrepotok() { # output given token from repo def local def res rv code idx def=`getrepodef "$1"` idx=$2 code="{ print \$${idx} }" res=`echo "$def" | awk -F';' "$code"` echo "$res" if [[ -z $res ]]; then rv=1 else rv=0 fi return $rv } function getreponame() { local reponame reponame="$1" if [[ $reponame =~ ^.*\;.*$ ]]; then # if we were given a repo def, return just the name reponame=`echo "$reponame" | sed -e 's/\;.*//g'` fi echo "$reponame" } function getrepodef() { local reponame match repodef x reponame="$1" if [[ $reponame =~ ^.*\;.*$ ]]; then repodef="$reponame" else # if we were just given a repo name, look up the def repodef="" for x in ${REPODEFS[@]}; do match="^${reponame};" if [[ $x =~ $match ]]; then repodef="$x" fi done fi echo "$repodef" } function gettagval() { # return 0 if tag matches local reponame lookfor justtags x t val reponame="$1" lookfor="$2" justtags=`getrepotags "$reponame"` val="" IFS=',' read -ra t <<< "$justtags" for x in ${t[@]}; do if [[ $x == ${lookfor}=* ]]; then val=`echo "$x" | cut -d= -f2` break fi done if [[ ! -z ${val} ]]; then echo "$val" return 0 fi return 1 } function checktag() { # return 0 if tag matches local reponame lookfor justtags reponame="$1" lookfor="$2" justtags=`getrepotags "$reponame"` if [[ $justtags =~ ^.*${lookfor}.*$ ]]; then return 0 fi return 1 } function do_mount() { local msg if [[ $USEUSMB -eq 1 ]]; then [[ $VERBOSE -eq 1 ]] && log "debug: mounting $DATAPATH via smb" mount | grep -q ${DATAPATH} if [[ $? -ne 0 ]]; then # try mounting it. ${USMB} ${USMBNAME} >/dev/null if [[ $? -eq 0 ]]; then log "Mount usmb volume '$USMBNAME': success" else log "Mount usmb volume '$USMBNAME': FAILED" return 1 fi fi elif [[ $USENFS -eq 1 ]]; then [[ $VERBOSE -eq 1 ]] && log "debug: mounting $DATAPATH via nfs" mount | grep -q ${DATAPATH} if [[ $? -ne 0 ]]; then # try mounting it. mkdir -p $DATAPATH mount_nfs -R 1 ${NFSPATH} ${DATAPATH} >/dev/null if [[ $? -eq 0 ]]; then log "Mount nfs volume '$NFSPATH' to $DATAPATH: success" else log "Mount nfs volume '$NFSPATH' to $DATAPATH: FAILED" return 1 fi fi elif [[ $USELOCALMOUNT -eq 1 ]]; then [[ $VERBOSE -eq 1 ]] && log "debug: mounting $DATAPATH via fstab" mount | grep -q ${DATAPATH} if [[ $? -ne 0 ]]; then # try mounting it. mkdir -p $DATAPATH mount ${DATAPATH} >/dev/null if [[ $? -eq 0 ]]; then log "Mount $DATAPATH: success" else log "Mount $DATAPATH: FAILED" return 1 fi fi fi # datapath is used for sql filename in pgsql/mysql backups if checktag "$f" postgres; then # Make sure the db exists if ! [[ -z ${DATAPATH} ]]; then psql -lqt | cut -d \| -f 1 | grep -qw ${DATAPATH} if [[ $? -ne 0 ]]; then log "Error: PostgreSQL database '${DATAPATH}' doesn't exist" return 1 fi fi elif checktag "$f" mysql; then # Make sure the db exists if ! [[ -z ${DATAPATH} ]]; then mysql -e 'show databases' | grep -v "^Database$" | grep -qw ${DATAPATH} if [[ $? -ne 0 ]]; then log "Error: MySQL database '${DATAPATH}' doesn't exist" return 1 fi fi else if ! [[ -e ${DATAPATH} ]]; then log "Error: ${DATAPATH} doesn't exist" return 1 fi count=`ls ${DATAPATH} | wc -l` if [ $count -le 2 ]; then log "Error: ${DATAPATH} exists but appears to be empty" return 1 fi fi return 0 } function do_umount() { if is_mounted $DATAPATH; then if [[ $USEUSMB -eq 1 ]]; then log "Unmounting usmb volume '$USMBNAME'" usmb -u $USMBNAME elif [[ $USENFS -eq 1 ]]; then log "Unmounting nfs volume '$NFSPATH' from $DATAPATH" umount $DATAPATH elif [[ $USELOCALMOUNT -eq 1 ]]; then log "Unmounting dir $DATAPATH" umount $DATAPATH fi fi } function updatedir() { local temp r tempwd tempwd=$(echo "$wd" | sed -e 's-/\{1,\}-/-g') temp=$( ${RESTIC} $AUTHOPTS $CONNECTIONSOPTS -q ls -l $RESTORESNAPID $tempwd 2>/dev/null ) r=$? if [[ $r -ne 0 || -z $temp ]]; then return 1 fi wd="$tempwd" contents=$( echo "$temp" | awk -v wd=$wd '{ sub(wd,""); sub(wd "/", ""); print $0; }') subdirs=$( echo "${contents}" | grep "^d" | awk '{ print $NF}' | sed -e 's,^/,,') subfiles=$( echo "${contents}" | awk '{ print $NF}' | sed -e 's,^/,,') return 0 } function showrestoreinfo() { cecho "$YELLOW$BOLD" "Repo to restore from:" cecho "$YELLOW" " $f (snapshot $RESTORESNAPID)" echo cecho "$YELLOW$BOLD" "Files to restore:" if [[ -z $RESTORELIST ]]; then cecho "$YELLOW" " (none)" else cecho "$YELLOW" "$RESTORELIST" | sed -e 's/^ //' | tr ' ' '\n' | sed -e 's/^/ /g' fi echo } function restorecmd_help() { echo echo "x Abort" echo "q Abort" echo "lcd dir Change local restore dir" echo "ls Show files in current repo dir" echo "cd Change current repo dir" echo "dirs Start subdirectories of current repo dir" echo "go Start restore of marked file" echo "w List files marked for restore" echo "a file Mark file to be restored" echo "d file Un-mark file from restore list" echo } function is_mounted() { local dir dev1 dev2 rv formatarg dir="$1" if [[ $(uname -s) == "Linux" ]]; then formatarg="-c" else formatarg="-f" fi dev1=$(stat $formatarg %d $dir 2>/dev/null) dev2=$(stat $formatarg %d $dir/.. 2>/dev/null) if [[ $dev1 == $dev2 ]]; then rv=1 else rv=0 fi return $rv } function check_repomount_needed() { local path path=`gettagval "$f" premount` if [[ $? -eq 0 ]]; then grep -q $path /etc/fstab if [[ $? -ne 0 ]]; then log "Error: repo '$REPO' has premount tag but $path not in fstab." return 1 fi MOUNTREPO=1 else MOUNTREPO=0 fi return 0 } function check_datamount_needed() { if [[ $CMD == "go" ]]; then NEEDMOUNT=1 elif [[ $CMD == "get" ]]; then NEEDMOUNT=1 elif [[ $CMD == "diff" ]]; then NEEDMOUNT=1 else NEEDMOUNT=0 fi } # Handle args ARGS="acd:hs:tTvx:" while getopts "$ARGS" i; do case "$i" in a) DOALL=1 ;; d) RESTOREDIRBASE="$OPTARG"; ;; h) usage; exit 1 ;; s) SPEED="$OPTARG"; ;; x) CONNECTIONS="$OPTARG"; ;; c) CRONMODE=1; LOG=${LOGFILE} ;; t) TESTMODE=1 ;; T) TELEGRAM=1 ;; v) VERBOSE=1 ;; *) echo "ERROR: invalid argument: $i"; usage; ;; esac done shift $((OPTIND - 1)) if [[ $CRONMODE -eq 0 ]]; then BOLD="\033[1m" PLAIN="\033[0m" UNDERLINE="\033[4m" RED="\033[31m" GREEN="\033[32m" YELLOW="\033[33m" BLUE="\033[34m" CYAN="\033[36m" LINK="$BLUE$UNDERLINE" fi if ! [ -e ${RCFILE} ]; then error "can't find ${RCFILE}" echo "" usage-rc exit 1 fi if ! [ -e ${REPOFILE} ]; then error "can't find ${REPOFILE}" echo "" usage-repo exit 1 fi which bc >/dev/null 2>&1 if [ $? -ne 0 ]; then error "can't find bc in path" echo "" exit 1 fi . ${RCFILE} if [[ -z $RESTIC_KEEPDAYS ]]; then error "\$RESTIC_KEEPDAYS not set." exit 1 fi # Convert speed from Mbps to KBps if ! [[ -z $SPEED ]]; then KSPEED=$(echo "scale=2; $SPEED * 125" | bc) else KSPEED="" fi CMD="$1" shift 1 # command synonyms if [[ $CMD == "repolist" ]]; then CMD="repos" elif [[ $CMD == "restore" ]]; then CMD="get" elif [[ $CMD == "snapshots" ]]; then CMD="ls" fi if (echo "$VALIDCOMMANDS" | grep -wq $CMD) ; then true else error "invalid command $CMD. Should one of: $VALIDCOMMANDS" exit 1 fi # Get list of defined repos idx=0 for f in `cat $REPOFILE | grep ";" | grep -v "^#"`; do REPODEFS[$idx]="$f" mode=`getmode "$f"` if [[ $mode == "restic" ]]; then thisrpath=`getrepopath $f` if [[ ${thisrpath,,} == *b2* ]]; then if [[ -z $B2_APP_ID ]]; then error "\$B2_APP_ID not set." exit 1 fi if [[ -z $B2_APP_KEY ]]; then error "\$B2_APP_KEY not set." exit 1 fi fi fi if [[ $DOALL -eq 1 ]]; then checktag "$f" auto if [[ $? -eq 0 ]]; then REPO_LIST="$REPO_LIST `echo $f | sed -e 's/\;.*//'`" fi fi idx=$((idx + 1)) done if [[ $CMD == "get" ]]; then # one repo + snapshotid/date err=0 if [[ -z $REPO_LIST ]]; then if [[ $# -ge 1 ]]; then REPO_LIST=$1 ;shift 1 else error "no reponame provided." exit 1 fi if [[ $# -ge 1 ]]; then RESTORESNAPID=$1 ; shift 1 fi RESTORELIST="$*" else echo "restore usage: $0 get repo_name (snapshotid|latest)" echo " or" echo " $0 get repo_name (snapshotid|latest) file1 file2 etc" echo echo "Files will be restored to $RESTOREDIRBASE/repo_name/" echo exit 1 fi else # one or more repos if [[ -z $REPO_LIST ]]; then REPO_LIST="$*" fi fi if [[ $CMD == "repos" ]]; then format="%-20s%-20s%-30s%s\n" cecho "${CYAN}" "Repos defined in $BOLD${RCFILE}$PLAIN$CYAN:" echo "" head=$(printf "$format" "Repo" "Path/DB" "Repo path" "Tags") cecho "$BOLD$YELLOW" "$head" for x in ${REPODEFS[@]}; do IFS=';' read -ra tok <<< "$x" thisrepo=${tok[0]} thispath=${tok[1]} if [[ -z ${tok[2]} ]]; then tags="n/a" else tags=`echo "${tok[2]}" | sed -e 's/,/ /g'` fi thisrpath=`getrepopath $thisrepo` [[ -z $thisrpath ]] && thisrpath="(default)" if [[ $tags == *postgres* && -z $thispath ]]; then thispath="(all dbs)" elif [[ $tags == *mysql* && -z $thispath ]]; then thispath="(all dbs)" fi line=$(printf "$format" "$thisrepo" "$thispath" "$thisrpath" "$tags") cecho "$YELLOW" "$line" done exit 0 fi if [[ -z $REPO_LIST ]]; then usage exit 1 fi # Validate repos GOODREPOS="" for f in $REPO_LIST; do REPO=${f} DATAPATH=$(getdatapath $f) REPOPATH=$(getrepopath $f) mode=`getmode "$REPO"` checktag "$f" usmb if [[ $? -eq 0 ]]; then if [[ -z $USMB_PREFIX ]]; then USMBNAME=$f else if ! [[ $DATAPATH == *${USMB_PREFIX}* ]]; then log "Error: path '$DATAPATH' of repo '$REPO' does not contain smb prefix '$USMB_PREFIX'." continue fi USMBNAME=`echo "${DATAPATH}" | sed -e "s,${USMB_PREFIX}/\(.*\).*,\1,"` fi USEUSMB=1 else USMBNAME="" USEUSMB=0 fi checktag "$f" nfs if [[ $? -eq 0 ]]; then if [[ -z $DEF_NFS_SERVER ]]; then log "Error: repo '$REPO' has nfs tag but \$DEF_NFS_SERVER not defined." continue fi if [[ -z $DEF_NFS_SERVER_BASE ]]; then log "Error: repo '$REPO' has nfs tag but \$DEF_NFS_SERVER_BASE not defined." continue fi if ! [[ $DATAPATH == *${NFS_PREFIX}* ]]; then log "Error: path '$DATAPATH' of repo '$REPO' does not contain nfs prefix '$NFS_PREFIX'." continue fi NFSNAME=`echo "${DATAPATH}" | sed -e "s,${NFS_PREFIX}/\(.*\).*,\1,"` NFSPATH="${DEF_NFS_SERVER}:${DEF_NFS_SERVER_BASE}/${NFSNAME}" USENFS=1 else NFSNAME="" USENFS=0 fi check_repomount_needed "$f" # sets $MOUNTREPO [[ $? -ne 0 ]] && continue checktag "$f" localmount if [[ $? -eq 0 ]]; then dir=`echo "$DATAPATH" | sed -e 's,/$,,'` grep -q $dir /etc/fstab if [[ $? -ne 0 ]]; then log "Error: repo '$REPO' has localmount tag but $DATAPATH not in fstab." continue fi USELOCALMOUNT=1 else USELOCALMOUNT=0 fi if [[ -z $DATAPATH ]]; then checktag "$f" postgres pgrv=$? checktag "$f" mysql myrv=$? if [[ $pgrv -ne 0 && $myrv -ne 0 ]]; then log "error - can't find matching repo for $f - make sure it is listed in ${REPOFILE}" continue fi fi check_datamount_needed "$f" # sets $NEEDMOUNT if [[ $CMD == "go" ]]; then NEEDMOUNT=1 elif [[ $CMD == "get" ]]; then NEEDMOUNT=1 elif [[ $CMD == "diff" ]]; then NEEDMOUNT=1 else NEEDMOUNT=0 fi if [[ $NEEDMOUNT -eq 1 ]]; then do_mount $DATAPATH if [[ $? -ne 0 ]]; then continue fi fi if [[ -z $GOODREPOS ]]; then GOODREPOS="$f" else GOODREPOS="$GOODREPOS $f" fi done REPO_LIST="$GOODREPOS" if [[ -z $REPO_LIST ]]; then log "Error: errors found with all repos. Aborting." exit 1 fi [[ $VERBOSE -eq 1 ]] && log "Processing these repos: $REPO_LIST" if [[ $TESTMODE -eq 1 ]]; then action "Would have run '$CMD' on these repos:" for f in $REPO_LIST; do DATAPATH=$(getdatapath $f) echo " $f -> $DATAPATH" if [[ $NEEDMOUNT -eq 1 ]]; then do_umount fi done exit 0 fi errcount=0 # used for 'check' mode warncount=0 # used for 'check' mode errtext="" for f in $REPO_LIST; do [[ $VERBOSE -eq 1 ]] && log "debug: starting repo $f" REPO=${f} DATAPATH=$(getdatapath $REPO) FULLLOGFILE="${FULLLOGFILE_BASE}_${REPO}.txt" checktag "$f" usmb if [[ $? -eq 0 ]]; then if [[ -z $USMB_PREFIX ]]; then USMBNAME=$f else USMBNAME=`echo "${DATAPATH}" | sed -e "s,${USMB_PREFIX}/\(.*\).*,\1,"` fi USEUSMB=1 else USMBNAME="" USEUSMB=0 fi checktag "$f" nfs if [[ $? -eq 0 ]]; then NFSNAME=`echo "${DATAPATH}" | sed -e "s,${NFS_PREFIX}/\(.*\).*,\1,"` NFSPATH="${DEF_NFS_SERVER}:${DEF_NFS_SERVER_BASE}/${NFSNAME}" USENFS=1 else NFSNAME="" USENFS=0 fi checktag "$f" localmount if [[ $? -eq 0 ]]; then USELOCALMOUNT=1 else USELOCALMOUNT=0 fi export REPO_PATH=`getrepopath "$f"` if [[ $? -eq 0 ]]; then USINGDEFAULT=0 else USINGDEFAULT=1 fi check_datamount_needed "$f" # sets $NEEDMOUNT check_repomount_needed "$f" # sets $MOUNTREPO if [[ $MOUNTREPO -eq 1 ]]; then path=`gettagval "$f" premount` [[ $VERBOSE -eq 1 ]] && log "debug: doing repo premount at $path via fstab" if is_mounted $path; then [[ ! $CMD == "check" ]] && log "Pre-mount of '$path': already mounted" else mount $path if [[ $? -eq 0 ]]; then [[ ! $CMD == "check" ]] && log "Pre-mount of '$path': success" else [[ ! $CMD == "check" ]] && log "Pre-mount of '$path': FAILED" continue fi fi fi mode=`getmode "$f"` SPEEDOPTS="" AUTHOPTS="" OTHEROPTS="" CONNECTIONOPTS="" if [[ $mode == "restic" ]]; then MYREPO="restic: $REPO_PATH" export RESTIC_REPOSITORY="$REPO_PATH" export RESTIC_PASSWORD_FILE=`getrepopassfile "$f"` if [[ ! -z $RESTIC_PASSWORD_FILE ]]; then unset RESTIC_PASSWORD AUTHOPTS="-p ${RESTIC_PASSWORD_FILE}" else AUTHOPTS="-p ${DEF_AUTHFILE}" fi if [[ -z $KSPEED ]]; then SPEEDOPTS="" else SPEEDOPTS="--limit-upload $KSPEED" fi if [[ -z $CONNECTIONS ]]; then CONNECTIONSOPTS="" else CONNECTIONSOPTS="-o b2.connections=${CONNECTIONS}" fi checktag "$f" postgres if [[ $? -eq 0 ]]; then CONNECTIONSOPTS="$OTHEROPTS --tag postgres" fi checktag "$f" mysql if [[ $? -eq 0 ]]; then CONNECTIONSOPTS="$OTHEROPTS --tag mysql" fi if [[ $VERBOSE -eq 1 ]]; then OTHEROPTS="$OTHEROPTS -v" fi DBOPTS="" DBDUMPCMD="" checktag "$f" postgres if [[ $? -eq 0 ]]; then DBOPTS="$DBOPTS --clean" OTHEROPTS="$OTHEROPTS --stdin" if [[ -z ${DATAPATH} ]]; then DBDUMPCMD="pg_dumpall" DBFILENAME="alldbs.sql" else DBDUMPCMD="pg_dump" DBFILENAME="${DATAPATH}.sql" DBOPTS="$DBOPTS ${DATAPATH}" # Last dbdump option is database name fi fi checktag "$f" mysql if [[ $? -eq 0 ]]; then DBOPTS="$DBOPTS --all-databases --master-data --single-transaction" OTHEROPTS="$OTHEROPTS --stdin" if [[ -z ${DATAPATH} ]]; then DBDUMPCMD="mysqldump" DBFILENAME="mysql_alldbs.sql" else DBDUMPCMD="mysqldump" DBFILENAME="mysql_${DATAPATH}.sql" DBOPTS="$DBOPTS ${DATAPATH}" # Last dbdump option is database name fi fi export RESTIC_EXCLUDEFILE=`getrepoexcludefile "$f"` if [[ ! -z $RESTIC_EXCLUDEFILE ]]; then OTHEROPTS="$OTHEROPTS --exclude-file=${RESTIC_EXCLUDEFILE}" fi OTHEROPTS="$OTHEROPTS $RESTICOPTS" elif [[ $mode == "rsync" ]]; then # rsync MYREPO="rsync: $REPO_PATH" export RSYNC_REPOSITORY="$REPO_PATH" export RSYNC_FULLDIR="${DEF_RSYNC_DIR}/${REPO}" if [[ $USINGDEFAULT -eq 1 ]]; then list="DEF_RSYNC_SERVER DEF_RSYNC_USER DEF_RSYNC_DIR RSYNC_OPTIONS" else list="RSYNC_OPTIONS" fi for xx in $list; do eval val='$'$xx if [[ -z $val ]]; then error "\$$xx not set. Please update ${RCFILE}." echo "" usage-rc exit 1 fi done if [[ -z $KSPEED ]]; then SPEEDOPTS="" else SPEEDOPTS="--bwlimit=${KSPEED}k" fi CONNECTIONSOPTS="" else # rclone MYREPO="rclone: $REPO_PATH" export RCLONE_REPOSITORY="$REPO_PATH" if [[ -z $KSPEED ]]; then SPEEDOPTS="" else SPEEDOPTS="--bwlimit=${KSPEED}k" fi if [[ -z $CONNECTIONS ]]; then CONNECTIONSOPTS="" else CONNECTIONSOPTS="--transfers=${CONNECTIONS}" fi fi if [[ ! $CMD == "check" ]]; then text="Starting '$CMD' on repo '$REPO' [$DATAPATH <-> $MYREPO]" log "$text" fi if [[ $CMD == "check" ]]; then line=`egrep "^$REPO:go:" $STATFILE 2>/dev/null` if [[ $? -eq 0 ]]; then # Get result code=`echo "$line" | cut -d: -f3` stamp=`echo "$line" | cut -d: -f4` date --version 2>&1 | grep -q GNU if [[ $? -eq 0 ]]; then humanstamp=`date --date="@$stamp"` else humanstamp=`date -r $stamp` fi now=`date +%s` age=$( echo "$now - $stamp" | bc ) oneday=$( echo "24 * 60 * 60" | bc) if [[ $code -eq 0 ]]; then if [[ $age -le $oneday ]]; then if [[ $TELEGRAM -eq 1 ]]; then echo "**OK**: Last backup for __${REPO}__ succeeded on $humanstamp." else cecho "$GREEN" "OK: Last backup for $BOLD${REPO}$PLAIN$GREEN succeeded on $humanstamp." fi rv=0 else if [[ $TELEGRAM -eq 1 ]]; then echo "**CRITICAL**: Last backup for __${REPO}__ succeeded on $humanstamp (age $age not in last 24 hours)." else cecho "$RED" "CRITICAL: Last backup for $BOLD${REPO}${PLAIN}${RED} succeeded on $humanstamp (age $age not in last 24 hours)." fi errcount=$(( $errcount + 1)) rv=2 fi else if [[ $TELEGRAM -eq 1 ]]; then echo "**CRITICAL**: Last backup for __${REPO}__ failed on $humanstamp." else cecho "$RED" "CRITICAL: Last backup for $BOLD${REPO}$PLAIN$RED failed on $humanstamp." fi rv=2 errcount=$(( $errcount + 1)) fi else if [[ $TELEGRAM -eq 1 ]]; then echo "**WARNING**: No history found for repo __${REPO}__" else cecho "$YELLOW" "WARNING: No history found for repo $BOLD$REPO$PLAIN$YELLOW'" fi rv=1 warncount=$(( $warncount + 1)) fi elif [[ $mode == "restic" ]]; then if [[ $CMD == "go" ]]; then if checktag "$f" postgres; then btype=postgres elif checktag "$f" mysql; then btype=mysql else btype=normal fi if [[ $CRONMODE -eq 1 ]]; then if [[ $VERBOSE -eq 1 ]]; then echo "debug: running: [${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH" >>${LOG} echo "debug: full log is in ${FULLLOGFILE}" >>${LOG} if [[ $btype == "postgres" || $btype == "mysql" ]] ; then $DBDUMPCMD $DBOPTS | ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS --stdin-filename $DBFILENAME 2>&1 >> ${FULLLOGFILE} rv=$? else ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH 2>&1 >> ${FULLLOGFILE} rv=$? fi egrep "^(Added|processed|snapshot)" ${FULLLOGFILE} >>${LOG} else if [[ $btype == "postgres" || $btype == "mysql" ]] ; then $DBDUMPCMD $DBOPTS | ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS --stdin-filename $DBFILENAME 2>&1 | egrep "^(Added|processed|snapshot)" >> ${LOG} rv=${PIPESTATUS[0]} else ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH 2>&1 | egrep "^(Added|processed|snapshot)" >> ${LOG} rv=${PIPESTATUS[0]} fi fi else if [[ $btype == "postgres" || $btype == "mysql" ]] ; then [[ $VERBOSE -eq 1 ]] && log "debug: running: [$DBDUMPCMD $DBOPTS | ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS --stdin-filename $DBFILENAME" $DBDUMPCMD $DBOPTS | ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS --stdin-filename $DBFILENAME 2>&1 >> ${LOG} rv=$? else [[ $VERBOSE -eq 1 ]] && log "debug: running: [${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH" ${RESTIC} backup $AUTHOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH 2>&1 >> ${LOG} rv=$? fi fi elif [[ $CMD == "get" ]]; then RESTOREDIR="$RESTOREDIRBASE/$f/" snaps=$(${RESTIC} snapshots $AUTHOPTS $CONNECTIONSOPTS 2>&1 | grep -v ID | awk '(NF >= 4) { print }') if checktag "$f" postgres; then snaps=$(echo "$snaps" | grep -w postgres) elif checktag "$f" mysql; then snaps=$(echo "$snaps" | grep -w mysql) else snaps=$(echo "$snaps" | egrep -vw "postgres|mysql") fi snapids=$(echo "$snaps" | awk '{ print $1 }') defsid=$(echo "$snapids" | tail -1) if [[ -z $RESTORESNAPID ]]; then cecho "$BOLD$CYAN" "Found these snapshots:" echo "$snaps" RESTORESNAPID="xxxxxx" while [[ ! $snaps == *$RESTORESNAPID* ]]; do read -p "snapshot [$defsid]: " RESTORESNAPID if [[ $RESTORESNAPID == "latest" || -z $RESTORESNAPID ]]; then RESTORESNAPID=$defsid fi done elif [[ $RESTORESNAPID == "latest" ]]; then RESTORESNAPID=$defsid elif [[ ! $snaps == *$RESTORESNAPID* ]]; then error "snapshot '$RESTORESNAPID' not found." echo cecho "$RED" "Valid snapshots for repo $f:" cecho "$RED" "$snaps" exit 1 fi if [[ -z $RESTORELIST ]]; then action "Opening repo for snapshot $BOLD$RESTORESNAPID$PLAIN$CYAN..." wd=/ updatedir c="ls" doit=0 while [[ ! $c == "x" && ! c == "q" ]]; do if [[ $c == "ls" ]]; then echo "$contents" elif [[ $c == "go" ]]; then if [[ -z $RESTORELIST ]]; then cecho "$RED" "Nothing to restore!" else doit=1 fi break elif [[ $c == "x" || $c == "q" ]]; then doit=0 break elif [[ $c == "?" || $c == "h" ]]; then restorecmd_help elif [[ -z $c ]]; then true elif [[ $c == "dirs" ]]; then echo "subdirs:" echo "$subdirs" | sed -e 's/^/ /' elif [[ $c == w* ]]; then showrestoreinfo elif [[ $c == a* || $c == mark* ]]; then file=$(echo "$c" | awk '{ print $2 }') # remove leading / file=$(echo $file | sed -e 's,^/,,') if [[ -z $file ]]; then error "no file provided" else echo "$RESTORELIST" | tr ' ' '\n' | egrep -q "^$wd$file$" thisrv=$? if [[ $thisrv -eq 0 ]]; then error "'$file' already in restore list" else if [[ $file == */* ]]; then # add without checking if [[ -z $RESTORELIST ]]; then RESTORELIST="$wd$file" else RESTORELIST="$RESTORELIST $wd$file" fi action "Added to restore list: $BOLD$file$PLAIN$CYAN (no verify)" else echo "$subfiles" | grep -qw "$file" rv=$? if [[ $rv -eq 0 ]]; then RESTORELIST="$RESTORELIST $wd$file" action "Added to restore list: $BOLD$file" else error "'$file' doesn't exist" fi fi fi fi elif [[ $c == d* || $c == unmark* ]]; then file=$(echo "$c" | awk '{ print $2 }') file="$wd$file" echo "$RESTORELIST" | tr ' ' '\n' | egrep -q "^$file$" if [[ $? -eq 0 ]]; then #RESTORELIST=$(echo "$RESTORELIST" | sed -e 's/^ //' | tr ' ' '\n' | grep -wv $file | tr '\n' ' ' | egrep -v "^$" | sed -e 's/^/ /') RESTORELIST=$(echo "$RESTORELIST" | sed -e "s, $file,,") action "Removed from restore list: $BOLD$file" else error "Restore list doesn't contain '$file'" fi elif [[ $c == cd* ]]; then newdir=$(echo "$c" | awk '{ print $2 }') olddir=$wd needupdate=1 if [[ $newdir == "/" ]]; then wd=$newdir rv=0 elif [[ $newdir == /* ]]; then wd=$newdir/ updatedir rv=$? needupdate=0 elif [[ $newdir == */* ]]; then wd=$wd$newdir/ updatedir rv=$? needupdate=0 elif [[ $newdir == ".." ]]; then wd=$(dirname $wd) rv=0 else echo "$subdirs" | grep -qw "$newdir" rv=$? if [[ $rv -eq 0 ]]; then wd=$wd$newdir/ fi fi if [[ $rv -eq 0 ]]; then [[ $needupdate -eq 1 ]] && updatedir else error "no such directory '$newdir'" wd=$olddir fi elif [[ $c == lcd* ]]; then newlocaldir=$(echo "$c" | awk '{ print $2 }') if [[ -d $newlocaldir ]]; then RESTOREDIR="$newlocaldir" elif [[ -e $newlocaldir ]]; then error "'$newlocaldir' is not a directory" else error "'$newlocaldir' does not exist" fi else error "bad command '$c' (? for help)" fi echo action "Restore path: $RESTOREDIR" echo -en "$CYAN$wd> $PLAIN" read c done else # restorelist not empty doit=1 fi if [[ $doit -eq 1 ]]; then showrestoreinfo action "Performing restore..." echo RESTOREOPTS="" for r in $RESTORELIST; do RESTOREOPTS="$RESTOREOPTS -i $r" done [[ $VERBOSE -eq 1 ]] && log "debug: running: ${RESTIC} restore $AUTHOPTS $CONNECTIONSOPTS -t "$RESTOREDIR" $RESTOREOPTS $RESTORESNAPID" ${RESTIC} restore $AUTHOPTS $CONNECTIONSOPTS -t "$RESTOREDIR" $RESTOREOPTS $RESTORESNAPID 2>&1 >> ${LOG} rv=$? if [[ $rv -eq 0 ]]; then # check that we got them all tried=0 success=0 failed=0 for r in $RESTORELIST; do tried=$((tried + 1)) if [[ -e $RESTOREDIR/$r ]]; then success=$((success + 1)) else failed=$((failed + 1)) fi done echo echo -en "${CYAN}${BOLD}Restored $success / $tried items$PLAIN" [[ $failed -gt 0 ]] && cecho "$RED" " ($failed failed)" || echo echo if [[ $success -eq $tried ]]; then rv=0 else rv=1 fi fi else action "${BOLD}Aborting..." rv=1 fi elif [[ $CMD == "ls" ]]; then [[ $VERBOSE -eq 1 ]] && log "debug: running: ${RESTIC} snapshots $AUTHOPTS $CONNECTIONSOPTS $OTHEROPTS " ${RESTIC} snapshots $AUTHOPTS $CONNECTIONSOPTS 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "stats" ]]; then ${RESTIC} stats $AUTHOPTS $CONNECTIONSOPTS 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "diff" ]]; then log "Error: diff not supported in restic." rv=1 elif [[ $CMD == "init" ]]; then # check whether it already exists first! ${RESTIC} stats $AUTHOPTS $CONNECTIONSOPTS >/dev/null 2>&1 if [ $? -eq 0 ]; then log "ERROR: Repo ${REPO} already exists. Aborting." rv=1 else cmdtorun="${RESTIC} init $AUTHOPTS $CONNECTIONSOPTS" if [[ $VERBOSE -eq 1 ]]; then log "export RESTIC_REPOSITORY=\"$REPO_PATH\"" log "export RESTIC_PASSWORD_FILE=`getrepopassfile \"$f\"`" log "will run: [${cmdtorun}]" fi errtext=`${RESTIC} init $AUTHOPTS $CONNECTIONSOPTS 2>&1` rv=$? fi elif [[ $CMD == "forget" ]]; then # age out stuff after RESTIC_KEEPDAYS days FORGETOPTS="-d ${RESTIC_KEEPDAYS}" ${RESTIC} forget $AUTHOPTS $CONNECTIONSOPTS $FORGETOPTS 2>&1 | awk '($1 == "Applying") { print } ($2 == "snapshots") { show=0 } (show == 1 && $1 != "ID" && NF > 1) { print "Removing: " $0 } ($1 == "remove") { show=1 }' >> ${LOG} rv=$? ${RESTIC} prune $AUTHOPTS 2>&1 | egrep "^(repository contain|found|will|remove)" >> ${LOG} else log "Error: invalid command $CMD" rv=1 fi elif [[ $mode == "rsync" ]]; then REMOTE="" if checktag "$f" postgres; then log "Error: postgres backups not supported for rsync mode." rv=1 elif checktag "$f" mysql; then log "Error: mysql backups not supported for rsync mode." rv=1 elif [[ $CMD == "go" ]]; then ${RSYNC} ${RSYNC_OPTIONS} $DATAPATH/ ${RSYNC_REPOSITORY} 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "ls" ]]; then ssh ${DEF_RSYNC_USER}@${DEF_RSYNC_SERVER} ls ${RSYNC_FULLDIR} 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "stats" ]]; then action "Size of remote data for ${REPO}:" ssh ${DEF_RSYNC_USER}@${DEF_RSYNC_SERVER} du -hs ${RSYNC_FULLDIR} 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "diff" ]]; then ${RSYNC} ${RSYNC_OPTIONS} -n $DATAPATH/ ${RSYNC_REPOSITORY} 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "init" ]]; then # check whether it already exists first! ssh ${DEF_RSYNC_USER}@${DEF_RSYNC_SERVER} ls -d ${RSYNC_FULLDIR} 2>&1 >> ${LOG} if [ $? -eq 0 ]; then log "ERROR: Repo ${REPO} already exists. Aborting." else ssh ${DEF_RSYNC_USER}@${DEF_RSYNC_SERVER} mkdir -p ${RSYNC_FULLDIR} 2>&1 >>${LOG} if [ $? -eq 0 ]; then log "Created directory ${RSYNC_FULLDIR}..." else log "Creation of ${RSYNC_FULLDIR} failed." fi fi elif [[ $CMD == "forget" ]]; then log "Error: forget not supported for $mode mode." rv=1 else log "Error: invalid command $CMD" rv=1 fi else # rclone REMOTE="" if checktag "$f" postgres; then log "Error: postgres backups not supported for $mode mode." rv=1 elif checktag "$f" mysql; then log "Error: mysql backups not supported for $mode mode." rv=1 elif [[ $CMD == "go" ]]; then ${RCLONE} sync $RCLONEOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $DATAPATH $RCLONE_REPOSITORY 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "ls" ]]; then ${RCLONE} ls $RCLONEOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $RCLONE_REPOSITORY 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "stats" ]]; then ${RCLONE} size $RCLONEOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $RCLONE_REPOSITORY 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "diff" ]]; then ${RCLONE} check $RCLONEOPTS $CONNECTIONSOPTS $SPEEDOPTS $OTHEROPTS $RCLONE_REPOSITORY 2>&1 >> ${LOG} rv=$? elif [[ $CMD == "init" ]]; then # check whether it already exists first! ${RCLONE} size $RCLONEOPTS $RCLONE_REPOSITORY >/dev/null 2>&1 if [ $? -eq 0 ]; then log "ERROR: Repo ${REPO} already exists. Aborting." else # does 'remote' exist? ${RCLONE} listremotes | grep -q remote: >/dev/null 2>&1 rv=$? if [ $rv -ne 0 ]; then log "Rclone remote doesn't exist - creating it..." ${RCLONE} $RCLONEOPTS config create remote b2 account $B2_APP_ID key $B2_APP_KEY >>${LOG} rv=$? if [ $rv -ne 0 ]; then log "Rclone remote init failed." exit 1 fi fi if [ $rv -eq 0 ]; then log "Creating B2 bucket for $RCLONE_REPOSITORY..." # create the bucket ${RCLONE} $RCLONEOPTS mkdir $RCLONE_REPOSITORY >>${LOG} else log "Rclone bucket creation of $RCLONE_REPOSITORY failed." fi fi elif [[ $CMD == "forget" ]]; then log "Error: forget not supported for rclone mode." rv=1 else log "Error: invalid command $CMD" rv=1 fi fi if [[ ! $CMD == "check" ]]; then # Record result updatestats $f $CMD $rv # did our command succeed? if [[ $rv -ne 0 ]]; then log "Error: '$CMD' on repo '$REPO' failed" if [[ ! -z $errtext ]]; then log "$errtext" fi fi log "Finished '$CMD' on repo '$REPO'" fi done # Clean up for f in $REPO_LIST; do check_datamount_needed "$f" # sets $NEEDMOUNT if [[ $NEEDMOUNT -eq 1 ]]; then do_umount fi check_repomount_needed "$f" # sets $MOUNTREPO if [[ $MOUNTREPO -eq 1 ]]; then path=`gettagval "$f" premount` if is_mounted $path; then umount $path if [[ $? -eq 0 ]]; then [[ ! $CMD == "check" ]] && log "Un-mount of '$path': success" else [[ ! $CMD == "check" ]] && log "Un-mount of '$path': FAILED" fi fi fi done if [[ $CMD == "check" ]]; then if [[ $errcount -ge 1 ]]; then rv=2 elif [[ $warncount -ge 1 ]]; then rv=1 else rv=0 fi exit $rv fi