diff --git a/AppBackup-restic.sh b/AppBackup-restic.sh index 6c14a04..6e016d6 100644 --- a/AppBackup-restic.sh +++ b/AppBackup-restic.sh @@ -3,34 +3,57 @@ # Script Name: AppBackup-restic.sh # Description: # Creates a backup of a web application and optionally its MySQL database. -# The script archives the application home directory and uploads the backup -# either to a remote SFTP server or to a Backblaze B2 repository using Restic, -# depending on the host on which it is executed. +# The script archives the application home directory locally and backs it +# up to a Backblaze B2 repository using Restic. # # Usage: # AppBackup-restic.sh --appname --appuser [--apppassword ] # # 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: -# 1.0 - Initial version. -# 1.1 - Added logging to file. -# 1.2 - Added strict error handling, input validation, and FQDN support. -# 1.3 - Added trap-based error logging and improved operational visibility. -# 1.4 - Added application-specific log files, interrupt handling, -# and enhanced logging. -# 1.5 - Added command-line help. -# 1.6 - Replaced positional parameters with named command-line options -# and added support for short/long option formats. -# 1.7 - Added email delivery of current execution log on success, -# failure, or interruption. +# 1.0 - Initial version. +# 1.1 - Added logging to file. +# 1.2 - Added strict error handling, input validation, and FQDN support. +# 1.3 - Added trap-based error logging and improved operational visibility. +# 1.4 - Added application-specific log files, interrupt handling, +# and enhanced logging. +# 1.5 - Added command-line help. +# 1.6 - Replaced positional parameters with named command-line options +# and added support for short/long option formats. +# 1.7 (a) - Added --no-tablespaces to mysqldump. Added --single-transaction +# 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 nounset set -o pipefail +SCRIPT_VERSION="20261031-01" +NOTIFY_EMAIL="ackitsme3@gmail.com" + #------------------------------------------------------------------------------ # Help Function #------------------------------------------------------------------------------ @@ -39,12 +62,12 @@ show_help() { cat << EOF -AppBackup-restic.sh v1.7 +AppBackup-restic.sh v${SCRIPT_VERSION} DESCRIPTION Creates a backup of a web application's files and optionally its - MySQL database. The backup is uploaded either via SFTP or Restic, - depending on the host. + MySQL database. The backup is sent to a Backblaze B2 repository + using Restic. USAGE @@ -62,7 +85,7 @@ OPTIONS Required. -p, --apppassword - MySQL password and SFTP password (if applicable). + MySQL password. If omitted, the database backup is skipped. Optional. -h, --help @@ -70,13 +93,13 @@ OPTIONS EXAMPLES - AppBackup-restic.sh \ - --appname KISNetManager \ - --appuser kisuser \ + AppBackup-restic.sh \\ + --appname KISNetManager \\ + --appuser kisuser \\ --apppassword MySecurePassword - AppBackup-restic.sh \ - -a CustomerPortal \ + AppBackup-restic.sh \\ + -a CustomerPortal \\ -u portaluser LOG FILES @@ -91,7 +114,7 @@ EMAIL NOTIFICATIONS Current execution logs are emailed to: - ackitsme3@gmail.com + ${NOTIFY_EMAIL} Success, failure, and interruption events each generate a separate email. @@ -102,7 +125,7 @@ AUTHOR VERSION - 1.7 + ${SCRIPT_VERSION} EOF @@ -188,6 +211,7 @@ SAFE_APPNAME=$(echo "$AppName" | tr -cs '[:alnum:]_-' '_') LOGFILE="/tmp/AppBackup-${SAFE_APPNAME}.log" RUNLOG="/tmp/AppBackup-${SAFE_APPNAME}-${DATE}.log" +RESTIC_OUTPUT="" touch "$RUNLOG" @@ -214,11 +238,11 @@ send_email_log() { 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 - mail -s "$subject" ackitsme3@gmail.com < "$RUNLOG" + mail -s "$subject" "$NOTIFY_EMAIL" < "$RUNLOG" else @@ -227,6 +251,80 @@ send_email_log() { 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 #------------------------------------------------------------------------------ @@ -238,6 +336,8 @@ error_handler() { log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}" + cleanup_restic_output + send_email_log \ "FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})" @@ -248,6 +348,8 @@ interrupt_handler() { log "ERROR: Backup interrupted by signal" + cleanup_restic_output + send_email_log \ "INTERRUPTED: AppBackup ${AppName} (${HOSTNAME_FQDN})" @@ -262,7 +364,7 @@ trap interrupt_handler SIGINT SIGTERM #------------------------------------------------------------------------------ log "=================================================================" -log "Starting backup job" +log "Starting backup job (AppBackup-restic.sh v${SCRIPT_VERSION})" log "Application: $AppName" log "Application User: $AppUser" log "Host: $HOSTNAME_FQDN" @@ -270,6 +372,25 @@ log "Historical Log: $LOGFILE" log "Run Log: $RUNLOG" 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 #------------------------------------------------------------------------------ @@ -312,6 +433,8 @@ if [[ -n "$AppPassword" ]]; then mkdir -p "/home/${AppUser}/SQLBackup" mysqldump \ + --no-tablespaces \ + --single-transaction \ -u "$AppUser" \ --password="$AppPassword" \ "$AppName" \ @@ -350,37 +473,39 @@ log "Archive created successfully" # Upload Backup #------------------------------------------------------------------------------ -if [[ "$HOSTNAME_FQDN" != "ackitserver.ackcentuate.com" ]]; then +log "Loading Restic environment" - if [[ -z "$AppPassword" ]]; then - log "ERROR: SFTP upload requires --apppassword" - exit 1 - fi +source /root/.restic-env - log "Uploading archive via SFTP" +log "Starting Restic backup" - lftp "sftp://${AppUser}:${AppPassword}@webbuilder.ackcentuate.com" -e \ - "cd /home/Backups; put /home/Backups/${HostPrefix}${ArchiveFile}; bye" +# Capture restic's JSON output in a temporary file. Errors and warnings +# 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 - - 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" - + log "WARNING: Restic returned no snapshot summary for ${AppName}" fi #------------------------------------------------------------------------------ @@ -424,4 +549,4 @@ send_email_log \ rm -f "$RUNLOG" -exit 0 \ No newline at end of file +exit 0