306 lines
8.9 KiB
Bash
Executable File
306 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Generic Restic Repository Integrity Check Script
|
|
# Usage: ./restic-integrity-check.sh [OPTIONS] <repository>
|
|
|
|
set -euo pipefail
|
|
|
|
# Default values
|
|
RESTIC_PASSWORD_FILE="$HOME/.restic-password"
|
|
LOG_FILE=""
|
|
HEALTHCHECK_URL=""
|
|
READ_DATA=true
|
|
VERBOSE=false
|
|
DRY_RUN=false
|
|
|
|
# Function to display usage
|
|
usage() {
|
|
cat << EOF
|
|
Usage: $0 [OPTIONS] <repository>
|
|
|
|
Generic integrity check script for Restic repositories.
|
|
|
|
REQUIRED ARGUMENTS:
|
|
repository Restic repository URL (e.g., sftp:user@host:/path)
|
|
|
|
OPTIONS:
|
|
-p, --password-file FILE Password file path (default: ~/.restic-password)
|
|
-l, --log-file FILE Log file path (default: no logging)
|
|
-h, --healthcheck URL Healthchecks.io ping URL for monitoring
|
|
|
|
CHECK OPTIONS:
|
|
--no-read-data Skip reading and verifying data blobs (faster)
|
|
--read-data Verify data blobs (default, more thorough)
|
|
|
|
OTHER OPTIONS:
|
|
--dry-run Show what would be checked without doing it
|
|
-v, --verbose Enable verbose output
|
|
--help Show this help message
|
|
|
|
EXAMPLES:
|
|
# Basic integrity check
|
|
$0 sftp:user@host:/backups/music
|
|
|
|
# With healthcheck monitoring and custom log
|
|
$0 --healthcheck https://hc-ping.com/uuid \\
|
|
--log-file /var/log/integrity-check.log \\
|
|
sftp:user@host:/backups/documents
|
|
|
|
# Quick check without reading data (faster)
|
|
$0 --no-read-data --verbose \\
|
|
local:/path/to/repo
|
|
|
|
# Multiple repositories check (run separately)
|
|
for repo in repo1 repo2 repo3; do
|
|
$0 --verbose "sftp:user@host:/backups/\$repo"
|
|
done
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
RESTIC_PASSWORD_FILE Alternative to --password-file
|
|
RESTIC_PASSWORD Direct password (not recommended)
|
|
RESTIC_REPOSITORY Alternative to repository argument
|
|
|
|
NOTES:
|
|
- The --read-data option (default) performs a thorough check by reading
|
|
and verifying all data blobs. This is slower but more comprehensive.
|
|
- Use --no-read-data for faster checks that only verify repository
|
|
structure and metadata.
|
|
- Exit codes: 0 = success, 1 = check failed or error occurred
|
|
|
|
EOF
|
|
}
|
|
|
|
# Function to log messages
|
|
log_message() {
|
|
local message="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
|
|
if [ "$VERBOSE" = true ]; then
|
|
echo "$message"
|
|
fi
|
|
|
|
if [ -n "$LOG_FILE" ]; then
|
|
echo "$message" >> "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
# Function to send healthcheck ping
|
|
send_healthcheck() {
|
|
local status="$1"
|
|
local message="$2"
|
|
|
|
if [ -z "$HEALTHCHECK_URL" ]; then
|
|
[ "$VERBOSE" = true ] && echo "Healthcheck: $status - $message (no URL configured)"
|
|
return
|
|
fi
|
|
|
|
case "$status" in
|
|
"START")
|
|
curl -fsS -m 10 --retry 3 "$HEALTHCHECK_URL/start" >/dev/null 2>&1 || true
|
|
;;
|
|
"SUCCESS")
|
|
curl -fsS -m 10 --retry 3 --data-raw "$message" "$HEALTHCHECK_URL" >/dev/null 2>&1 || true
|
|
;;
|
|
"FAILED")
|
|
curl -fsS -m 10 --retry 3 --data-raw "$message" "$HEALTHCHECK_URL/fail" >/dev/null 2>&1 || true
|
|
;;
|
|
esac
|
|
|
|
[ "$VERBOSE" = true ] && echo "Healthcheck sent: $status - $message"
|
|
}
|
|
|
|
# Parse command line arguments
|
|
POSITIONAL_ARGS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-p|--password-file)
|
|
RESTIC_PASSWORD_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-l|--log-file)
|
|
LOG_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-h|--healthcheck)
|
|
HEALTHCHECK_URL="$2"
|
|
shift 2
|
|
;;
|
|
--no-read-data)
|
|
READ_DATA=false
|
|
shift
|
|
;;
|
|
--read-data)
|
|
READ_DATA=true
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
echo "Unknown option $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
POSITIONAL_ARGS+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Restore positional parameters
|
|
set -- "${POSITIONAL_ARGS[@]}"
|
|
|
|
# Check required arguments
|
|
if [ $# -lt 1 ]; then
|
|
echo "Error: Missing required repository argument"
|
|
echo
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
REPOSITORY="$1"
|
|
|
|
# Use environment variable as fallback for repository
|
|
if [ -n "${RESTIC_REPOSITORY:-}" ] && [ "$REPOSITORY" = "${RESTIC_REPOSITORY}" ]; then
|
|
REPOSITORY="$RESTIC_REPOSITORY"
|
|
fi
|
|
|
|
# Use environment variable as fallback for password file
|
|
if [ -n "${RESTIC_PASSWORD_FILE:-}" ]; then
|
|
RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE}"
|
|
fi
|
|
|
|
# Validation
|
|
if [ ! -f "$RESTIC_PASSWORD_FILE" ] && [ -z "${RESTIC_PASSWORD:-}" ]; then
|
|
echo "ERROR: Restic password file not found: $RESTIC_PASSWORD_FILE"
|
|
echo " Set RESTIC_PASSWORD environment variable or provide valid password file"
|
|
send_healthcheck "FAILED" "Restic password file not found: $RESTIC_PASSWORD_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Build restic command base
|
|
RESTIC_CMD="restic -r $REPOSITORY"
|
|
if [ -f "$RESTIC_PASSWORD_FILE" ]; then
|
|
RESTIC_CMD="$RESTIC_CMD --password-file $RESTIC_PASSWORD_FILE"
|
|
fi
|
|
|
|
# Generate repository name for logging/monitoring
|
|
REPO_NAME=$(echo "$REPOSITORY" | sed 's|.*/||' | sed 's|:.*||')
|
|
if [ -z "$REPO_NAME" ]; then
|
|
REPO_NAME="repository"
|
|
fi
|
|
|
|
log_message "Starting integrity check for repository: $REPOSITORY"
|
|
send_healthcheck "START" "Starting integrity check for $REPO_NAME"
|
|
|
|
# Build check command arguments
|
|
CHECK_ARGS=()
|
|
if [ "$READ_DATA" = true ]; then
|
|
CHECK_ARGS+=("--read-data")
|
|
log_message "Performing thorough check with data verification (this may take a while)"
|
|
else
|
|
log_message "Performing quick check without data verification"
|
|
fi
|
|
|
|
# Test repository connectivity first
|
|
log_message "Testing repository connectivity..."
|
|
CONNECTIVITY_OUTPUT=""
|
|
if ! CONNECTIVITY_OUTPUT=$(eval "$RESTIC_CMD snapshots --last" 2>&1); then
|
|
log_message "ERROR: Cannot connect to repository or repository is empty"
|
|
if [ "$VERBOSE" = true ]; then
|
|
log_message "Connectivity error: $CONNECTIVITY_OUTPUT"
|
|
fi
|
|
send_healthcheck "FAILED" "Cannot connect to repository $REPO_NAME"
|
|
exit 1
|
|
fi
|
|
|
|
# Perform integrity check
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_message "DRY RUN: Would check repository $REPOSITORY"
|
|
echo "Would run: $RESTIC_CMD check ${CHECK_ARGS[*]}"
|
|
exit 0
|
|
fi
|
|
|
|
log_message "Running repository integrity check..."
|
|
START_TIME=$(date +%s)
|
|
|
|
CHECK_OUTPUT=""
|
|
CHECK_SUCCESS=false
|
|
|
|
if [ "$VERBOSE" = true ]; then
|
|
# Show progress in verbose mode
|
|
if eval "$RESTIC_CMD check ${CHECK_ARGS[*]}"; then
|
|
CHECK_SUCCESS=true
|
|
fi
|
|
else
|
|
# Capture output for logging
|
|
if CHECK_OUTPUT=$(eval "$RESTIC_CMD check ${CHECK_ARGS[*]}" 2>&1); then
|
|
CHECK_SUCCESS=true
|
|
fi
|
|
fi
|
|
|
|
if [ "$CHECK_SUCCESS" = true ]; then
|
|
END_TIME=$(date +%s)
|
|
DURATION=$((END_TIME - START_TIME))
|
|
|
|
log_message "Repository integrity check completed successfully in ${DURATION} seconds"
|
|
|
|
# Get repository stats for the success message
|
|
REPO_STATS=""
|
|
if STATS_OUTPUT=$(eval "$RESTIC_CMD stats" 2>/dev/null); then
|
|
REPO_STATS=$(echo "$STATS_OUTPUT" | grep -E "(Total Size|Total File Count)" | head -2 | tr '\n' ', ' | sed 's/, $//' || echo "Repository stats available")
|
|
if [ -z "$REPO_STATS" ]; then
|
|
REPO_STATS="Repository stats available"
|
|
fi
|
|
else
|
|
REPO_STATS="Repository stats unavailable"
|
|
fi
|
|
|
|
SUCCESS_MSG="Integrity check passed for $REPO_NAME in ${DURATION}s. $REPO_STATS"
|
|
send_healthcheck "SUCCESS" "$SUCCESS_MSG"
|
|
|
|
# Additional verbose output
|
|
if [ "$VERBOSE" = true ]; then
|
|
echo "=== Check Summary ==="
|
|
echo "Repository: $REPOSITORY"
|
|
echo "Duration: ${DURATION} seconds"
|
|
echo "Data verification: $([ "$READ_DATA" = true ] && echo "enabled" || echo "disabled")"
|
|
echo "Result: PASSED"
|
|
if [ -n "$REPO_STATS" ] && [ "$REPO_STATS" != "Repository stats unavailable" ]; then
|
|
echo "Stats: $REPO_STATS"
|
|
fi
|
|
[ -n "$CHECK_OUTPUT" ] && echo -e "\nDetailed output:\n$CHECK_OUTPUT"
|
|
fi
|
|
else
|
|
END_TIME=$(date +%s)
|
|
DURATION=$((END_TIME - START_TIME))
|
|
|
|
ERROR_MSG="CRITICAL: Repository integrity check failed for $REPO_NAME after ${DURATION}s - backup repository may be corrupted!"
|
|
log_message "$ERROR_MSG"
|
|
send_healthcheck "FAILED" "$ERROR_MSG"
|
|
|
|
if [ "$VERBOSE" = true ]; then
|
|
echo "=== Check Summary ==="
|
|
echo "Repository: $REPOSITORY"
|
|
echo "Duration: ${DURATION} seconds"
|
|
echo "Data verification: $([ "$READ_DATA" = true ] && echo "enabled" || echo "disabled")"
|
|
echo "Result: FAILED"
|
|
echo "WARNING: Repository may be corrupted! Check logs and consider running 'restic repair' if needed."
|
|
[ -n "$CHECK_OUTPUT" ] && echo -e "\nError output:\n$CHECK_OUTPUT"
|
|
fi
|
|
|
|
exit 1
|
|
fi
|
|
|
|
log_message "Integrity check process completed successfully"
|