#!/bin/bash # Title: Perfacilis Incremental Back-up script # Description: Create back-ups of dirs and dbs by copying them to Perfacilis' back-up servers # We strongly recommend to put this in /etc/cron.hourly/backup # Author: Roy Arisse # See: https://github.com/perfacilis/backup # Version: 0.15.4 # Usage: bash /etc/cron.hourly/backup readonly BACKUP_LOCAL_DIR="/backup" readonly BACKUP_DIRS=("$BACKUP_LOCAL_DIR" /home /root /etc /var/www) readonly RSYNC_TARGET="username@backup.perfacilis.com::profile" readonly RSYNC_DEFAULTS="-trlqpz4 --delete --delete-excluded --prune-empty-dirs" readonly RSYNC_EXCLUDE=(tmp/ temp/ .cache/ cache/) readonly RSYNC_SECRET='RSYNCSECRETHERE' readonly DB_LIST=$(mariadb -Bse 'SHOW DATABASES') readonly DB_TABLE_LIST=() readonly DB_DUMP="mariadb-dump -E -R --max-allowed-packet=512MB -q --single-transaction -Q --skip-comments" readonly DB_ENCRYPTION_KEY="" # Amount of increments per interval and duration per interval resp. readonly -A INCREMENTS=([hourly]=24 [daily]=7 [weekly]=4 [monthly]=12 [yearly]=5) readonly -A DURATIONS=([hourly]=3600 [daily]=86400 [weekly]=604800 [monthly]=2419200 [yearly]=31536000) # ++++++++++ NO CHANGES REQUIRED BELOW THIS LINE ++++++++++ set -e export LC_ALL=C log() { local TAG="${0##*/}" local MSG="$*" logger -p local0.notice -t "$TAG" -- "$MSG" # Interactive shell if [ -t 0 ]; then echo "$MSG" fi } check_only_instance() { local LOCKFILE="$BACKUP_LOCAL_DIR/lock" local PID if [ -f "$LOCKFILE" ]; then PID=$(sed -r 's/[^0-9]//g' "$LOCKFILE") if [ -z "$PID" ] || ! ps q "$PID" > /dev/null; then log "Remove lockfile (no such process $PID)" rm "$LOCKFILE" else log "Already running as $PID" exit 0 fi fi echo $$ > $LOCKFILE trap "rm -f $LOCKFILE" EXIT SIGINT SIGTERM ERR } prepare_local_dir() { [ -d $BACKUP_LOCAL_DIR ] || mkdir -p $BACKUP_LOCAL_DIR } prepare_remote_dir() { local TARGET="$1" local RSYNC_OPTS=$(get_rsync_opts) local EMPTYDIR=$(mktemp -d) local DIR TREE if [ -z "$TARGET" ]; then echo "Usage: prepare_remote_dir remote/dir/structure" exit 1 fi # Remove options that delete empty dir RSYNC_OPTS=$(echo "$RSYNC_OPTS" | sed -E 's/--(delete|delete-excluded|prune-empty-dirs)//g') for DIR in ${TARGET//\// }; do TREE="$TREE/$DIR" rsync $RSYNC_OPTS $EMPTYDIR/ $RSYNC_TARGET/${TREE/#\//} done rm -rf $EMPTYDIR } get_last_inc_file() { local PERIOD="$1" if [ -z "$PERIOD" ]; then echo "Usage: ${FUNCTION[0]} daily" exit 1 fi echo "$BACKUP_LOCAL_DIR/last_inc_$PERIOD" } get_next_increment() { local PERIOD="$1" local LIMIT="${INCREMENTS[$PERIOD]}" local LAST NEXT INCFILE if [ -z "$PERIOD" -o -z "$LIMIT" ]; then echo "Usage: get_next_increment period" echo "- period = 'hourly', 'daily', 'weekly', 'monthly'" exit 1 fi INCFILE=$(get_last_inc_file $PERIOD) if [ -f "$INCFILE" ]; then LAST=$(cat "$INCFILE" | tr -d "\n") fi if [ -z "$LAST" ]; then echo 0 return fi NEXT=$(($LAST+1)) if [ "$NEXT" -ge "$LIMIT" ]; then echo 0 return fi echo $NEXT } # Return biggest interval to backup get_interval_to_backup() { local NOW=$(date +%s) local LAST PERIOD INCFILE DURATION DIFF local TODO="" # Sort associative array: biggest first for PERIOD in "${!DURATIONS[@]}"; do echo "${DURATIONS["$PERIOD"]} $PERIOD" done | sort -rn | while read DURATION PERIOD; do # Skip disabled intervals if [[ ${INCREMENTS[$PERIOD]} -eq 0 ]]; then continue; fi LAST=0 INCFILE=$(get_last_inc_file $PERIOD) if [ -f "$INCFILE" ]; then LAST=$(date -r "$INCFILE" +%s) fi DIFF=$((NOW - LAST)) if [ $DIFF -ge $DURATION ]; then echo "$PERIOD" break fi done } get_rsync_opts() { local EXCLUDE SECRET OPTS EXCLUDE=$(dirname $0)/rsync.exclude SECRET=$(dirname $0)/rsync.secret OPTS=$RSYNC_DEFAULTS if [ -n "$RSYNC_EXCLUDE" ]; then if [ ! -f $EXCLUDE ]; then printf '%s\n' "${RSYNC_EXCLUDE[@]}" > "$EXCLUDE" chmod 600 "$EXCLUDE" fi OPTS="$OPTS --exclude-from=$EXCLUDE" fi if [ ! -z "$RSYNC_SECRET" ]; then if [ ! -f "$SECRET" ]; then echo "$RSYNC_SECRET" > "$SECRET" chmod 600 "$SECRET" fi OPTS="$OPTS --password-file=$SECRET" fi echo "$OPTS" } backup_packagelist() { local TODO=$(get_interval_to_backup) if [ -z "$TODO" ]; then return fi log "Back-up list of installed packages" dpkg --get-selections > $BACKUP_LOCAL_DIR/packagelist.txt } backup_databases() { local TODO DB TODO=$(get_interval_to_backup) if [ -z "$TODO" ]; then return fi if [ -z "$DB_LIST" ] || [ -z "$DB_DUMP" ]; then log "Skipping database backup!" return fi log "Back-up databases:" for DB in $DB_LIST; do log "+ $DB" # If table dump returns false, dump entire database if ! backup_database_tables "$DB"; then if [ -n "$DB_ENCRYPTION_KEY" ]; then $DB_DUMP "$DB" | gzip --rsyncable -c | openssl smime -encrypt -binary -text -aes256 -out "$BACKUP_LOCAL_DIR/$DB.sql.gz.enc" -outform DER "$DB_ENCRYPTION_KEY" else $DB_DUMP "$DB" | gzip --rsyncable > "$BACKUP_LOCAL_DIR/$DB.sql.gz" fi fi done } backup_database_tables() { local DUMP_FILE DUMP_FILE_UPDATED local TABLE TABLE_UPDATED local DB="$1" if [ -z "$DB" ]; then echo "Usage: backup_database_tables database_name" exit 1 fi # If not DB_TABLE_LIST, return error to dump entire database if [ "${#DB_TABLE_LIST[@]}" -eq 0 ]; then return 1; fi # Dump file per table "${DB_TABLE_LIST[@]//%DB%/$DB}" | while read -r TABLE TABLE_UPDATED; do DUMP_FILE="$BACKUP_LOCAL_DIR/$DB.$TABLE.sql.gz" if [ -n "$DB_ENCRYPTION_KEY" ]; then DUMP_FILE+=".enc" fi TABLE_UPDATED=$(date -d "$TABLE_UPDATED" +%s 2>/dev/null || echo "9999999999") DUMP_FILE_UPDATED=$(date -r "$DUMP_FILE" +%s 2>/dev/null || echo "0") # Skip if dump file is later than table update time if [ "$DUMP_FILE_UPDATED" -ge "$TABLE_UPDATED" ]; then continue fi log " - $TABLE" if [ -n "$DB_ENCRYPTION_KEY" ]; then $DB_DUMP "$DB" "$TABLE" | gzip --rsyncable -c | openssl smime -encrypt -binary -text -aes256 -out "$DUMP_FILE" -outform DER "$DB_ENCRYPTION_KEY" else $DB_DUMP "$DB" "$TABLE" | gzip --rsyncable > "$DUMP_FILE" fi done } backup_folders() { local RSYNC_OPTS=$(get_rsync_opts) local DIR TARGET INC INCDIR local VANISHED='^(file has vanished: |rsync warning: some files vanished before they could be transferred)' local PERIOD=$(get_interval_to_backup) if [ -z "$PERIOD" ]; then log "No intervals to back-up yet." exit fi INC=$(get_next_increment $PERIOD) log "Moving $PERIOD back-up to target: $INC" prepare_remote_dir "current" for DIR in ${BACKUP_DIRS[@]}; do TARGET=${DIR/#\//} TARGET=${TARGET//\//_} # Make path absolute if target is not RSYNC profile # Also remove "user@server:" for SSH setups INCDIR="/$PERIOD/$INC/$TARGET" if [ -z "$RSYNC_SECRET" ]; then INCDIR="${RSYNC_TARGET##*:}$INCDIR" fi log "- $DIR" rsync $RSYNC_OPTS --backup --backup-dir=$INCDIR \ $DIR/ $RSYNC_TARGET/current/$TARGET 2>&1 | (egrep -v "$VANISHED" || true) done } signoff_increments() { local STARTTIME="$1" local PERIOD=$(get_interval_to_backup) local INC INCFILE INC=$(get_next_increment $PERIOD) INCFILE=$(get_last_inc_file $PERIOD) echo $INC > "$INCFILE" touch -t "$STARTTIME" "$INCFILE" } cleanup() { log "Cleanup" rm -f "$(dirname $0)/rsync.exclude" rm -f "$(dirname $0)/rsync.secret" exit } main() { starttime=$(date +%Y%m%d%H%M.%S) log "Back-up initiated at `date`" trap "cleanup" EXIT SIGINT SIGTERM ERR prepare_local_dir check_only_instance backup_packagelist backup_databases backup_folders signoff_increments $starttime log "Back-up completed at `date`" } main