Initial commit: Add quantumrick backup system
- Automated backup system using Restic - Systemd timer integration for scheduled backups - Service management tools (start/stop/restore) - Multi-service support with template-based configuration - Backup and restore functionality with test/production modes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
75a1e66ed2
54
backup/gen-conf.sh
Normal file
54
backup/gen-conf.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to generate Restic configuration with secure password
|
||||
# Location: /home/citadel/services/paperless/generate-restic-config.sh
|
||||
|
||||
set -e
|
||||
|
||||
CONFIG_FILE="/home/citadel/backup/restic.conf"
|
||||
REPO_PATH="/mnt/data/backup/quantumrick"
|
||||
|
||||
echo "=== Generating Restic Configuration ==="
|
||||
|
||||
# Check if config already exists
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo "Configuration file already exists at $CONFIG_FILE"
|
||||
read -p "Do you want to overwrite it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate secure password (32 characters)
|
||||
echo "Generating secure password..."
|
||||
RESTIC_PASSWORD=$(openssl rand -base64 32)
|
||||
|
||||
# Create configuration file
|
||||
echo "Creating configuration file..."
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
# Restic configuration for Paperless NGX backups
|
||||
# Generated on $(date)
|
||||
|
||||
# Repository path
|
||||
export RESTIC_REPOSITORY="$REPO_PATH"
|
||||
|
||||
# Repository password
|
||||
export RESTIC_PASSWORD="$RESTIC_PASSWORD"
|
||||
|
||||
# Cache directory (optional)
|
||||
export RESTIC_CACHE_DIR="/tmp/restic-cache"
|
||||
EOF
|
||||
|
||||
# Set secure permissions
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
|
||||
echo "✅ Configuration file created at $CONFIG_FILE"
|
||||
echo "🔒 Password generated and saved securely"
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANT: Save this password somewhere safe!"
|
||||
echo " If you lose it, you won't be able to restore your backups!"
|
||||
echo ""
|
||||
echo "Password: $RESTIC_PASSWORD"
|
||||
echo ""
|
51
backup/init-restic.sh
Normal file
51
backup/init-restic.sh
Normal file
@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to initialize Restic repository for Paperless NGX backups
|
||||
# Location: /home/citadel/services/paperless/init-restic-repo.sh
|
||||
|
||||
set -e
|
||||
|
||||
REPO_PATH="/mnt/data/backup/quantumrick"
|
||||
CONFIG_FILE="/home/citadel/backup/restic.conf"
|
||||
|
||||
echo "=== Initializing Restic Repository ==="
|
||||
|
||||
# Check if repository directory exists
|
||||
if [ ! -d "$(dirname "$REPO_PATH")" ]; then
|
||||
echo "Creating backup directory..."
|
||||
sudo mkdir -p "$(dirname "$REPO_PATH")"
|
||||
sudo chown $(whoami):$(whoami) "$(dirname "$REPO_PATH")"
|
||||
fi
|
||||
|
||||
# Check if config file exists
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: Configuration file $CONFIG_FILE not found!"
|
||||
echo "Please run generate-restic-config.sh first to create the configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source the configuration
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
# Check if repository is already initialized
|
||||
if restic -r "$REPO_PATH" cat config &>/dev/null; then
|
||||
echo "Repository already initialized at $REPO_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Initializing new Restic repository at $REPO_PATH"
|
||||
|
||||
# Initialize the repository
|
||||
restic init -r "$REPO_PATH"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Repository successfully initialized!"
|
||||
echo "Repository path: $REPO_PATH"
|
||||
echo "Configuration: $CONFIG_FILE"
|
||||
else
|
||||
echo "❌ Failed to initialize repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Repository Information ==="
|
||||
restic -r "$REPO_PATH" stats
|
138
backup/install-service
Executable file
138
backup/install-service
Executable file
@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generic script to install systemd timer for any service backup
|
||||
# Location: /home/citadel/backup/install-service-timer.sh
|
||||
# Usage: sudo ./install-service.sh <service_name> [schedule]
|
||||
|
||||
set -e
|
||||
|
||||
# Check arguments
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <service_name> [schedule]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 paperless # Daily at 3:00 AM (default)"
|
||||
echo " $0 nextcloud \"*-*-* 02:30:00\" # Daily at 2:30 AM"
|
||||
echo " $0 gitlab \"Mon *-*-* 04:00:00\" # Weekly on Monday at 4:00 AM"
|
||||
echo ""
|
||||
echo "Available services with backup.sh:"
|
||||
find /home/citadel/services -name "backup.sh" -printf " %h\n" | sed 's|/home/citadel/services/||' 2>/dev/null || echo " (none found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_NAME="$1"
|
||||
SCHEDULE="${2:-*-*-* 03:00:00}" # Default: daily at 3:00 AM
|
||||
|
||||
SERVICE_DIR="/home/citadel/services/$SERVICE_NAME"
|
||||
BACKUP_SCRIPT="$SERVICE_DIR/backup.sh"
|
||||
BACKUP_DIR="/home/citadel/backup"
|
||||
|
||||
# Systemd template files
|
||||
SERVICE_TEMPLATE="service-backup@.service"
|
||||
TIMER_TEMPLATE="service-backup@.timer"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
|
||||
# Instance names
|
||||
SERVICE_INSTANCE="service-backup@$SERVICE_NAME.service"
|
||||
TIMER_INSTANCE="service-backup@$SERVICE_NAME.timer"
|
||||
|
||||
echo "=== Installing Systemd Timer for $SERVICE_NAME Backup ==="
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "This script needs to be run with sudo for systemd operations"
|
||||
echo "Usage: sudo $0 $SERVICE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if service directory exists
|
||||
if [ ! -d "$SERVICE_DIR" ]; then
|
||||
echo "ERROR: Service directory not found: $SERVICE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if backup script exists
|
||||
if [ ! -f "$BACKUP_SCRIPT" ]; then
|
||||
echo "ERROR: Backup script not found: $BACKUP_SCRIPT"
|
||||
echo "Please create a backup.sh script in the service directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make backup script executable
|
||||
chmod +x "$BACKUP_SCRIPT"
|
||||
|
||||
# Check if template files exist
|
||||
if [ ! -f "$BACKUP_DIR/$SERVICE_TEMPLATE" ]; then
|
||||
echo "ERROR: Service template not found: $BACKUP_DIR/$SERVICE_TEMPLATE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BACKUP_DIR/$TIMER_TEMPLATE" ]; then
|
||||
echo "ERROR: Timer template not found: $BACKUP_DIR/$TIMER_TEMPLATE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop and disable existing timer if it exists
|
||||
if systemctl is-active --quiet "$TIMER_INSTANCE" 2>/dev/null; then
|
||||
echo "Stopping existing timer..."
|
||||
systemctl stop "$TIMER_INSTANCE"
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet "$TIMER_INSTANCE" 2>/dev/null; then
|
||||
echo "Disabling existing timer..."
|
||||
systemctl disable "$TIMER_INSTANCE"
|
||||
fi
|
||||
|
||||
# Copy template files if they don't exist in systemd directory
|
||||
if [ ! -f "$SYSTEMD_DIR/$SERVICE_TEMPLATE" ]; then
|
||||
echo "Installing service template..."
|
||||
cp "$BACKUP_DIR/$SERVICE_TEMPLATE" "$SYSTEMD_DIR/"
|
||||
chmod 644 "$SYSTEMD_DIR/$SERVICE_TEMPLATE"
|
||||
fi
|
||||
|
||||
if [ ! -f "$SYSTEMD_DIR/$TIMER_TEMPLATE" ]; then
|
||||
echo "Installing timer template..."
|
||||
cp "$BACKUP_DIR/$TIMER_TEMPLATE" "$SYSTEMD_DIR/"
|
||||
chmod 644 "$SYSTEMD_DIR/$TIMER_TEMPLATE"
|
||||
fi
|
||||
|
||||
# Create custom timer with specific schedule if different from default
|
||||
if [ "$SCHEDULE" != "*-*-* 03:00:00" ]; then
|
||||
echo "Creating custom timer with schedule: $SCHEDULE"
|
||||
sed "s|OnCalendar=\*-\*-\* 03:00:00|OnCalendar=$SCHEDULE|" \
|
||||
"$BACKUP_DIR/$TIMER_TEMPLATE" > "$SYSTEMD_DIR/$TIMER_INSTANCE"
|
||||
chmod 644 "$SYSTEMD_DIR/$TIMER_INSTANCE"
|
||||
fi
|
||||
|
||||
# Reload systemd
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable and start timer
|
||||
echo "Enabling and starting timer for $SERVICE_NAME..."
|
||||
systemctl enable "service-backup@$SERVICE_NAME.timer"
|
||||
systemctl start "service-backup@$SERVICE_NAME.timer"
|
||||
|
||||
# Create log file with proper permissions
|
||||
LOG_FILE="/var/log/$SERVICE_NAME-backup.log"
|
||||
touch "$LOG_FILE"
|
||||
chown citadel:citadel "$LOG_FILE"
|
||||
|
||||
echo ""
|
||||
echo "✅ Systemd timer installed successfully for $SERVICE_NAME!"
|
||||
echo ""
|
||||
echo "📋 Useful commands:"
|
||||
echo " Check timer status: systemctl status service-backup@$SERVICE_NAME.timer"
|
||||
echo " View timer schedule: systemctl list-timers service-backup@$SERVICE_NAME.timer"
|
||||
echo " Run backup manually: systemctl start service-backup@$SERVICE_NAME.service"
|
||||
echo " View backup logs: journalctl -u service-backup@$SERVICE_NAME.service"
|
||||
echo " View recent logs: journalctl -u service-backup@$SERVICE_NAME.service -f"
|
||||
echo " Stop timer: systemctl stop service-backup@$SERVICE_NAME.timer"
|
||||
echo " Disable timer: systemctl disable service-backup@$SERVICE_NAME.timer"
|
||||
echo ""
|
||||
echo "📅 Schedule: $SCHEDULE (with up to 5min random delay)"
|
||||
echo "📝 Logs: Available via journalctl and $LOG_FILE"
|
||||
|
||||
echo ""
|
||||
echo "🔍 Current timer status:"
|
||||
systemctl status "service-backup@$SERVICE_NAME.timer" --no-pager -l || true
|
156
backup/list-snapshots
Executable file
156
backup/list-snapshots
Executable file
@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to list available snapshots from Restic repository
|
||||
# Location: /home/citadel/backup/list-snapshots
|
||||
# Usage: ./list-snapshots [service_name]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="/home/citadel/backup"
|
||||
CONFIG_FILE="$BACKUP_DIR/restic.conf"
|
||||
|
||||
# Colors for output
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
show_help() {
|
||||
echo "Usage: $0 [service_name]"
|
||||
echo ""
|
||||
echo "List available snapshots from Restic repository"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # List all snapshots grouped by service"
|
||||
echo " $0 paperless # List snapshots for paperless service only"
|
||||
echo ""
|
||||
}
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
list_all_snapshots() {
|
||||
log "All available snapshots:"
|
||||
echo ""
|
||||
|
||||
local snapshots=$(restic snapshots --json 2>/dev/null)
|
||||
|
||||
if [ -z "$snapshots" ] || [ "$snapshots" = "null" ] || [ "$snapshots" = "[]" ]; then
|
||||
echo "No snapshots found in repository."
|
||||
return
|
||||
fi
|
||||
|
||||
# Filter out timestamp tags (like paperless-20250609-115655) and retention tags
|
||||
local services=$(echo "$snapshots" | jq -r '.[].tags[]' | grep -v -E '^(daily|weekly|monthly|yearly)$' | grep -v -E '^.*-[0-9]{8}-[0-9]{6}$' | sort -u)
|
||||
|
||||
for service in $services; do
|
||||
echo -e "${GREEN}=== $service ===${NC}"
|
||||
|
||||
# Display snapshots for this service
|
||||
echo "$snapshots" | jq -r ".[] | select(.tags[] == \"$service\") | \"\(.short_id) \(.time[:19]) \(.summary.total_files_processed) \((.summary.total_bytes_processed / 1024 / 1024 / 1024 * 100 | floor) / 100)\"" | \
|
||||
awk 'BEGIN {printf "%-10s %-20s %-10s %s\n", "ID", "DATE", "FILES", "SIZE(GB)"; print "-------------------------------------------------------"} {printf "%-10s %-20s %-10s %.2f\n", $1, $2, $3, $4}'
|
||||
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
list_service_snapshots() {
|
||||
local service="$1"
|
||||
|
||||
log "Snapshots for service: $service"
|
||||
echo ""
|
||||
|
||||
local snapshots=$(restic snapshots --tag "$service" --json 2>/dev/null)
|
||||
|
||||
if [ -z "$snapshots" ] || [ "$snapshots" = "null" ] || [ "$snapshots" = "[]" ]; then
|
||||
echo "No snapshots found for service: $service"
|
||||
return
|
||||
fi
|
||||
|
||||
# Display snapshots in a readable format
|
||||
echo "$snapshots" | jq -r '.[] | "\(.short_id) \(.time[:19]) \(.tags | join(",")) \(.summary.total_files_processed) \((.summary.total_bytes_processed / 1024 / 1024 / 1024 * 100 | floor) / 100)"' | \
|
||||
awk 'BEGIN {printf "%-10s %-20s %-35s %-10s %s\n", "ID", "DATE", "TAGS", "FILES", "SIZE(GB)"; print "-------------------------------------------------------------------------------------"} {printf "%-10s %-20s %-35s %-10s %.2f\n", $1, $2, $3, $4, $5}'
|
||||
|
||||
echo ""
|
||||
|
||||
# Show summary
|
||||
local count=$(echo "$snapshots" | jq length)
|
||||
local total_size=$(echo "$snapshots" | jq -r '.[].summary.total_bytes_processed' | awk '{sum+=$1} END {printf "%.2f", sum/1024/1024/1024}')
|
||||
local oldest=$(echo "$snapshots" | jq -r '.[0].time[:19]' 2>/dev/null)
|
||||
local newest=$(echo "$snapshots" | jq -r '.[-1].time[:19]' 2>/dev/null)
|
||||
|
||||
echo -e "${YELLOW}Summary:${NC}"
|
||||
echo " Total snapshots: $count"
|
||||
echo " Total size: ${total_size}GB"
|
||||
if [ "$oldest" != "$newest" ]; then
|
||||
echo " Date range: $oldest to $newest"
|
||||
else
|
||||
echo " Date: $oldest"
|
||||
fi
|
||||
}
|
||||
|
||||
show_repository_info() {
|
||||
log "Repository information:"
|
||||
echo ""
|
||||
|
||||
# Repository stats
|
||||
restic stats --mode raw-data 2>/dev/null | head -10
|
||||
|
||||
echo ""
|
||||
log "Repository status:"
|
||||
restic check --read-data-subset=1% 2>/dev/null && echo "✅ Repository is healthy" || echo "⚠️ Repository check failed or not available"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
main() {
|
||||
local service="${1:-}"
|
||||
|
||||
if [ "$service" = "--help" ] || [ "$service" = "-h" ] || [ "$service" = "help" ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we're in the backup directory or config exists
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: Configuration file not found: $CONFIG_FILE"
|
||||
echo "Make sure you're running this from the backup directory or have valid restic.conf"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source Restic configuration
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
# Test repository access
|
||||
if ! restic snapshots >/dev/null 2>&1; then
|
||||
echo "Error: Cannot access Restic repository"
|
||||
echo "Check your configuration and repository password"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# List snapshots
|
||||
if [ -n "$service" ]; then
|
||||
list_service_snapshots "$service"
|
||||
else
|
||||
list_all_snapshots
|
||||
fi
|
||||
|
||||
# Show repository info
|
||||
echo ""
|
||||
show_repository_info
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
if ! command -v restic &> /dev/null; then
|
||||
echo "Error: restic is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is not installed (required for JSON parsing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
242
backup/manage
Executable file
242
backup/manage
Executable file
@ -0,0 +1,242 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to manage backup timers for all services
|
||||
# Location: /home/citadel/backup/manage
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/home/citadel/backup"
|
||||
SERVICES_DIR="/home/citadel/services"
|
||||
|
||||
show_help() {
|
||||
echo "Usage: $0 <command> [service_name]"
|
||||
echo ""
|
||||
echo "Backup Commands:"
|
||||
echo " list List all backup timers and their status"
|
||||
echo " status [service] Show detailed status for service (or all)"
|
||||
echo " start <service> Start backup timer for service"
|
||||
echo " stop <service> Stop backup timer for service"
|
||||
echo " restart <service> Restart backup timer for service"
|
||||
echo " enable <service> Enable backup timer for service"
|
||||
echo " disable <service> Disable backup timer for service"
|
||||
echo " run <service> Run backup manually for service"
|
||||
echo " logs <service> Show logs for service backup"
|
||||
echo " install <service> Install timer for service"
|
||||
echo " remove <service> Remove timer for service"
|
||||
echo " available List services with backup.sh available"
|
||||
echo ""
|
||||
echo "Restore Commands:"
|
||||
echo " snapshots [service] List available snapshots"
|
||||
echo " restore <service> Restore service (test mode)"
|
||||
echo " restore-prod <service> Restore service (production mode)"
|
||||
echo " cleanup-test [service] Clean up test restore instances"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 list"
|
||||
echo " $0 status paperless"
|
||||
echo " $0 run paperless"
|
||||
echo " $0 logs paperless"
|
||||
echo " $0 snapshots paperless"
|
||||
echo " $0 restore paperless"
|
||||
echo " $0 cleanup-test"
|
||||
}
|
||||
|
||||
list_timers() {
|
||||
echo "=== Backup Timers Status ==="
|
||||
echo ""
|
||||
|
||||
# Get all service-backup timers
|
||||
local timer_output=$(systemctl list-timers "service-backup@*.timer" --all --no-legend 2>/dev/null || true)
|
||||
|
||||
if [ -z "$timer_output" ]; then
|
||||
echo "No backup timers found."
|
||||
echo ""
|
||||
echo "Available services with backup.sh:"
|
||||
find "$SERVICES_DIR" -name "backup.sh" -printf " %h\n" | sed "s|$SERVICES_DIR/||" 2>/dev/null || echo " (none found)"
|
||||
return
|
||||
fi
|
||||
|
||||
printf "%-20s %-10s %-15s %s\n" "SERVICE" "STATUS" "ENABLED" "NEXT RUN"
|
||||
printf "%-20s %-10s %-15s %s\n" "-------" "------" "-------" "--------"
|
||||
|
||||
# Parse each line from systemctl list-timers
|
||||
# Format: NEXT(1-4) LEFT(5) LAST(6-9) PASSED(10) UNIT(11) ACTIVATES(12)
|
||||
echo "$timer_output" | while read -r line; do
|
||||
# Extract timer name (11th field)
|
||||
local timer=$(echo "$line" | awk '{print $11}')
|
||||
|
||||
# Extract service name from timer name
|
||||
local service_name=$(echo "$timer" | sed 's/service-backup@\(.*\)\.timer/\1/')
|
||||
|
||||
# Skip if service name is empty
|
||||
if [ -z "$service_name" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get next run (first 4 fields for date/time)
|
||||
local next_run=$(echo "$line" | awk '{print $1, $2, $3, $4}')
|
||||
|
||||
# Get status and enabled state
|
||||
local status=$(systemctl is-active "$timer" 2>/dev/null || echo "inactive")
|
||||
local enabled=$(systemctl is-enabled "$timer" 2>/dev/null || echo "disabled")
|
||||
|
||||
printf "%-20s %-10s %-15s %s\n" "$service_name" "$status" "$enabled" "$next_run"
|
||||
done
|
||||
}
|
||||
|
||||
show_status() {
|
||||
local service="$1"
|
||||
|
||||
if [ -n "$service" ]; then
|
||||
echo "=== Status for $service ==="
|
||||
systemctl status "service-backup@$service.timer" --no-pager -l
|
||||
echo ""
|
||||
echo "Next scheduled runs:"
|
||||
systemctl list-timers "service-backup@$service.timer" --no-legend
|
||||
else
|
||||
echo "=== All Backup Timers Status ==="
|
||||
systemctl list-timers "service-backup@*.timer" --no-legend
|
||||
fi
|
||||
}
|
||||
|
||||
manage_timer() {
|
||||
local action="$1"
|
||||
local service="$2"
|
||||
|
||||
if [ -z "$service" ]; then
|
||||
echo "ERROR: Service name required for $action"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local timer="service-backup@$service.timer"
|
||||
local service_unit="service-backup@$service.service"
|
||||
|
||||
case $action in
|
||||
start)
|
||||
systemctl start "$timer"
|
||||
echo "✅ Started timer for $service"
|
||||
;;
|
||||
stop)
|
||||
systemctl stop "$timer"
|
||||
echo "✅ Stopped timer for $service"
|
||||
;;
|
||||
restart)
|
||||
systemctl restart "$timer"
|
||||
echo "✅ Restarted timer for $service"
|
||||
;;
|
||||
enable)
|
||||
systemctl enable "$timer"
|
||||
echo "✅ Enabled timer for $service"
|
||||
;;
|
||||
disable)
|
||||
systemctl disable "$timer"
|
||||
echo "✅ Disabled timer for $service"
|
||||
;;
|
||||
run)
|
||||
echo "Running backup for $service..."
|
||||
systemctl start "$service_unit"
|
||||
echo "✅ Backup started for $service"
|
||||
echo "Monitor with: journalctl -u $service_unit -f"
|
||||
;;
|
||||
logs)
|
||||
journalctl -u "$service_unit" --no-pager -l
|
||||
;;
|
||||
remove)
|
||||
if systemctl is-active --quiet "$timer" 2>/dev/null; then
|
||||
systemctl stop "$timer"
|
||||
fi
|
||||
if systemctl is-enabled --quiet "$timer" 2>/dev/null; then
|
||||
systemctl disable "$timer"
|
||||
fi
|
||||
echo "✅ Removed timer for $service"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_timer() {
|
||||
local service="$1"
|
||||
|
||||
if [ -z "$service" ]; then
|
||||
echo "ERROR: Service name required for install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: Install requires sudo privileges"
|
||||
echo "Usage: sudo $0 install $service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$BACKUP_DIR/install" "$service"
|
||||
}
|
||||
|
||||
list_available() {
|
||||
echo "=== Services with backup.sh available ==="
|
||||
echo ""
|
||||
|
||||
local found=false
|
||||
for service_dir in "$SERVICES_DIR"/*; do
|
||||
if [ -d "$service_dir" ] && [ -f "$service_dir/backup.sh" ]; then
|
||||
local service_name=$(basename "$service_dir")
|
||||
local timer_status="not installed"
|
||||
|
||||
if systemctl is-enabled --quiet "service-backup@$service_name.timer" 2>/dev/null; then
|
||||
timer_status="installed"
|
||||
fi
|
||||
|
||||
printf " %-20s (timer: %s)\n" "$service_name" "$timer_status"
|
||||
found=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found" = false ]; then
|
||||
echo "No services with backup.sh found in $SERVICES_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
case "${1:-}" in
|
||||
list)
|
||||
list_timers
|
||||
;;
|
||||
status)
|
||||
show_status "$2"
|
||||
;;
|
||||
start|stop|restart|enable|disable|run|logs|remove)
|
||||
if [ "$1" = "remove" ] || [ "$1" = "enable" ] || [ "$1" = "disable" ] || [ "$1" = "start" ] || [ "$1" = "stop" ] || [ "$1" = "restart" ]; then
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: $1 requires sudo privileges"
|
||||
echo "Usage: sudo $0 $1 $2"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
manage_timer "$1" "$2"
|
||||
;;
|
||||
install)
|
||||
install_timer "$2"
|
||||
;;
|
||||
available)
|
||||
list_available
|
||||
;;
|
||||
snapshots)
|
||||
"$BACKUP_DIR/list-snapshots" "$2"
|
||||
;;
|
||||
restore)
|
||||
"$BACKUP_DIR/restore" "$2" --test
|
||||
;;
|
||||
restore-prod)
|
||||
"$BACKUP_DIR/restore" "$2" --production
|
||||
;;
|
||||
cleanup-test)
|
||||
"$BACKUP_DIR/cleanup-test" "$2"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown command '${1:-}'"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
11
backup/restic.conf
Normal file
11
backup/restic.conf
Normal file
@ -0,0 +1,11 @@
|
||||
# Restic configuration for Paperless NGX backups
|
||||
# Generated on Mon Jun 9 11:45:41 CEST 2025
|
||||
|
||||
# Repository path
|
||||
export RESTIC_REPOSITORY="/mnt/data/backup/quantumrick"
|
||||
|
||||
# Repository password
|
||||
export RESTIC_PASSWORD="CHANGE-ME"
|
||||
|
||||
# Cache directory (optional)
|
||||
export RESTIC_CACHE_DIR="/tmp/restic-cache"
|
478
backup/restore
Executable file
478
backup/restore
Executable file
@ -0,0 +1,478 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Restic backup restore script
|
||||
# Location: /home/citadel/backup/restore
|
||||
# Usage: ./restore <service_name> [--test|--production]
|
||||
|
||||
set -euo pipefail # Strict mode: exit on error, undefined vars, pipe failures
|
||||
|
||||
# Configuration
|
||||
readonly BACKUP_DIR="/home/citadel/backup"
|
||||
readonly SERVICES_DIR="/home/citadel/services"
|
||||
readonly CONFIG_FILE="$BACKUP_DIR/restic.conf"
|
||||
readonly SCRIPT_NAME=$(basename "$0")
|
||||
readonly VERSION="2.1.0"
|
||||
|
||||
# Global variables
|
||||
USER_CHOICE=""
|
||||
|
||||
# Colors
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly NC='\033[0m'
|
||||
|
||||
# Logging functions
|
||||
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
local deps=("restic" "jq" "awk" "grep")
|
||||
local missing=()
|
||||
|
||||
for dep in "${deps[@]}"; do
|
||||
if ! command -v "$dep" &>/dev/null; then
|
||||
missing+=("$dep")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -ne 0 ]; then
|
||||
error "Missing dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show help
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Usage: $SCRIPT_NAME <service_name> [OPTIONS]
|
||||
|
||||
Options:
|
||||
--test Restore to test instance (default)
|
||||
--production Restore to production instance
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
|
||||
Examples:
|
||||
$SCRIPT_NAME paperless # Restore in test mode (default)
|
||||
$SCRIPT_NAME paperless --test # Restore to test instance
|
||||
$SCRIPT_NAME paperless --production # Restore to production (requires confirmation)
|
||||
|
||||
Available services:
|
||||
EOF
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Secure validation of config file
|
||||
if grep -qE '^[^#]*export\s+RESTIC_' "$CONFIG_FILE"; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
# List services from snapshots
|
||||
if restic snapshots &>/dev/null; then
|
||||
restic snapshots --json 2>/dev/null | \
|
||||
jq -r '.[].tags[]' 2>/dev/null | \
|
||||
grep -v -E '^(daily|weekly|monthly|yearly)$' | \
|
||||
sort -u | sed 's/^/ /' || echo " (cannot list services)"
|
||||
else
|
||||
echo " (cannot access repository)"
|
||||
fi
|
||||
else
|
||||
error "Invalid configuration file format"
|
||||
fi
|
||||
else
|
||||
echo " (no config found)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
# Check configuration file
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
error "Config file not found: $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Check config file permissions
|
||||
if [ ! -r "$CONFIG_FILE" ]; then
|
||||
error "Cannot read config file: $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Secure validation before source
|
||||
if ! grep -qE '^[^#]*export\s+RESTIC_' "$CONFIG_FILE"; then
|
||||
error "Invalid configuration file format"
|
||||
fi
|
||||
|
||||
# Source config
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
# Check required variables
|
||||
if [ -z "${RESTIC_REPOSITORY:-}" ]; then
|
||||
error "RESTIC_REPOSITORY not defined in config"
|
||||
fi
|
||||
|
||||
# Test restic access
|
||||
if ! restic snapshots &>/dev/null; then
|
||||
error "Cannot access Restic repository"
|
||||
fi
|
||||
|
||||
log "Prerequisites OK"
|
||||
}
|
||||
|
||||
# Show available snapshots
|
||||
show_snapshots() {
|
||||
local service="$1"
|
||||
|
||||
# Validate service name
|
||||
if [[ ! "$service" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||||
error "Invalid service name format"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log "Available snapshots for $service:"
|
||||
echo ""
|
||||
|
||||
# Get snapshots with error handling
|
||||
local snapshots_json
|
||||
if ! snapshots_json=$(restic snapshots --tag "$service" --json 2>/dev/null); then
|
||||
error "Failed to retrieve snapshots"
|
||||
fi
|
||||
|
||||
if [ -z "$snapshots_json" ] || [ "$snapshots_json" = "[]" ]; then
|
||||
warning "No snapshots found for service: $service"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Format and display snapshots
|
||||
echo "$snapshots_json" | jq -r '.[] | "\(.short_id) \(.time[:19]) \(.summary.total_files_processed // "?") files"' | \
|
||||
awk 'BEGIN {printf "%-12s %-20s %s\n", "ID", "DATE", "FILES"; print "----------------------------------------"}
|
||||
{printf "%-12s %-20s %s\n", $1, $2, $3" "$4}'
|
||||
|
||||
echo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get user choice
|
||||
get_user_choice() {
|
||||
local max_attempts=3
|
||||
local attempt=0
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
echo ""
|
||||
echo "Enter snapshot ID (or 'latest' for most recent, 'q' to quit):"
|
||||
echo -n "> "
|
||||
read -r USER_CHOICE
|
||||
|
||||
echo ""
|
||||
|
||||
# Basic validation
|
||||
if [ "$USER_CHOICE" = "q" ] || [ "$USER_CHOICE" = "quit" ]; then
|
||||
log "Operation cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$USER_CHOICE" ]; then
|
||||
# Format validation (alphanumeric or "latest")
|
||||
if [[ "$USER_CHOICE" =~ ^[a-zA-Z0-9]+$ ]] || [ "$USER_CHOICE" = "latest" ]; then
|
||||
return 0
|
||||
else
|
||||
warning "Invalid snapshot ID format. Please use alphanumeric characters only."
|
||||
fi
|
||||
else
|
||||
warning "No selection made"
|
||||
fi
|
||||
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
error "Maximum attempts exceeded"
|
||||
}
|
||||
|
||||
# Resolve snapshot ID
|
||||
resolve_snapshot_id() {
|
||||
local service="$1"
|
||||
local choice="$2"
|
||||
|
||||
if [ "$choice" = "latest" ]; then
|
||||
local latest_id
|
||||
latest_id=$(restic snapshots --tag "$service" --json 2>/dev/null | jq -r '.[-1].short_id // empty')
|
||||
|
||||
if [ -z "$latest_id" ]; then
|
||||
error "No snapshots found for service: $service"
|
||||
fi
|
||||
|
||||
echo "$latest_id"
|
||||
else
|
||||
echo "$choice"
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify snapshot
|
||||
verify_snapshot() {
|
||||
local service="$1"
|
||||
local snapshot_id="$2"
|
||||
|
||||
log "Verifying snapshot $snapshot_id for service $service"
|
||||
|
||||
# Get snapshot info
|
||||
local snapshot_info
|
||||
snapshot_info=$(restic snapshots --tag "$service" --json 2>/dev/null | \
|
||||
jq -r ".[] | select(.short_id == \"$snapshot_id\")")
|
||||
|
||||
if [ -z "$snapshot_info" ]; then
|
||||
error "Snapshot $snapshot_id not found for service $service"
|
||||
fi
|
||||
|
||||
# Extract date
|
||||
local date
|
||||
date=$(echo "$snapshot_info" | jq -r '.time[:19]')
|
||||
|
||||
# Check integrity (optional but recommended)
|
||||
if restic check --read-data-subset=1/10 "$snapshot_id" &>/dev/null; then
|
||||
success "Found snapshot $snapshot_id from $date (integrity check passed)"
|
||||
else
|
||||
warning "Found snapshot $snapshot_id from $date (integrity check skipped)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check service capabilities
|
||||
check_service_capabilities() {
|
||||
local service="$1"
|
||||
local service_dir="$SERVICES_DIR/$service"
|
||||
|
||||
echo ""
|
||||
log "Checking service capabilities for $service"
|
||||
|
||||
# Check service directory exists
|
||||
if [ ! -d "$service_dir" ]; then
|
||||
error "Service directory not found: $service_dir"
|
||||
fi
|
||||
|
||||
# Check for custom restore script
|
||||
local custom_restore="$service_dir/restore"
|
||||
local has_custom_restore=false
|
||||
if [ -f "$custom_restore" ] && [ -x "$custom_restore" ]; then
|
||||
has_custom_restore=true
|
||||
success "Found custom restore script: $custom_restore"
|
||||
else
|
||||
log "No custom restore script found - will extract to temporary directory"
|
||||
fi
|
||||
|
||||
echo "$has_custom_restore"
|
||||
}
|
||||
|
||||
# Create secure temporary directory
|
||||
create_secure_temp_dir() {
|
||||
local prefix="${1:-restore}"
|
||||
local temp_dir
|
||||
|
||||
# Use mktemp to create a secure temporary directory
|
||||
temp_dir=$(mktemp -d "/tmp/${prefix}-$(date +%Y%m%d-%H%M%S)-XXXXXX")
|
||||
|
||||
if [ ! -d "$temp_dir" ]; then
|
||||
error "Failed to create temporary directory"
|
||||
fi
|
||||
|
||||
# Set restrictive permissions
|
||||
chmod 700 "$temp_dir"
|
||||
|
||||
echo "$temp_dir"
|
||||
}
|
||||
|
||||
# Extract snapshot to temporary directory
|
||||
extract_snapshot_to_temp() {
|
||||
local snapshot_id="$1"
|
||||
local temp_dir
|
||||
|
||||
temp_dir=$(create_secure_temp_dir "restore")
|
||||
|
||||
log "Extracting snapshot $snapshot_id to temporary directory"
|
||||
log "Target directory: $temp_dir"
|
||||
|
||||
# Extract with error handling
|
||||
if ! restic restore "$snapshot_id" --target "$temp_dir" 2>&1 | tee -a "$temp_dir/restore.log"; then
|
||||
error "Failed to extract snapshot"
|
||||
fi
|
||||
|
||||
success "Snapshot extracted to: $temp_dir"
|
||||
echo ""
|
||||
echo "📁 Contents of the restore:"
|
||||
ls -la "$temp_dir" | head -20
|
||||
echo ""
|
||||
echo "💡 Manual restore instructions:"
|
||||
echo " 1. Review the extracted data in: $temp_dir"
|
||||
echo " 2. Copy the required files to their destination"
|
||||
echo " 3. Verify permissions and ownership"
|
||||
echo " 4. Remove the temporary directory: rm -rf $temp_dir"
|
||||
echo ""
|
||||
echo "⚠️ The directory will remain until you delete it manually"
|
||||
}
|
||||
|
||||
# Run custom restore
|
||||
run_custom_restore() {
|
||||
local service="$1"
|
||||
local snapshot_id="$2"
|
||||
local mode="$3"
|
||||
local service_dir="$SERVICES_DIR/$service"
|
||||
local custom_restore="$service_dir/restore"
|
||||
|
||||
log "Running custom restore script for $service"
|
||||
log "Script: $custom_restore"
|
||||
log "Snapshot: $snapshot_id"
|
||||
log "Mode: $mode"
|
||||
|
||||
# Create restore log
|
||||
local log_file="/tmp/restore-${service}-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
# Change directory with verification
|
||||
if ! cd "$service_dir"; then
|
||||
error "Cannot change to service directory: $service_dir"
|
||||
fi
|
||||
|
||||
# Execute restore script with log capture
|
||||
if "$custom_restore" "$snapshot_id" "--$mode" 2>&1 | tee "$log_file"; then
|
||||
success "Custom restore script completed"
|
||||
log "Restore log saved to: $log_file"
|
||||
else
|
||||
error "Custom restore script failed. Check log: $log_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
# Check dependencies
|
||||
check_dependencies
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local service=""
|
||||
local mode="test" # Default mode: test (safer)
|
||||
|
||||
# Parse arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--test)
|
||||
mode="test"
|
||||
shift
|
||||
;;
|
||||
--production)
|
||||
mode="production"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-v|--version)
|
||||
echo "$SCRIPT_NAME version $VERSION"
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
if [ -z "$service" ]; then
|
||||
service="$1"
|
||||
else
|
||||
error "Too many arguments"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check service is specified
|
||||
if [ -z "$service" ]; then
|
||||
error "Service name required"
|
||||
fi
|
||||
|
||||
# Validate service name format
|
||||
if [[ ! "$service" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||||
error "Invalid service name format. Use only alphanumeric characters, hyphens, and underscores."
|
||||
fi
|
||||
|
||||
# Check service directory exists
|
||||
if [ ! -d "$SERVICES_DIR/$service" ]; then
|
||||
error "Service directory not found: $SERVICES_DIR/$service"
|
||||
fi
|
||||
|
||||
echo "=== Restore $service ==="
|
||||
log "Mode: $mode"
|
||||
|
||||
# Step 1: Check prerequisites
|
||||
check_prerequisites
|
||||
|
||||
# Step 2: Check service capabilities
|
||||
local has_custom_restore
|
||||
has_custom_restore=$(check_service_capabilities "$service")
|
||||
|
||||
# Step 3: Show snapshots
|
||||
if ! show_snapshots "$service"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: Get user choice
|
||||
get_user_choice
|
||||
local choice="$USER_CHOICE"
|
||||
|
||||
# Step 5: Resolve and verify snapshot
|
||||
local snapshot_id
|
||||
snapshot_id=$(resolve_snapshot_id "$service" "$choice")
|
||||
verify_snapshot "$service" "$snapshot_id"
|
||||
|
||||
# Step 6: Display restore plan
|
||||
echo ""
|
||||
log "Restore plan:"
|
||||
echo " Service: $service"
|
||||
echo " Snapshot: $snapshot_id"
|
||||
echo " Mode: $mode"
|
||||
|
||||
if [ "$has_custom_restore" = "true" ]; then
|
||||
echo " Method: Custom restore script"
|
||||
else
|
||||
echo " Method: Extract to temporary directory"
|
||||
fi
|
||||
|
||||
# Step 7: Final confirmation
|
||||
echo ""
|
||||
if [ "$mode" = "test" ]; then
|
||||
read -p "Proceed with TEST restore? [y/N]: " -r confirm
|
||||
if [[ ! $confirm =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}⚠️ WARNING: This is a PRODUCTION restore!${NC}"
|
||||
echo -e "${RED}This operation may overwrite existing data.${NC}"
|
||||
echo ""
|
||||
read -p "Type 'yes-production' to proceed: " -r confirm
|
||||
if [ "$confirm" != "yes-production" ]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 8: Execute restore
|
||||
echo ""
|
||||
if [ "$has_custom_restore" = "true" ]; then
|
||||
run_custom_restore "$service" "$snapshot_id" "$mode"
|
||||
else
|
||||
# No custom script, extract to temp for both test and production
|
||||
extract_snapshot_to_temp "$snapshot_id"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
success "Restore operation completed!"
|
||||
}
|
||||
|
||||
# Trap to clean up on interruption
|
||||
trap 'echo -e "\n${RED}Interrupted${NC}"; exit 130' INT TERM
|
||||
|
||||
# Entry point
|
||||
main "$@"
|
18
backup/service-backup@.service
Normal file
18
backup/service-backup@.service
Normal file
@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Backup Service for %i
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=citadel
|
||||
Group=citadel
|
||||
WorkingDirectory=/home/citadel/services/%i
|
||||
ExecStart=/home/citadel/services/%i/backup.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
14
backup/service-backup@.timer
Normal file
14
backup/service-backup@.timer
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Daily Backup Timer for %i
|
||||
Requires=service-backup@%i.service
|
||||
|
||||
[Timer]
|
||||
# Run daily at 3:00 AM
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
# If system was down during scheduled time, run on next boot
|
||||
Persistent=true
|
||||
# Add randomization to avoid conflicts with other services
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
Loading…
x
Reference in New Issue
Block a user