Update to version 20261031-01
# 20261031-01 - Consolidated the two divergent 1.7 versions into a single # script. Adopted YYYYMMDD-xx versioning scheme. Version is now # defined once (SCRIPT_VERSION) and used in help and logs. # Removed SFTP upload; backups always go to Restic. # Restic snapshot details (ID, parent, tags, file/dir counts, # data added, snapshot size, duration) are now written to the # logs. restic backup now runs with --json and the details are # read from its backup summary. Added required-command check. # Temporary restic output is removed on failure or interrupt.
This commit is contained in:
1 parent
fac0d3202b
commit
ea149aa082
1 file changed
+179
-54
+179
-54
@@ -3,34 +3,57 @@
|
|||||||
# Script Name: AppBackup-restic.sh
|
# Script Name: AppBackup-restic.sh
|
||||||
# Description:
|
# Description:
|
||||||
# Creates a backup of a web application and optionally its MySQL database.
|
# Creates a backup of a web application and optionally its MySQL database.
|
||||||
# The script archives the application home directory and uploads the backup
|
# The script archives the application home directory locally and backs it
|
||||||
# either to a remote SFTP server or to a Backblaze B2 repository using Restic,
|
# up to a Backblaze B2 repository using Restic.
|
||||||
# depending on the host on which it is executed.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# AppBackup-restic.sh --appname <name> --appuser <user> [--apppassword <password>]
|
# AppBackup-restic.sh --appname <name> --appuser <user> [--apppassword <password>]
|
||||||
#
|
#
|
||||||
# Author: Charlie Silverman
|
# Author: Charlie Silverman
|
||||||
# Version: 1.7
|
# Version: 20261031-01
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# restic, jq, awk, tar, mysqldump (when --apppassword is used),
|
||||||
|
# mailx or mail (for email notifications), numfmt (optional, for
|
||||||
|
# human-readable sizes).
|
||||||
|
# Restic environment file: /root/.restic-env
|
||||||
|
#
|
||||||
|
# Versioning:
|
||||||
|
# Versions use the format YYYYMMDD-xx, where YYYYMMDD is the release date
|
||||||
|
# and xx is the release number for that day, starting at 01.
|
||||||
#
|
#
|
||||||
# Revision History:
|
# Revision History:
|
||||||
# 1.0 - Initial version.
|
# 1.0 - Initial version.
|
||||||
# 1.1 - Added logging to file.
|
# 1.1 - Added logging to file.
|
||||||
# 1.2 - Added strict error handling, input validation, and FQDN support.
|
# 1.2 - Added strict error handling, input validation, and FQDN support.
|
||||||
# 1.3 - Added trap-based error logging and improved operational visibility.
|
# 1.3 - Added trap-based error logging and improved operational visibility.
|
||||||
# 1.4 - Added application-specific log files, interrupt handling,
|
# 1.4 - Added application-specific log files, interrupt handling,
|
||||||
# and enhanced logging.
|
# and enhanced logging.
|
||||||
# 1.5 - Added command-line help.
|
# 1.5 - Added command-line help.
|
||||||
# 1.6 - Replaced positional parameters with named command-line options
|
# 1.6 - Replaced positional parameters with named command-line options
|
||||||
# and added support for short/long option formats.
|
# and added support for short/long option formats.
|
||||||
# 1.7 - Added email delivery of current execution log on success,
|
# 1.7 (a) - Added --no-tablespaces to mysqldump. Added --single-transaction
|
||||||
# failure, or interruption.
|
# to eliminate the need to lock tables during backup.
|
||||||
|
# 1.7 (b) - Added email delivery of current execution log on success,
|
||||||
|
# failure, or interruption.
|
||||||
|
# 20261031-01 - Consolidated the two divergent 1.7 versions into a single
|
||||||
|
# script. Adopted YYYYMMDD-xx versioning scheme. Version is now
|
||||||
|
# defined once (SCRIPT_VERSION) and used in help and logs.
|
||||||
|
# Removed SFTP upload; backups always go to Restic.
|
||||||
|
# Restic snapshot details (ID, parent, tags, file/dir counts,
|
||||||
|
# data added, snapshot size, duration) are now written to the
|
||||||
|
# logs. restic backup now runs with --json and the details are
|
||||||
|
# read from its backup summary. Added required-command check.
|
||||||
|
# Temporary restic output is removed on failure or interrupt.
|
||||||
#
|
#
|
||||||
|
|
||||||
set -o errexit
|
set -o errexit
|
||||||
set -o nounset
|
set -o nounset
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
|
SCRIPT_VERSION="20261031-01"
|
||||||
|
NOTIFY_EMAIL="ackitsme3@gmail.com"
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
# Help Function
|
# Help Function
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
@@ -39,12 +62,12 @@ show_help() {
|
|||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
AppBackup-restic.sh v1.7
|
AppBackup-restic.sh v${SCRIPT_VERSION}
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
Creates a backup of a web application's files and optionally its
|
Creates a backup of a web application's files and optionally its
|
||||||
MySQL database. The backup is uploaded either via SFTP or Restic,
|
MySQL database. The backup is sent to a Backblaze B2 repository
|
||||||
depending on the host.
|
using Restic.
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
|
|
||||||
@@ -62,7 +85,7 @@ OPTIONS
|
|||||||
Required.
|
Required.
|
||||||
|
|
||||||
-p, --apppassword <password>
|
-p, --apppassword <password>
|
||||||
MySQL password and SFTP password (if applicable).
|
MySQL password. If omitted, the database backup is skipped.
|
||||||
Optional.
|
Optional.
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
@@ -70,13 +93,13 @@ OPTIONS
|
|||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
|
|
||||||
AppBackup-restic.sh \
|
AppBackup-restic.sh \\
|
||||||
--appname KISNetManager \
|
--appname KISNetManager \\
|
||||||
--appuser kisuser \
|
--appuser kisuser \\
|
||||||
--apppassword MySecurePassword
|
--apppassword MySecurePassword
|
||||||
|
|
||||||
AppBackup-restic.sh \
|
AppBackup-restic.sh \\
|
||||||
-a CustomerPortal \
|
-a CustomerPortal \\
|
||||||
-u portaluser
|
-u portaluser
|
||||||
|
|
||||||
LOG FILES
|
LOG FILES
|
||||||
@@ -91,7 +114,7 @@ EMAIL NOTIFICATIONS
|
|||||||
|
|
||||||
Current execution logs are emailed to:
|
Current execution logs are emailed to:
|
||||||
|
|
||||||
ackitsme3@gmail.com
|
${NOTIFY_EMAIL}
|
||||||
|
|
||||||
Success, failure, and interruption events each generate
|
Success, failure, and interruption events each generate
|
||||||
a separate email.
|
a separate email.
|
||||||
@@ -102,7 +125,7 @@ AUTHOR
|
|||||||
|
|
||||||
VERSION
|
VERSION
|
||||||
|
|
||||||
1.7
|
${SCRIPT_VERSION}
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -188,6 +211,7 @@ SAFE_APPNAME=$(echo "$AppName" | tr -cs '[:alnum:]_-' '_')
|
|||||||
|
|
||||||
LOGFILE="/tmp/AppBackup-${SAFE_APPNAME}.log"
|
LOGFILE="/tmp/AppBackup-${SAFE_APPNAME}.log"
|
||||||
RUNLOG="/tmp/AppBackup-${SAFE_APPNAME}-${DATE}.log"
|
RUNLOG="/tmp/AppBackup-${SAFE_APPNAME}-${DATE}.log"
|
||||||
|
RESTIC_OUTPUT=""
|
||||||
|
|
||||||
touch "$RUNLOG"
|
touch "$RUNLOG"
|
||||||
|
|
||||||
@@ -214,11 +238,11 @@ send_email_log() {
|
|||||||
|
|
||||||
if command -v mailx >/dev/null 2>&1; then
|
if command -v mailx >/dev/null 2>&1; then
|
||||||
|
|
||||||
mailx -s "$subject" ackitsme3@gmail.com < "$RUNLOG"
|
mailx -s "$subject" "$NOTIFY_EMAIL" < "$RUNLOG"
|
||||||
|
|
||||||
elif command -v mail >/dev/null 2>&1; then
|
elif command -v mail >/dev/null 2>&1; then
|
||||||
|
|
||||||
mail -s "$subject" ackitsme3@gmail.com < "$RUNLOG"
|
mail -s "$subject" "$NOTIFY_EMAIL" < "$RUNLOG"
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
@@ -227,6 +251,80 @@ send_email_log() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Convert a byte count to a human-readable size (e.g. 1.23GiB)
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
human_bytes() {
|
||||||
|
|
||||||
|
local bytes="${1:-0}"
|
||||||
|
|
||||||
|
if command -v numfmt >/dev/null 2>&1; then
|
||||||
|
numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \
|
||||||
|
|| echo "${bytes} B"
|
||||||
|
else
|
||||||
|
echo "${bytes} B"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Write Restic snapshot details from a backup summary (JSON) to the log
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
log_snapshot_info() {
|
||||||
|
|
||||||
|
local summary="$1"
|
||||||
|
|
||||||
|
local snapshot_id parent_id
|
||||||
|
local files_new files_changed files_unmodified
|
||||||
|
local dirs_new dirs_changed dirs_unmodified
|
||||||
|
local data_added data_added_packed
|
||||||
|
local total_files total_bytes total_duration
|
||||||
|
|
||||||
|
snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary")
|
||||||
|
parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \
|
||||||
|
| jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
files_new=$(jq -r '.files_new // 0' <<< "$summary")
|
||||||
|
files_changed=$(jq -r '.files_changed // 0' <<< "$summary")
|
||||||
|
files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary")
|
||||||
|
dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary")
|
||||||
|
dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary")
|
||||||
|
dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary")
|
||||||
|
data_added=$(jq -r '.data_added // 0' <<< "$summary")
|
||||||
|
data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary")
|
||||||
|
total_files=$(jq -r '.total_files_processed // 0' <<< "$summary")
|
||||||
|
total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
|
||||||
|
total_duration=$(jq -r '.total_duration // 0' <<< "$summary")
|
||||||
|
|
||||||
|
log "---------------- Restic Snapshot -----------------"
|
||||||
|
log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})"
|
||||||
|
log "Parent snapshot: ${parent_id:0:8}"
|
||||||
|
log "Tags: apache, website, ${AppName}, ${HOSTNAME_FQDN}"
|
||||||
|
log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified"
|
||||||
|
log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified"
|
||||||
|
if [[ -n "$data_added_packed" ]]; then
|
||||||
|
log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)"
|
||||||
|
else
|
||||||
|
log "Data added: $(human_bytes "$data_added")"
|
||||||
|
fi
|
||||||
|
log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files"
|
||||||
|
log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds"
|
||||||
|
log "--------------------------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Remove temporary Restic output
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
cleanup_restic_output() {
|
||||||
|
|
||||||
|
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
|
||||||
|
rm -f "${RESTIC_OUTPUT}"
|
||||||
|
RESTIC_OUTPUT=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
# Error Handling
|
# Error Handling
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
@@ -238,6 +336,8 @@ error_handler() {
|
|||||||
|
|
||||||
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
|
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
|
||||||
|
|
||||||
|
cleanup_restic_output
|
||||||
|
|
||||||
send_email_log \
|
send_email_log \
|
||||||
"FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
|
"FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
|
||||||
|
|
||||||
@@ -248,6 +348,8 @@ interrupt_handler() {
|
|||||||
|
|
||||||
log "ERROR: Backup interrupted by signal"
|
log "ERROR: Backup interrupted by signal"
|
||||||
|
|
||||||
|
cleanup_restic_output
|
||||||
|
|
||||||
send_email_log \
|
send_email_log \
|
||||||
"INTERRUPTED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
|
"INTERRUPTED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
|
||||||
|
|
||||||
@@ -262,7 +364,7 @@ trap interrupt_handler SIGINT SIGTERM
|
|||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
log "================================================================="
|
log "================================================================="
|
||||||
log "Starting backup job"
|
log "Starting backup job (AppBackup-restic.sh v${SCRIPT_VERSION})"
|
||||||
log "Application: $AppName"
|
log "Application: $AppName"
|
||||||
log "Application User: $AppUser"
|
log "Application User: $AppUser"
|
||||||
log "Host: $HOSTNAME_FQDN"
|
log "Host: $HOSTNAME_FQDN"
|
||||||
@@ -270,6 +372,25 @@ log "Historical Log: $LOGFILE"
|
|||||||
log "Run Log: $RUNLOG"
|
log "Run Log: $RUNLOG"
|
||||||
log "================================================================="
|
log "================================================================="
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Verify Requirements
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for cmd in restic jq awk tar; do
|
||||||
|
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
|
||||||
|
log "ERROR: Missing required command: $cmd"
|
||||||
|
|
||||||
|
send_email_log \
|
||||||
|
"FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
# Restic Repository Configuration
|
# Restic Repository Configuration
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
@@ -312,6 +433,8 @@ if [[ -n "$AppPassword" ]]; then
|
|||||||
mkdir -p "/home/${AppUser}/SQLBackup"
|
mkdir -p "/home/${AppUser}/SQLBackup"
|
||||||
|
|
||||||
mysqldump \
|
mysqldump \
|
||||||
|
--no-tablespaces \
|
||||||
|
--single-transaction \
|
||||||
-u "$AppUser" \
|
-u "$AppUser" \
|
||||||
--password="$AppPassword" \
|
--password="$AppPassword" \
|
||||||
"$AppName" \
|
"$AppName" \
|
||||||
@@ -350,37 +473,39 @@ log "Archive created successfully"
|
|||||||
# Upload Backup
|
# Upload Backup
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
if [[ "$HOSTNAME_FQDN" != "ackitserver.ackcentuate.com" ]]; then
|
log "Loading Restic environment"
|
||||||
|
|
||||||
if [[ -z "$AppPassword" ]]; then
|
source /root/.restic-env
|
||||||
log "ERROR: SFTP upload requires --apppassword"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Uploading archive via SFTP"
|
log "Starting Restic backup"
|
||||||
|
|
||||||
lftp "sftp://${AppUser}:${AppPassword}@webbuilder.ackcentuate.com" -e \
|
# Capture restic's JSON output in a temporary file. Errors and warnings
|
||||||
"cd /home/Backups; put /home/Backups/${HostPrefix}${ArchiveFile}; bye"
|
# still go to the run log via stderr.
|
||||||
|
RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)"
|
||||||
|
|
||||||
log "SFTP upload completed successfully"
|
restic backup "/home/${AppUser}/" \
|
||||||
|
--json \
|
||||||
|
--tag apache \
|
||||||
|
--tag website \
|
||||||
|
--tag "$AppName" \
|
||||||
|
--tag "$HOSTNAME_FQDN" \
|
||||||
|
> "$RESTIC_OUTPUT" 2>> "$RUNLOG"
|
||||||
|
|
||||||
|
log "Restic backup completed successfully"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Log Restic Snapshot Details
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
|
||||||
|
"$RESTIC_OUTPUT" | tail -n 1)
|
||||||
|
|
||||||
|
cleanup_restic_output
|
||||||
|
|
||||||
|
if [[ -n "$summary" ]]; then
|
||||||
|
log_snapshot_info "$summary"
|
||||||
else
|
else
|
||||||
|
log "WARNING: Restic returned no snapshot summary for ${AppName}"
|
||||||
log "Loading Restic environment"
|
|
||||||
|
|
||||||
source /root/.restic-env
|
|
||||||
|
|
||||||
log "Starting Restic backup"
|
|
||||||
|
|
||||||
restic backup "/home/${AppUser}/" \
|
|
||||||
--tag apache \
|
|
||||||
--tag website \
|
|
||||||
--tag "$AppName" \
|
|
||||||
--tag "$HOSTNAME_FQDN" \
|
|
||||||
>> "$RUNLOG" 2>&1
|
|
||||||
|
|
||||||
log "Restic backup completed successfully"
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user