Initial upload
first build
This commit is contained in:
1 parent
ea149aa082
commit
3c0639f940
1 file changed
+322
@@ -0,0 +1,322 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script Name: openwrtupload.sh
|
||||
# Description:
|
||||
# Backs up OpenWrt configuration archives to a Backblaze B2 repository
|
||||
# using Restic. Each .gz archive found in the source directory is
|
||||
# extracted to a staging directory, backed up with Restic, and then
|
||||
# removed along with the staging directory.
|
||||
#
|
||||
# Usage:
|
||||
# openwrtupload.sh
|
||||
#
|
||||
# Author: Charlie Silverman
|
||||
# Version: 20261031-01
|
||||
#
|
||||
# Requirements:
|
||||
# restic, jq, awk, tar, 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:
|
||||
# 20261031-01 - Added script header and YYYYMMDD-xx versioning.
|
||||
# Added strict error handling with error/interrupt logging;
|
||||
# an archive is no longer deleted if its backup fails.
|
||||
# Staging directory is cleaned up on failure or interrupt.
|
||||
# Restic snapshot details (ID, parent, tags, file/dir counts,
|
||||
# data added, snapshot size, duration) are now written to the
|
||||
# log. restic backup now runs with --json and the details are
|
||||
# read from its backup summary. Added totals at end of run.
|
||||
# Added required-command check.
|
||||
#
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_VERSION="20261031-01"
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Configuration
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
LOG_FILE="/home/glibackup/openwrt-backup.log"
|
||||
SOURCE_DIR="/home/glibackup/openwrt"
|
||||
STAGING_DIR="/home/Backups/openwrt"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
HOSTNAME_FQDN="$(hostname -f)"
|
||||
|
||||
RESTIC_TAGS=(openwrt backup)
|
||||
|
||||
# Restic repository configuration
|
||||
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/openwrt"
|
||||
export RESTIC_ENV_FILE="/root/.restic-env"
|
||||
|
||||
RESTIC_OUTPUT=""
|
||||
STAGING_IN_USE=""
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Logging Function
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
log() {
|
||||
|
||||
local message
|
||||
|
||||
message="[$(date '+%F %T')] $*"
|
||||
|
||||
echo "$message"
|
||||
echo "$message" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# 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
|
||||
local tags
|
||||
|
||||
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")
|
||||
|
||||
tags=$(IFS=,; echo "${RESTIC_TAGS[*]}" | sed 's/,/, /g')
|
||||
|
||||
log "---------------- Restic Snapshot -----------------"
|
||||
log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})"
|
||||
log "Parent snapshot: ${parent_id:0:8}"
|
||||
log "Tags: ${tags}"
|
||||
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 "--------------------------------------------------"
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Cleanup Function
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
cleanup() {
|
||||
|
||||
if [[ -n "${STAGING_IN_USE:-}" && -d "$STAGING_DIR" ]]; then
|
||||
log "Cleaning up staging directory ${STAGING_DIR}"
|
||||
rm -rf "$STAGING_DIR" || log "WARNING: Failed to remove ${STAGING_DIR}"
|
||||
STAGING_IN_USE=""
|
||||
fi
|
||||
|
||||
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
|
||||
rm -f "$RESTIC_OUTPUT"
|
||||
RESTIC_OUTPUT=""
|
||||
fi
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Error Handling
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
error_handler() {
|
||||
|
||||
local exit_code=$?
|
||||
local line_number=$1
|
||||
|
||||
trap - ERR
|
||||
|
||||
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
|
||||
|
||||
cleanup
|
||||
|
||||
log "----------------------------------------------------"
|
||||
|
||||
exit "${exit_code}"
|
||||
}
|
||||
|
||||
interrupt_handler() {
|
||||
|
||||
trap - SIGINT SIGTERM
|
||||
|
||||
log "ERROR: Backup interrupted by signal"
|
||||
|
||||
cleanup
|
||||
|
||||
log "----------------------------------------------------"
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'error_handler $LINENO' ERR
|
||||
trap interrupt_handler SIGINT SIGTERM
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Startup Logging
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
log "===================================================="
|
||||
log "OpenWrt Backup Starting (openwrtupload.sh v${SCRIPT_VERSION})"
|
||||
log "Host: ${HOSTNAME_FQDN}"
|
||||
log "Source Directory: ${SOURCE_DIR}"
|
||||
log "Log File: ${LOG_FILE}"
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Load Restic Environment
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$RESTIC_ENV_FILE"
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Locate Archives
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
shopt -s nullglob
|
||||
ARCHIVES=("$SOURCE_DIR"/*.gz)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#ARCHIVES[@]} -eq 0 ]]; then
|
||||
log "No backup files found."
|
||||
log "----------------------------------------------------"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Found ${#ARCHIVES[@]} archive(s) to back up"
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Process Each Archive
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
TAG_ARGS=()
|
||||
for tag in "${RESTIC_TAGS[@]}"; do
|
||||
TAG_ARGS+=(--tag "$tag")
|
||||
done
|
||||
|
||||
total_bytes_all=0
|
||||
total_added_all=0
|
||||
archive_count=0
|
||||
|
||||
for FILE in "${ARCHIVES[@]}"; do
|
||||
|
||||
log "Backing up archive ${FILE}..."
|
||||
|
||||
# Start from an empty staging directory
|
||||
rm -rf "$STAGING_DIR"
|
||||
mkdir -p "$STAGING_DIR"
|
||||
STAGING_IN_USE="yes"
|
||||
|
||||
log "Extracting archive ${FILE} to ${STAGING_DIR}"
|
||||
|
||||
tar xzf "$FILE" -C "$STAGING_DIR"
|
||||
|
||||
log "Backing up ${STAGING_DIR} with restic"
|
||||
|
||||
# Capture restic's JSON output in a temporary file. Errors and
|
||||
# warnings still go to the log via stderr.
|
||||
RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)"
|
||||
|
||||
restic backup \
|
||||
"$STAGING_DIR" \
|
||||
--json \
|
||||
"${TAG_ARGS[@]}" \
|
||||
> "$RESTIC_OUTPUT" 2>> "$LOG_FILE"
|
||||
|
||||
summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
|
||||
"$RESTIC_OUTPUT" | tail -n 1)
|
||||
|
||||
rm -f "$RESTIC_OUTPUT"
|
||||
RESTIC_OUTPUT=""
|
||||
|
||||
if [[ -n "$summary" ]]; then
|
||||
log_snapshot_info "$summary"
|
||||
snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
|
||||
added_bytes=$(jq -r '.data_added // 0' <<< "$summary")
|
||||
else
|
||||
log "WARNING: Restic returned no snapshot summary for ${FILE}"
|
||||
snap_bytes=0
|
||||
added_bytes=0
|
||||
fi
|
||||
|
||||
total_bytes_all=$(awk -v a="$total_bytes_all" -v b="$snap_bytes" 'BEGIN{printf "%.0f", a + b}')
|
||||
total_added_all=$(awk -v a="$total_added_all" -v b="$added_bytes" 'BEGIN{printf "%.0f", a + b}')
|
||||
archive_count=$((archive_count + 1))
|
||||
|
||||
log "Cleaning up staging directory ${STAGING_DIR}"
|
||||
rm -rf "$STAGING_DIR"
|
||||
STAGING_IN_USE=""
|
||||
|
||||
log "Backup Complete."
|
||||
|
||||
log "Removing archive file ${FILE}"
|
||||
rm -f "$FILE"
|
||||
|
||||
log "----------------------------------------------------"
|
||||
|
||||
done
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Completion
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
log "All archives backed up successfully."
|
||||
log "Archives Processed: ${archive_count}"
|
||||
log "Total Backup Size: $(human_bytes "$total_bytes_all")"
|
||||
log "Total New Data Uploaded: $(human_bytes "$total_added_all")"
|
||||
log "===================================================="
|
||||
|
||||
exit 0
|
||||
Reference in new issue
Block a user