From 301d5dceda75b3444171ae49e8fa3ab81fe90557 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 13 Nov 2024 02:15:11 +0800 Subject: [PATCH] New updates and fail safe mechanism --- manifest.jps | 97 ++++++++- scripts/imports/backup_all.sh | 156 ++++++++++++--- scripts/imports/backup_core_files.sh | 26 ++- scripts/imports/backup_database.sh | 27 ++- scripts/imports/backup_media.sh | 33 ++- scripts/imports/check_sched.sh | 13 +- scripts/imports/manage_backup_schedule.sh | 232 +++++++++++++++++----- 7 files changed, 486 insertions(+), 98 deletions(-) diff --git a/manifest.jps b/manifest.jps index d749cb6..6db2e47 100644 --- a/manifest.jps +++ b/manifest.jps @@ -14,6 +14,8 @@ targetNodes: globals: envName: "${env.name}" + scriptPath: "/home/litespeed/mb-backups" + logPath: "/home/litespeed/mb-backups/logs" onInstall: - checkAddons @@ -21,12 +23,46 @@ onInstall: - importScripts settings: + scheduleSettings: + submitUnchanged: true + fields: + - name: frequency + caption: Backup Frequency + type: list + values: + daily: Daily Backup + weekly: Weekly Backup + default: daily + + - name: hour + caption: Backup Hour (0-23) + type: string + regex: "^([0-9]|1[0-9]|2[0-3])$" + regexText: Enter a valid hour (0-23) + default: "0" + + - name: dayOfWeek + caption: Day of Week (0=Sunday) + type: list + values: + 0: Sunday + 1: Monday + 2: Tuesday + 3: Wednesday + 4: Thursday + 5: Friday + 6: Saturday + default: "0" + showIf: + frequency: weekly + backupSettings: submitUnchanged: true fields: - name: blabel caption: Backup Label type: string + restoreSettings: submitUnchanged: true fields: @@ -42,6 +78,26 @@ buttons: successText: The backup process has been finished successfully. menu: + - caption: Configure Auto Backup + action: configureAutoBackup + confirmText: Configure automated backup schedule? + loadingText: Setting up backup schedule... + successText: Backup schedule configured successfully + settings: scheduleSettings + title: Configure Automated Backup Schedule + submitButtonText: Save Schedule + + - caption: Remove Auto Backup + action: removeAutoBackup + confirmText: Remove automated backup schedule? + loadingText: Removing backup schedule... + successText: Backup schedule removed successfully + + - caption: View Schedule + action: viewBackupSchedule + confirmText: View current backup schedule? + successText: Current schedule retrieved successfully + - caption: Check Backup Repository confirmText: Do you want to check and repair the backup repository? loadingText: Checking and repairing backup repository... @@ -104,12 +160,7 @@ menu: - caption: View Database Backups action: viewDatabaseBackups confirmText: Are you sure you want to view database backups? - successText: Database backups listed successfully. - - - caption: Auto Backup Sched - action: checkBackupSched - confirmText: Check the schedules for automated backups? - successText: Backup Scheds. + successText: Database backups listed successfully. onUninstall: - removeScript @@ -129,6 +180,32 @@ onAfterClone: backupCount: "5" actions: + configureAutoBackup: + - cmd[cp]: + user: root + commands: | + if [ ! -f /etc/restic-password ]; then + echo "Error: Restic password file not found" + exit 1 + fi + RESTIC_PWD=$(cat /etc/restic-password) + if [ "${settings.frequency}" = "daily" ]; then + bash ${globals.scriptPath}/manage_backup_schedule.sh add "0 ${settings.hour} * * *" "$RESTIC_PWD" + else + bash ${globals.scriptPath}/manage_backup_schedule.sh add "0 ${settings.hour} * * ${settings.dayOfWeek}" "$RESTIC_PWD" + fi + - return: + type: info + message: "${response.out}" + + removeAutoBackup: + - cmd[cp]: + user: root + commands: bash /home/litespeed/mb-backups/manage_backup_schedule.sh remove + - return: + type: info + message: "${response.out}" + checkBackupRepo: - cmd[cp]: user: root @@ -209,13 +286,13 @@ actions: type: info message: "${response.out}" - checkBackupSched: + viewBackupSchedule: - cmd[cp]: user: root - commands: bash /home/jelastic/mb-backups/check_sched.sh - - return: + commands: bash /home/litespeed/mb-backups/check_sched.sh + - return: type: info - message: "${response.out}" + message: "${response.out}" checkAddons: - script: |- diff --git a/scripts/imports/backup_all.sh b/scripts/imports/backup_all.sh index 7f4d60e..8965c30 100644 --- a/scripts/imports/backup_all.sh +++ b/scripts/imports/backup_all.sh @@ -1,76 +1,174 @@ #!/bin/bash -# Check if Restic password was supplied -if [ "$#" -ne 1 ]; then - echo "Usage: $0 RESTIC_PASSWORD" - exit 1 -fi +# Enable error handling +set -e +trap 'echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup failed" >> "$LOG_DIR/backup_error.log"' ERR + +# Load the backup logic functions +source /home/jelastic/mb-backups/backup-logic.sh # Configuration -RESTIC_PASSWORD="$1" +password_file="/etc/restic-password" APP_PATH='/var/www/webroot/ROOT' WP_CONFIG="${APP_PATH}/wp-config.php" backupPath='/mnt/backups' LOG_DIR="/home/jelastic/mb-backups/logs" -DB_NAME=$(grep "define( 'DB_NAME'" $WP_CONFIG | cut -d "'" -f 4) -DB_USER=$(grep "define( 'DB_USER'" $WP_CONFIG | cut -d "'" -f 4) -DB_PASSWORD=$(grep "define( 'DB_PASSWORD'" $WP_CONFIG | cut -d "'" -f 4) +TEMP_DIR="/tmp/wp_backup_tmp" +MAX_BACKUP_SIZE=$((10 * 1024 * 1024 * 1024)) # 10GB + +# Database configuration +DB_NAME=$(grep "define( 'DB_NAME'" "$WP_CONFIG" | cut -d "'" -f 4) +DB_USER=$(grep "define( 'DB_USER'" "$WP_CONFIG" | cut -d "'" -f 4) +DB_PASSWORD=$(grep "define( 'DB_PASSWORD'" "$WP_CONFIG" | cut -d "'" -f 4) BACKUP_USER='wp_backup' BACKUP_PASSWORD='gaQjveXl24Xo66w' -export RESTIC_REPOSITORY="$backupPath" -export RESTIC_PASSWORD - -# Ensure log directory exists +# Ensure directories exist mkdir -p "$LOG_DIR" +mkdir -p "$TEMP_DIR" + +# Cleanup function +cleanup() { + local exit_code=$? + rm -rf "${TEMP_DIR}"/* + if [ $exit_code -ne 0 ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup failed with exit code $exit_code" | tee -a "$LOG_DIR/backup_error.log" + fi +} +trap cleanup EXIT + +# Check backup size +check_backup_size() { + local path="$1" + local size=$(du -sb "$path" | cut -f1) + + if [ "$size" -gt "$MAX_BACKUP_SIZE" ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Warning: Backup size exceeds 10GB for $path" | tee -a "$LOG_DIR/backup_warning.log" + return 1 + fi + return 0 +} + +# Verify backup +verify_backup() { + local tag="$1" + local latest_snapshot=$(restic snapshots --latest 1 --tag "$tag" --json | jq -r '.[0].id') + if [ -n "$latest_snapshot" ]; then + restic check --read-data "$latest_snapshot" + return $? + fi + return 1 +} + +# Initialize backup +if [ ! -f "$password_file" ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Password file not found at $password_file" | tee -a "$LOG_DIR/backup_error.log" + exit 1 +fi + +export RESTIC_PASSWORD=$(cat "$password_file") +export RESTIC_REPOSITORY="$backupPath" + +# Check repository +check_backup_repo +if [ $? -ne 0 ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup repository check failed" | tee -a "$LOG_DIR/backup_error.log" + exit 1 +fi # Backup functions backup_core_files() { local log_file="${LOG_DIR}/backup_core_files_$(date +'%Y-%m-%d').log" echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Core Files Backup" | tee -a "$log_file" + + check_backup_size "$APP_PATH" || return 1 + local excludePaths=("$APP_PATH/wp-content/uploads") - local excludeOptions="" for path in "${excludePaths[@]}"; do excludeOptions+="--exclude $path " done if restic backup $excludeOptions "$APP_PATH" --tag core_files --tag full_backup; then - echo "[$(date +'%Y-%m-%d %H:%M:%S')] Core files backup completed successfully." | tee -a "$log_file" - else - echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Core files backup failed." | tee -a "$log_file" + verify_backup "core_files" || return 1 + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Core files backup completed successfully" | tee -a "$log_file" + return 0 fi + return 1 } backup_media_themes() { local log_file="${LOG_DIR}/backup_media_themes_$(date +'%Y-%m-%d').log" echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Media and Themes Backup" | tee -a "$log_file" + local includePaths=("$APP_PATH/wp-content/uploads") - + for path in "${includePaths[@]}"; do + check_backup_size "$path" || continue + if restic backup "$path" --tag media_themes --tag full_backup; then - echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup completed successfully for $path." | tee -a "$log_file" + verify_backup "media_themes" || return 1 + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup completed successfully for $path" | tee -a "$log_file" else - echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup failed for $path." | tee -a "$log_file" + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup failed for $path" | tee -a "$log_file" + return 1 fi done } backup_database() { local log_file="${LOG_DIR}/backup_database_$(date +'%Y-%m-%d').log" + local temp_dump="${TEMP_DIR}/wp_backup_${DB_NAME}_$(date +'%Y%m%d').sql" + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Database Backup" | tee -a "$log_file" - - # Correctly pass the password to mysqldump - if mysqldump -u "$BACKUP_USER" --password="$BACKUP_PASSWORD" "$DB_NAME" | restic backup --stdin --tag wordpress_db --tag full_backup; then - echo "[$(date +'%Y-%m-%d %H:%M:%S')] Database backup completed successfully." | tee -a "$log_file" + + if ! mysqldump -u "$BACKUP_USER" --password="$BACKUP_PASSWORD" "$DB_NAME" > "$temp_dump" 2>> "$log_file"; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Database dump failed" | tee -a "$log_file" + return 1 + fi + + check_backup_size "$temp_dump" || return 1 + + if restic backup "$temp_dump" --tag wordpress_db --tag full_backup; then + verify_backup "wordpress_db" || return 1 + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Database backup completed successfully" | tee -a "$log_file" + return 0 else - echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Database backup failed." | tee -a "$log_file" + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Database backup failed" | tee -a "$log_file" + return 1 fi } -# Execute backup functions -backup_core_files -backup_media_themes -backup_database +cleanup_old_backups() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting backup rotation" | tee -a "$LOG_DIR/backup_rotation.log" + restic forget \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 3 \ + --prune \ + --tag full_backup \ + 2>> "$LOG_DIR/backup_rotation.log" +} +# Main execution +main() { + local start_time=$(date +%s) + local backup_date=$(date +'%Y-%m-%d_%H-%M-%S') + local main_log="${LOG_DIR}/full_backup_${backup_date}.log" + + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting full backup process" | tee -a "$main_log" + + backup_core_files || exit 1 + backup_media_themes || exit 1 + backup_database || exit 1 + + cleanup_old_backups + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + echo "[$(date +'%Y-%m-%d %H:%M:%S')] Full backup completed in $duration seconds" | tee -a "$main_log" +} +# Execute main function +main \ No newline at end of file diff --git a/scripts/imports/backup_core_files.sh b/scripts/imports/backup_core_files.sh index fc13a24..91f3d30 100644 --- a/scripts/imports/backup_core_files.sh +++ b/scripts/imports/backup_core_files.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Load the backup logic functions +source /home/jelastic/mb-backups/backup-logic.sh + # Check for required arguments if [ $# -ne 2 ]; then echo "Usage: $0 " @@ -13,6 +16,7 @@ ADDITIONAL_TAG="$2" # Configuration APP_PATH='/var/www/webroot/ROOT' backupPath='/mnt/backups' +password_file="/etc/restic-password" LOG_DIR="/home/jelastic/mb-backups/logs" LOG_FILE="${LOG_DIR}/backup_core_files_$(date +'%Y-%m-%d').log" excludePaths=( @@ -26,6 +30,26 @@ mkdir -p "$LOG_DIR" export RESTIC_REPOSITORY="$backupPath" export RESTIC_PASSWORD +# Verify that the password file exists and matches the supplied password +if [ ! -f "$password_file" ]; then + echo "ERROR: Password file not found at $password_file" | tee -a "$LOG_FILE" + exit 1 +fi + +# Load and verify password from the file +stored_password=$(cat "$password_file") +if [ "$stored_password" != "$RESTIC_PASSWORD" ]; then + echo "ERROR: Password mismatch. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + +# Check repository accessibility and integrity +check_backup_repo +if [ $? -ne 0 ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup repository check failed. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + # Logging start echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Core Files Backup with tags: core_files, $ADDITIONAL_TAG" | tee -a "$LOG_FILE" @@ -40,8 +64,8 @@ if restic backup $excludeOptions "$APP_PATH" --tag core_files --tag "$ADDITIONAL echo "[$(date +'%Y-%m-%d %H:%M:%S')] Core files backup completed successfully." | tee -a "$LOG_FILE" else echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Core files backup failed." | tee -a "$LOG_FILE" + exit 1 fi # Logging end echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup process finished." | tee -a "$LOG_FILE" - diff --git a/scripts/imports/backup_database.sh b/scripts/imports/backup_database.sh index 5d43e8f..42f6656 100644 --- a/scripts/imports/backup_database.sh +++ b/scripts/imports/backup_database.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Load backup logic functions +source /home/jelastic/mb-backups/backup-logic.sh + # Validate input parameters if [ "$#" -ne 2 ]; then echo "Usage: $0 " @@ -14,6 +17,7 @@ ADDITIONAL_TAG="$2" APP_PATH='/var/www/webroot/ROOT' WP_CONFIG="${APP_PATH}/wp-config.php" backupPath='/mnt/backups' +password_file="/etc/restic-password" LOG_DIR="/home/jelastic/mb-backups/logs" LOG_FILE="${LOG_DIR}/backup_database_$(date +'%Y-%m-%d').log" DB_NAME=$(grep "define( 'DB_NAME'" $WP_CONFIG | cut -d "'" -f 4) @@ -35,11 +39,32 @@ export MYSQL_PWD=$BACKUP_PASSWORD # Use the backup user's password for mysqldum # Logging start echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Database Backup with additional tag: $ADDITIONAL_TAG" | tee -a "$LOG_FILE" -# Perform backup with additional tag +# Verify that the password file exists and matches the supplied password +if [ ! -f "$password_file" ]; then + echo "ERROR: Password file not found at $password_file" | tee -a "$LOG_FILE" + exit 1 +fi + +# Load and verify password from the file +stored_password=$(cat "$password_file") +if [ "$stored_password" != "$RESTIC_PASSWORD" ]; then + echo "ERROR: Password mismatch. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + +# Check repository accessibility and integrity +check_backup_repo +if [ $? -ne 0 ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup repository check failed. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + +# Perform database backup with additional tag if mysqldump -u "$BACKUP_USER" "$DB_NAME" | restic backup --stdin --tag wordpress_db --tag "$ADDITIONAL_TAG"; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] Database backup completed successfully with tags: wordpress_db, $ADDITIONAL_TAG." | tee -a "$LOG_FILE" else echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Database backup failed." | tee -a "$LOG_FILE" + exit 1 fi # Logging end diff --git a/scripts/imports/backup_media.sh b/scripts/imports/backup_media.sh index 82e6007..ad90963 100644 --- a/scripts/imports/backup_media.sh +++ b/scripts/imports/backup_media.sh @@ -1,18 +1,22 @@ #!/bin/bash -# Check for required arguments +# Load backup logic functions +source /home/jelastic/mb-backups/backup-logic.sh + +# Validate input parameters if [ "$#" -ne 2 ]; then echo "Usage: $0 " exit 1 fi -# Assign arguments to variables +# Assign command line arguments to variables RESTIC_PASSWORD="$1" ADDITIONAL_TAG="$2" # Configuration APP_PATH='/var/www/webroot/ROOT' backupPath='/mnt/backups' +password_file="/etc/restic-password" LOG_DIR="/home/jelastic/mb-backups/logs/manual/media" LOG_FILE="${LOG_DIR}/backup_media_$(date +'%Y-%m-%d').log" includePaths=("$APP_PATH/wp-content/uploads") @@ -25,14 +29,35 @@ export RESTIC_REPOSITORY="$backupPath" export RESTIC_PASSWORD # Logging start -echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Media Backup" | tee -a "$LOG_FILE" +echo "[$(date +'%Y-%m-%d %H:%M:%S')] Starting Media and Themes Backup" | tee -a "$LOG_FILE" -# Perform backup with additional tag +# Verify that the password file exists and matches the supplied password +if [ ! -f "$password_file" ]; then + echo "ERROR: Password file not found at $password_file" | tee -a "$LOG_FILE" + exit 1 +fi + +# Load and verify password from the file +stored_password=$(cat "$password_file") +if [ "$stored_password" != "$RESTIC_PASSWORD" ]; then + echo "ERROR: Password mismatch. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + +# Check repository accessibility and integrity +check_backup_repo +if [ $? -ne 0 ]; then + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup repository check failed. Aborting backup." | tee -a "$LOG_FILE" + exit 1 +fi + +# Perform media backup with additional tag for path in "${includePaths[@]}"; do if restic backup "$path" --tag media_themes --tag "$ADDITIONAL_TAG"; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] Backup completed successfully for $path with tags: media_themes, $ADDITIONAL_TAG." | tee -a "$LOG_FILE" else echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Backup failed for $path." | tee -a "$LOG_FILE" + exit 1 fi done diff --git a/scripts/imports/check_sched.sh b/scripts/imports/check_sched.sh index eca1753..ebbf23f 100644 --- a/scripts/imports/check_sched.sh +++ b/scripts/imports/check_sched.sh @@ -4,13 +4,18 @@ # Define the script path to check in cron jobs SCRIPT_PATH="/home/litespeed/mb-backups/backup_all.sh" +LOG_FILE="/home/litespeed/mb-backups/logs/cron_check.log" -# Use crontab -l to list all cron jobs, then grep to check for the specific script path +# Log the start of the script check +echo "[$(date)] Checking for automated backup cron job..." | tee -a "$LOG_FILE" + +# Check if the cron job is found in the crontab CRON_JOB=$(crontab -l | grep -F "$SCRIPT_PATH") if [ -z "$CRON_JOB" ]; then - echo "Automated Backups is not enabled." + echo "[$(date)] Automated Backups are NOT enabled." | tee -a "$LOG_FILE" else - echo "$CRON_JOB" | awk '{print "Schedule: " $1, $2, $3, $4, $5}' - # Note: This script stops here, providing the schedule. Computing the next run time in a human-readable format is not straightforward in bash without external tools. + echo "[$(date)] Automated Backups are enabled with the following schedule:" | tee -a "$LOG_FILE" + echo "$CRON_JOB" | awk '{print "Schedule: " $1, $2, $3, $4, $5}' | tee -a "$LOG_FILE" + echo "[$(date)] Note: Computing the next human-readable run time requires external tools." | tee -a "$LOG_FILE" fi diff --git a/scripts/imports/manage_backup_schedule.sh b/scripts/imports/manage_backup_schedule.sh index 7cea871..26c9b4e 100644 --- a/scripts/imports/manage_backup_schedule.sh +++ b/scripts/imports/manage_backup_schedule.sh @@ -1,62 +1,196 @@ #!/bin/bash +# Enable strict error handling +set -euo pipefail +trap 'handle_error $? $LINENO' ERR + # Configuration BACKUP_SCRIPT="/home/litespeed/mb-backups/backup_all.sh" LOG_DIR="/home/litespeed/mb-backups/logs/auto" ACTION_LOG_FILE="${LOG_DIR}/schedule_actions.log" BACKUP_LOG_PREFIX="${LOG_DIR}/backup_" +PASSWORD_FILE="/etc/restic-password" +LOCK_FILE="/tmp/backup_schedule.lock" +MAX_RETRIES=3 +RETRY_DELAY=5 -# Ensure the log directory exists -mkdir -p "$LOG_DIR" - -log_action() { - echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$ACTION_LOG_FILE" +# Error handling function +handle_error() { + local exit_code=$1 + local line_no=$2 + log_action "ERROR: Command failed at line ${line_no} with exit code ${exit_code}" + cleanup_and_exit 1 } -# Function to add or update the cron job with dynamic logging -add_update_cron_job() { - # Verify Restic password is provided - if [ -z "$3" ]; then - echo "Restic password is required." - log_action "Attempted to add/update a schedule without providing a Restic password." - exit 1 - fi - - # Prepare the cron command to include dynamic date in the backup log filename - CMD="RESTIC_PASSWORD=\"$3\" $BACKUP_SCRIPT > \"${BACKUP_LOG_PREFIX}\$(date +\\%Y-\\%m-\\%d_\\%H-\\%M-\\%S).log\" 2>&1" - - # Add or update the cron job - (crontab -l | grep -v "$BACKUP_SCRIPT" 2>/dev/null; echo "$2 $CMD") | crontab - - local update_msg="Backup schedule updated to: $2" - echo "$update_msg" - log_action "$update_msg" +# Cleanup function +cleanup_and_exit() { + local exit_code=$1 + [ -f "$LOCK_FILE" ] && rm -f "$LOCK_FILE" + exit "${exit_code}" } -# Function to remove the cron job -remove_cron_job() { - crontab -l | grep -v "$BACKUP_SCRIPT" | crontab - - local remove_msg="Backup schedule removed." - echo "$remove_msg" - log_action "$remove_msg" -} - -# Main logic to add, update, or remove the cron job based on user input -case $1 in - add|update) - if [ "$#" -ne 3 ]; then - echo "Usage for add/update: $0 {add|update} 'schedule' 'restic_password'" - echo "Example: $0 add '0 1 * * *' 'secret_password'" - log_action "Incorrect usage for add/update. Correct format not followed." - else - add_update_cron_job "$@" +# Ensure single instance +ensure_single_instance() { + if [ -f "$LOCK_FILE" ]; then + if kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then + log_action "ERROR: Another instance is running" + exit 1 fi - ;; - remove) - remove_cron_job - ;; - *) - echo "Invalid action: $1. Use add, update, or remove." - log_action "Invalid action attempted: $1" - exit 1 - ;; -esac + fi + echo $$ > "$LOCK_FILE" +} + +# Enhanced logging function +log_action() { + local timestamp=$(date +'%Y-%m-%d %H:%M:%S') + local log_msg="[$timestamp] $1" + echo "$log_msg" | tee -a "$ACTION_LOG_FILE" + + # Log critical errors to system log + if [[ "$1" == *"ERROR"* ]]; then + logger -t "backup-schedule" "$1" + fi +} + +# Validate system requirements +check_system_requirements() { + local required_space=1048576 # 1GB in KB + local available_space=$(df -k "$LOG_DIR" | awk 'NR==2 {print $4}') + + if [ ! -x "$BACKUP_SCRIPT" ]; then + log_action "ERROR: Backup script not executable or not found" + return 1 + fi + + if [ "$available_space" -lt "$required_space" ]; then + log_action "ERROR: Insufficient disk space" + return 1 + } + + return 0 +} + +# Validate cron schedule +validate_schedule() { + local schedule="$1" + if ! [[ $schedule =~ ^[0-9,\*/-]+ [0-9,\*/-]+ [0-9,\*/-]+ [0-9,\*/-]+ [0-9,\*/-]+$ ]]; then + log_action "ERROR: Invalid cron schedule format: $schedule" + return 1 + fi + return 0 +} + +# Get Restic password with retry mechanism +get_restic_password() { + local retry_count=0 + while [ $retry_count -lt $MAX_RETRIES ]; do + if [ -f "$PASSWORD_FILE" ] && [ -s "$PASSWORD_FILE" ]; then + cat "$PASSWORD_FILE" + return 0 + fi + retry_count=$((retry_count + 1)) + sleep $RETRY_DELAY + done + log_action "ERROR: Failed to retrieve Restic password after $MAX_RETRIES attempts" + return 1 +} + +# Enhanced cron job management +add_update_cron_job() { + local action="$1" + local schedule="$2" + local restic_password="$3" + local temp_crontab="/tmp/temp_crontab.$$" + + # Validate inputs + if ! validate_schedule "$schedule"; then + return 1 + fi + + # Prepare cron command with environment variables and error handling + local CMD="RESTIC_PASSWORD=\"$restic_password\" \ + LOG_FILE=\"${BACKUP_LOG_PREFIX}\$(date +\\%Y-\\%m-\\%d_\\%H-\\%M-\\%S).log\" \ + $BACKUP_SCRIPT > \"\$LOG_FILE\" 2>&1 || echo \"\$(date) Backup failed\" >> \"$ACTION_LOG_FILE\"" + + # Safely update crontab + crontab -l 2>/dev/null | grep -v "$BACKUP_SCRIPT" > "$temp_crontab" || true + echo "$schedule $CMD" >> "$temp_crontab" + + if crontab "$temp_crontab"; then + log_action "Successfully ${action}d backup schedule: $schedule" + rm -f "$temp_crontab" + return 0 + else + log_action "ERROR: Failed to ${action} backup schedule" + rm -f "$temp_crontab" + return 1 + fi +} + +# Enhanced cron job removal +remove_cron_job() { + local temp_crontab="/tmp/temp_crontab.$$" + + if crontab -l 2>/dev/null | grep -q "$BACKUP_SCRIPT"; then + crontab -l | grep -v "$BACKUP_SCRIPT" > "$temp_crontab" + if crontab "$temp_crontab"; then + log_action "Backup schedule successfully removed" + rm -f "$temp_crontab" + return 0 + else + log_action "ERROR: Failed to remove backup schedule" + rm -f "$temp_crontab" + return 1 + fi + else + log_action "No backup schedule found to remove" + return 0 + fi +} + +# Main execution +main() { + # Create log directory with proper permissions + mkdir -p "$LOG_DIR" + chmod 750 "$LOG_DIR" + + # Ensure single instance + ensure_single_instance + + # Check system requirements + if ! check_system_requirements; then + cleanup_and_exit 1 + fi + + case $1 in + add|update) + if [ "$#" -ne 3 ]; then + log_action "Usage: $0 {add|update} 'schedule' 'restic_password'" + log_action "Example: $0 add '0 1 * * *' 'secret_password'" + cleanup_and_exit 1 + fi + + local restic_password="${3:-$(get_restic_password)}" + if [ -z "$restic_password" ]; then + cleanup_and_exit 1 + fi + + add_update_cron_job "$1" "$2" "$restic_password" + ;; + remove) + remove_cron_job + ;; + status) + crontab -l | grep "$BACKUP_SCRIPT" || echo "No backup schedule found" + ;; + *) + log_action "Invalid action: $1. Use add, update, remove, or status" + cleanup_and_exit 1 + ;; + esac + + cleanup_and_exit 0 +} + +# Execute main function with all arguments +main "$@" \ No newline at end of file