mirror of
https://github.com/buildplan/du_setup.git
synced 2025-12-29 16:14:59 +00:00
more fixing again
This commit is contained in:
@@ -937,432 +937,273 @@ setup_backup() {
|
|||||||
|
|
||||||
if ! confirm "Configure rsync-based backups to a remote SSH server?"; then
|
if ! confirm "Configure rsync-based backups to a remote SSH server?"; then
|
||||||
print_info "Skipping backup configuration."
|
print_info "Skipping backup configuration."
|
||||||
|
log "Backup configuration skipped by user."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate USERNAME
|
# --- Pre-flight Check ---
|
||||||
if [[ -z "$USERNAME" ]]; then
|
if [[ -z "$USERNAME" ]] || ! id "$USERNAME" >/dev/null 2>&1; then
|
||||||
print_error "USERNAME is not set. Please run user setup first."
|
print_error "Cannot configure backup: valid admin user ('$USERNAME') not found."
|
||||||
exit 1
|
log "Backup configuration failed: USERNAME variable not set or user does not exist."
|
||||||
fi
|
return 1
|
||||||
if ! id "$USERNAME" >/dev/null 2>&1; then
|
|
||||||
print_error "Invalid USERNAME '$USERNAME'. User does not exist. Please run user setup first."
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_warning "The backup cron job will run as root to ensure access to all files in /home/$USERNAME."
|
|
||||||
print_info "This requires copying the root SSH key to the remote server after script completion."
|
|
||||||
|
|
||||||
# Generate SSH key for root (if not exists)
|
|
||||||
local ROOT_SSH_DIR="/root/.ssh"
|
local ROOT_SSH_DIR="/root/.ssh"
|
||||||
local ROOT_SSH_KEY="$ROOT_SSH_DIR/id_ed25519"
|
local ROOT_SSH_KEY="$ROOT_SSH_DIR/id_ed25519"
|
||||||
|
local BACKUP_SCRIPT_PATH="/root/run_backup.sh"
|
||||||
|
local EXCLUDE_FILE_PATH="/root/rsync_exclude.txt"
|
||||||
|
local CRON_MARKER="#-*- managed by setup_harden script -*-"
|
||||||
|
|
||||||
|
# --- Generate SSH Key for Root ---
|
||||||
if [[ ! -f "$ROOT_SSH_KEY" ]]; then
|
if [[ ! -f "$ROOT_SSH_KEY" ]]; then
|
||||||
print_info "Generating SSH key for root..."
|
print_info "Generating a dedicated SSH key for root's backup job..."
|
||||||
mkdir -p "$ROOT_SSH_DIR"
|
mkdir -p "$ROOT_SSH_DIR" && chmod 700 "$ROOT_SSH_DIR"
|
||||||
chmod 700 "$ROOT_SSH_DIR"
|
ssh-keygen -t ed25519 -f "$ROOT_SSH_KEY" -N "" -q
|
||||||
ssh-keygen -t ed25519 -f "$ROOT_SSH_KEY" -N "" -q || {
|
|
||||||
print_error "Failed to generate SSH key."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
chown -R root:root "$ROOT_SSH_DIR"
|
chown -R root:root "$ROOT_SSH_DIR"
|
||||||
print_success "Root SSH key generated."
|
print_success "Root SSH key generated at $ROOT_SSH_KEY"
|
||||||
|
log "Generated root SSH key for backups."
|
||||||
else
|
else
|
||||||
print_info "Root SSH key already exists at $ROOT_SSH_KEY."
|
print_info "Existing root SSH key found at $ROOT_SSH_KEY."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up stale backup artifacts from previous runs
|
# --- Collect Backup Destination Details ---
|
||||||
local BACKUP_SCRIPT="/root/backup.sh"
|
local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH
|
||||||
local EXCLUDE_FILE="/root/backup_exclude.txt"
|
read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${NC}")" BACKUP_DEST
|
||||||
local CRON_FILE=$(mktemp)
|
read -rp "$(echo -e "${CYAN}Enter destination SSH port (Hetzner uses 23) [22]: ${NC}")" BACKUP_PORT
|
||||||
if [[ -f "$BACKUP_SCRIPT" ]]; then
|
read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/my_backups/): ${NC}")" REMOTE_BACKUP_PATH
|
||||||
print_info "Found existing backup script at $BACKUP_SCRIPT. It will be replaced."
|
|
||||||
rm -f "$BACKUP_SCRIPT" || {
|
|
||||||
print_error "Failed to remove stale backup script."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
if [[ -f "$EXCLUDE_FILE" ]]; then
|
|
||||||
print_info "Found existing exclude file at $EXCLUDE_FILE. It will be replaced."
|
|
||||||
rm -f "$EXCLUDE_FILE" || {
|
|
||||||
print_error "Failed to remove stale exclude file."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
# Remove existing backup cron job
|
|
||||||
crontab -u root -l > "$CRON_FILE" 2>/dev/null || true
|
|
||||||
if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then
|
|
||||||
print_info "Removing existing backup cron job..."
|
|
||||||
sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE"
|
|
||||||
crontab -u root "$CRON_FILE" || {
|
|
||||||
print_error "Failed to update crontab."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
rm -f "$CRON_FILE"
|
|
||||||
|
|
||||||
# Ask for backup destination details with retry logic
|
# Validate inputs
|
||||||
local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_DIR
|
|
||||||
local retry_count=0
|
|
||||||
local max_retries=3
|
|
||||||
while true; do
|
|
||||||
if ! read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de, or press Enter to skip): ${NC}")" BACKUP_DEST; then
|
|
||||||
print_error "Failed to read backup destination input."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ -z "$BACKUP_DEST" ]]; then
|
|
||||||
print_info "Backup destination not provided. Skipping backup configuration."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
print_error "Invalid backup destination format. Expected user@host."
|
|
||||||
(( retry_count++ ))
|
|
||||||
if [[ $retry_count -lt $max_retries ]]; then
|
|
||||||
print_info "Please try again ($retry_count/$max_retries attempts)."
|
|
||||||
else
|
|
||||||
if confirm "Retry again or skip backup configuration?" "y"; then
|
|
||||||
retry_count=0
|
|
||||||
print_info "Resetting retries. Please enter a valid backup destination."
|
|
||||||
else
|
|
||||||
print_info "Skipping backup configuration."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
read -rp "$(echo -e "${CYAN}Enter SSH port for backup destination [22]: ${NC}")" BACKUP_PORT
|
|
||||||
read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR
|
|
||||||
BACKUP_PORT=${BACKUP_PORT:-22}
|
BACKUP_PORT=${BACKUP_PORT:-22}
|
||||||
REMOTE_BACKUP_DIR=${REMOTE_BACKUP_DIR:-/home/backup/}
|
if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$ ]]; then
|
||||||
if ! validate_backup_port "$BACKUP_PORT"; then
|
print_error "Invalid backup destination format. Expected user@host."
|
||||||
print_error "Invalid SSH port. Must be between 1 and 65535."
|
return 1
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
if [[ ! "$REMOTE_BACKUP_DIR" =~ ^/[^[:space:]]*/$ ]]; then
|
if [[ ! "$REMOTE_BACKUP_PATH" =~ ^/[^[:space:]]*/$ ]]; then
|
||||||
print_error "Invalid remote backup path. Must start and end with '/' and contain no spaces."
|
print_error "Invalid remote backup path. Must start and end with '/' and contain no spaces."
|
||||||
exit 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
if ! [[ "$BACKUP_PORT" =~ ^[0-9]+$ && "$BACKUP_PORT" -ge 1 && "$BACKUP_PORT" -le 65535 ]]; then
|
||||||
# Handle SSH key copying options
|
print_error "Invalid SSH port. Must be between 1 and 65535."
|
||||||
echo -e "${CYAN}Choose how to copy the root SSH key to the backup destination:${NC}"
|
return 1
|
||||||
echo -e " 1) Automate with password (requires sshpass)"
|
|
||||||
echo -e " 2) Manual copy (run ssh-copy-id later)"
|
|
||||||
echo -e " 3) Skip (test connection or copy manually later)"
|
|
||||||
read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" KEY_COPY_CHOICE
|
|
||||||
case "$KEY_COPY_CHOICE" in
|
|
||||||
1)
|
|
||||||
# Ensure sshpass is installed
|
|
||||||
if ! command -v sshpass >/dev/null 2>&1; then
|
|
||||||
print_info "Installing sshpass for automated key copying..."
|
|
||||||
if ! apt-get install -y -qq sshpass; then
|
|
||||||
print_error "Failed to install sshpass. Falling back to manual copy instructions."
|
|
||||||
KEY_COPY_CHOICE=2
|
|
||||||
else
|
|
||||||
print_success "sshpass installed."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ "$KEY_COPY_CHOICE" == "1" ]]; then
|
|
||||||
read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD
|
|
||||||
echo
|
|
||||||
print_info "Attempting automated SSH key copy..."
|
|
||||||
if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" -s "$BACKUP_DEST" 2>/dev/null; then
|
|
||||||
print_success "SSH key copied successfully."
|
|
||||||
else
|
|
||||||
print_error "Automated SSH key copy failed."
|
|
||||||
print_warning "Falling back to manual copy instructions."
|
|
||||||
KEY_COPY_CHOICE=2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
print_info "Manual SSH key copy selected."
|
|
||||||
;;
|
|
||||||
3|*)
|
|
||||||
print_info "Skipping SSH key copy."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Display SSH key copy instructions if not automated
|
|
||||||
if [[ "$KEY_COPY_CHOICE" != "1" || ! -f /root/.ssh/known_hosts || ! grep -q "$BACKUP_DEST" /root/.ssh/known_hosts ]]; then
|
|
||||||
print_warning "ACTION REQUIRED: If not already done, copy the root SSH key to the backup destination to enable backups."
|
|
||||||
echo -e "${YELLOW}Root public key:${NC}"
|
|
||||||
cat "$ROOT_SSH_KEY.pub"
|
|
||||||
echo -e "${CYAN}Run this command on your local machine or another terminal:${NC}"
|
|
||||||
echo -e " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST"
|
|
||||||
print_info "You can copy the SSH key later, but the backup cron job will fail until this is done."
|
|
||||||
fi
|
fi
|
||||||
|
print_info "Backup target set to: ${BACKUP_DEST}:${REMOTE_BACKUP_PATH} on port ${BACKUP_PORT}"
|
||||||
|
|
||||||
# Optional SSH connection test
|
# --- Handle SSH Key Copy ---
|
||||||
if confirm "Test SSH connection to the backup destination (optional)?"; then
|
echo -e "${CYAN}Choose how to copy the root SSH key:${NC}"
|
||||||
print_info "Testing connection (timeout: 5 seconds)..."
|
echo -e " 1) Automate with password (requires sshpass, less secure)"
|
||||||
DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2)
|
echo -e " 2) Manual copy (recommended)"
|
||||||
if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$BACKUP_DEST" true 2>/dev/null; then
|
read -rp "$(echo -e "${CYAN}Enter choice (1-2) [2]: ${NC}")" KEY_COPY_CHOICE
|
||||||
print_success "SSH connection successful!"
|
KEY_COPY_CHOICE=${KEY_COPY_CHOICE:-2}
|
||||||
else
|
if [[ "$KEY_COPY_CHOICE" == "1" ]]; then
|
||||||
print_error "SSH connection failed."
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
print_info "Verify the following:"
|
print_info "Installing sshpass for automated key copying..."
|
||||||
print_info " 1. The key was copied: ${YELLOW}ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST${NC}"
|
apt-get install -y -qq sshpass || {
|
||||||
if command -v nc >/dev/null 2>&1; then
|
print_warning "Failed to install sshpass. Falling back to manual copy."
|
||||||
print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}nc -zv $DEST_HOST $BACKUP_PORT${NC}"
|
KEY_COPY_CHOICE=2
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
if [[ "$KEY_COPY_CHOICE" == "1" ]]; then
|
||||||
|
read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD
|
||||||
|
echo
|
||||||
|
if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" "$BACKUP_DEST" 2>/dev/null; then
|
||||||
|
print_success "SSH key copied successfully."
|
||||||
else
|
else
|
||||||
print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}Contact your network admin or try 'telnet $DEST_HOST $BACKUP_PORT'${NC}"
|
print_error "Automated SSH key copy failed. Please copy manually."
|
||||||
|
KEY_COPY_CHOICE=2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [[ "$KEY_COPY_CHOICE" == "2" ]]; then
|
||||||
|
print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination."
|
||||||
|
echo -e "This will allow the root user to connect without a password for automated backups."
|
||||||
|
echo -e "${YELLOW}The root user's public key is:${NC}"
|
||||||
|
cat "${ROOT_SSH_KEY}.pub"
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Run the following command from this server's terminal to copy the key:${NC}"
|
||||||
|
echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" \"${BACKUP_DEST}\"${NC}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
# Create exclude file
|
# --- Test SSH Connection ---
|
||||||
print_info "Creating rsync exclude file at $EXCLUDE_FILE..."
|
if confirm "Test SSH connection to the backup destination (recommended)?"; then
|
||||||
cat > "$EXCLUDE_FILE" <<EOF
|
print_info "Testing connection (timeout: 10 seconds)..."
|
||||||
.ansible/
|
if ! ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=10 "$BACKUP_DEST" true 2>/dev/null; then
|
||||||
.bash_history
|
print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open."
|
||||||
.bash_logout
|
print_info " - Copy key: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" \"$BACKUP_DEST\""
|
||||||
.bashrc
|
print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\""
|
||||||
|
else
|
||||||
|
print_success "SSH connection to backup destination successful!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Create Exclude File ---
|
||||||
|
print_info "Creating rsync exclude file at $EXCLUDE_FILE_PATH..."
|
||||||
|
tee "$EXCLUDE_FILE_PATH" > /dev/null <<'EOF'
|
||||||
|
# Default Exclusions
|
||||||
.cache/
|
.cache/
|
||||||
.cloud-locale-test.skip
|
|
||||||
.config/
|
|
||||||
.lesshst
|
|
||||||
.docker/
|
.docker/
|
||||||
.local/
|
.local/
|
||||||
.profile
|
.npm/
|
||||||
.selected_editor
|
.vscode-server/
|
||||||
.ssh/
|
*.log
|
||||||
.sudo_as_admin_successful
|
|
||||||
.wget-hsts
|
|
||||||
.wg-easy/
|
|
||||||
*~
|
|
||||||
*.tmp
|
*.tmp
|
||||||
|
node_modules/
|
||||||
|
.bash_history
|
||||||
|
.wget-hsts
|
||||||
EOF
|
EOF
|
||||||
if confirm "Add additional files/directories to exclude from backup?"; then
|
if confirm "Add more directories/files to the exclude list?"; then
|
||||||
read -rp "$(echo -e "${CYAN}Enter directories/files to exclude (space-separated, e.g., .cache/ .log): ${NC}")" EXCLUDE_ITEMS
|
# Use read -a to handle spaces in paths correctly
|
||||||
for item in $EXCLUDE_ITEMS; do
|
read -rp "$(echo -e "${CYAN}Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${NC}")" -a extra_excludes
|
||||||
echo "$item" >> "$EXCLUDE_FILE"
|
for item in "${extra_excludes[@]}"; do
|
||||||
|
echo "$item" >> "$EXCLUDE_FILE_PATH"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
chmod 600 "$EXCLUDE_FILE" || {
|
chmod 600 "$EXCLUDE_FILE_PATH"
|
||||||
print_error "Failed to set permissions on exclude file."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
print_success "Rsync exclude file created."
|
print_success "Rsync exclude file created."
|
||||||
|
|
||||||
# Ask for cron schedule
|
# --- Collect Cron Schedule ---
|
||||||
print_info "Configuring cron schedule for backups..."
|
local CRON_SCHEDULE
|
||||||
read -rp "$(echo -e "${CYAN}Enter cron schedule (e.g., '0 3 * * *' for daily at 3 AM): ${NC}")" CRON_SCHEDULE
|
print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help."
|
||||||
CRON_SCHEDULE=${CRON_SCHEDULE:-0 3 * * *}
|
read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE
|
||||||
if ! echo "$CRON_SCHEDULE" | grep -qE '^((\*|[0-9,-]+(/[0-9]+)?)\s*){5}$'; then
|
CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"}
|
||||||
print_error "Invalid cron expression. Using default daily at 3 AM."
|
if ! echo "$CRON_SCHEDULE" | grep -qE '^(((\*\/)?[0-9,-]+|\*)\s){4}((\*\/)?[0-9,-]+|\*)$'; then
|
||||||
CRON_SCHEDULE="0 3 * * *"
|
print_error "Invalid cron expression. Using default: 5 3 * * *"
|
||||||
|
CRON_SCHEDULE="5 3 * * *"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ask for notification preference
|
# --- Collect Notification Details ---
|
||||||
local NOTIFICATION_SETUP="none" NTFY_URL NTFY_TOPIC NTFY_TOKEN DISCORD_WEBHOOK
|
local NOTIFICATION_SETUP="none" NTFY_URL="" NTFY_TOKEN="" DISCORD_WEBHOOK=""
|
||||||
if confirm "Enable backup notifications?"; then
|
if confirm "Enable backup status notifications?"; then
|
||||||
echo -e "${CYAN}Choose notification method:${NC}"
|
echo -e "${CYAN}Select notification method: 1) ntfy.sh 2) Discord [1]: ${NC}"
|
||||||
echo -e " 1) ntfy"
|
read -r n_choice
|
||||||
echo -e " 2) Discord"
|
if [[ "$n_choice" == "2" ]]; then
|
||||||
echo -e " 3) None"
|
NOTIFICATION_SETUP="discord"
|
||||||
read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" NOTIFICATION_CHOICE
|
read -rp "$(echo -e "${CYAN}Enter Discord Webhook URL: ${NC}")" DISCORD_WEBHOOK
|
||||||
case "$NOTIFICATION_CHOICE" in
|
if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then
|
||||||
1)
|
print_error "Invalid Discord webhook URL."
|
||||||
read -rp "$(echo -e "${CYAN}Enter ntfy URL (e.g., https://ntfy.sh): ${NC}")" NTFY_URL
|
return 1
|
||||||
read -rp "$(echo -e "${CYAN}Enter ntfy topic: ${NC}")" NTFY_TOPIC
|
fi
|
||||||
read -rp "$(echo -e "${CYAN}Enter ntfy token (optional, press Enter to skip): ${NC}")" NTFY_TOKEN
|
else
|
||||||
NTFY_URL=${NTFY_URL:-https://ntfy.sh}
|
NOTIFICATION_SETUP="ntfy"
|
||||||
NTFY_TOPIC=${NTFY_TOPIC:-vps-backups}
|
read -rp "$(echo -e "${CYAN}Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${NC}")" NTFY_URL
|
||||||
NOTIFICATION_SETUP="ntfy"
|
read -rp "$(echo -e "${CYAN}Enter ntfy Access Token (optional): ${NC}")" NTFY_TOKEN
|
||||||
;;
|
if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then
|
||||||
2)
|
print_error "Invalid ntfy URL. Must start with http:// or https://."
|
||||||
read -rp "$(echo -e "${CYAN}Enter Discord webhook URL: ${NC}")" DISCORD_WEBHOOK
|
return 1
|
||||||
if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then
|
fi
|
||||||
print_error "Invalid Discord webhook URL."
|
fi
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
NOTIFICATION_SETUP="discord"
|
|
||||||
;;
|
|
||||||
*) NOTIFICATION_SETUP="none" ;;
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create backup script
|
# --- Generate the Backup Script ---
|
||||||
print_info "Creating backup script at $BACKUP_SCRIPT..."
|
print_info "Generating the backup script at $BACKUP_SCRIPT_PATH..."
|
||||||
cat > "$BACKUP_SCRIPT" <<EOF
|
tee "$BACKUP_SCRIPT_PATH" > /dev/null <<EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# rsync backup script for remote SSH server
|
# Generated by server setup script on $(date)
|
||||||
# Generated by setup_harden_debian_ubuntu.sh on \$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
set -Euo pipefail
|
set -Euo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
# Configuration
|
# --- CONFIGURATION ---
|
||||||
RSYNC_CMD="\$(command -v rsync)"
|
LOCAL_DIR="/home/${USERNAME}/"
|
||||||
HOSTNAME_CMD="\$(command -v hostname)"
|
REMOTE_DEST="${BACKUP_DEST}"
|
||||||
DATE_CMD="\$(command -v date)"
|
REMOTE_PATH="${REMOTE_BACKUP_PATH}"
|
||||||
STAT_CMD="\$(command -v stat)"
|
SSH_PORT="${BACKUP_PORT}"
|
||||||
MV_CMD="\$(command -v mv)"
|
EXCLUDE_FILE="${EXCLUDE_FILE_PATH}"
|
||||||
TOUCH_CMD="\$(command -v touch)"
|
LOG_FILE="/var/log/backup_rsync.log"
|
||||||
NC_CMD="\$(command -v nc)"
|
LOCK_FILE="/tmp/backup_rsync.lock"
|
||||||
AWK_CMD="\$(command -v awk)"
|
HOSTNAME="\$(hostname -f)"
|
||||||
NUMFMT_CMD="\$(command -v numfmt)"
|
NOTIFICATION_SETUP="${NOTIFICATION_SETUP}"
|
||||||
GREP_CMD="\$(command -v grep)"
|
NTFY_URL="${NTFY_URL}"
|
||||||
|
NTFY_TOKEN="${NTFY_TOKEN}"
|
||||||
|
DISCORD_WEBHOOK="${DISCORD_WEBHOOK}"
|
||||||
|
EOF
|
||||||
|
|
||||||
LOCAL_DIR="/home/$USERNAME/"
|
# Use a quoted heredoc to write the script logic literally, preventing variable expansion
|
||||||
REMOTE_DIR="$REMOTE_BACKUP_DIR"
|
tee -a "$BACKUP_SCRIPT_PATH" > /dev/null <<'EOF'
|
||||||
BACKUP_DEST="$BACKUP_DEST"
|
|
||||||
EXCLUDE_FILE="$EXCLUDE_FILE"
|
# --- SCRIPT LOGIC ---
|
||||||
SSH_PORT="$BACKUP_PORT"
|
|
||||||
LOG_FILE="/var/log/backup_\$(date +%Y%m%d_%H%M%S).log"
|
|
||||||
MAX_LOG_SIZE=10485760 # 10 MB
|
|
||||||
NOTIFICATION_SETUP="$NOTIFICATION_SETUP"
|
|
||||||
EOF
|
|
||||||
if [[ "$NOTIFICATION_SETUP" != "none" ]]; then
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<EOF
|
|
||||||
CURL_CMD="\$(command -v curl)"
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<EOF
|
|
||||||
NTFY_URL="$NTFY_URL/$NTFY_TOPIC"
|
|
||||||
NTFY_TOKEN="$NTFY_TOKEN"
|
|
||||||
EOF
|
|
||||||
elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<EOF
|
|
||||||
DISCORD_WEBHOOK="$DISCORD_WEBHOOK"
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<'EOF'
|
|
||||||
# Notification function
|
|
||||||
send_notification() {
|
send_notification() {
|
||||||
local title="$1"
|
local status="$1"
|
||||||
local message="$2"
|
local message="$2"
|
||||||
local priority="${3:-default}"
|
local title
|
||||||
local color=65280 # Green for success
|
local color
|
||||||
if [[ "$title" == *"FAILED"* ]]; then
|
|
||||||
color=16711680 # Red for failure
|
if [[ "$status" == "SUCCESS" ]]; then
|
||||||
|
title="✅ Backup SUCCESS: $HOSTNAME"
|
||||||
|
color=3066993 # Green
|
||||||
|
else
|
||||||
|
title="❌ Backup FAILED: $HOSTNAME"
|
||||||
|
color=15158332 # Red
|
||||||
fi
|
fi
|
||||||
EOF
|
|
||||||
if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then
|
if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then
|
||||||
cat >> "$BACKUP_SCRIPT" <<EOF
|
curl -s -H "Title: $title" \
|
||||||
"\${CURL_CMD:-true}" -s ${NTFY_TOKEN:+-u :"$NTFY_TOKEN"} \
|
${NTFY_TOKEN:+-H "Authorization: Bearer $NTFY_TOKEN"} \
|
||||||
-H "Title: $title" \
|
-d "$message" "$NTFY_URL" > /dev/null 2>&1
|
||||||
-H "Priority: $priority" \
|
|
||||||
-d "$message" \
|
|
||||||
"$NTFY_URL" > /dev/null 2>> "$LOG_FILE"
|
|
||||||
EOF
|
|
||||||
elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then
|
elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then
|
||||||
cat >> "$BACKUP_SCRIPT" <<EOF
|
# Escape JSON special characters in the message
|
||||||
"\${CURL_CMD:-true}" -s -H "Content-Type: application/json" \
|
local escaped_message
|
||||||
-d "{\"embeds\": [{\"title\": \"$title\", \"description\": \"$message\", \"color\": $color}]}" \
|
escaped_message=$(echo "$message" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||||
"$DISCORD_WEBHOOK" > /dev/null 2>> "$LOG_FILE"
|
local json_payload
|
||||||
EOF
|
json_payload=$(printf '{"embeds": [{"title": "%s", "description": "%s", "color": %d}]}' "$title" "$escaped_message" "$color")
|
||||||
else
|
curl -s -H "Content-Type: application/json" -d "$json_payload" "$DISCORD_WEBHOOK" > /dev/null 2>&1
|
||||||
cat >> "$BACKUP_SCRIPT" <<'EOF'
|
|
||||||
: # No notifications configured
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<'EOF'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Format backup stats
|
|
||||||
format_backup_stats() {
|
|
||||||
local stats_line
|
|
||||||
stats_line=$("$GREP_CMD" 'Total transferred file size' "$LOG_FILE" | tail -n 1)
|
|
||||||
if [ -n "$stats_line" ]; then
|
|
||||||
local bytes
|
|
||||||
bytes=$(echo "$stats_line" | "$AWK_CMD" '{gsub(/,/, ""); print $5}')
|
|
||||||
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
|
|
||||||
local human_readable
|
|
||||||
human_readable=$("$NUMFMT_CMD" --to=iec-i --suffix=B --format="%.2f" "$bytes")
|
|
||||||
printf "Data Transferred: $human_readable"
|
|
||||||
else
|
|
||||||
printf "Data Transferred: 0 B (No changes)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "See log for statistics."
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dependency check
|
# --- DEPENDENCY CHECKS ---
|
||||||
EOF
|
# Corrected dependency check loop
|
||||||
if [[ "$NOTIFICATION_SETUP" != "none" ]]; then
|
for cmd in rsync curl numfmt awk flock; do
|
||||||
cat >> "$BACKUP_SCRIPT" <<'EOF'
|
|
||||||
if [[ -n "${CURL_CMD:-}" ]] && ! command -v "$CURL_CMD" &>/dev/null; then
|
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: curl not found" >> "$LOG_FILE"
|
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "curl not found" "high"
|
|
||||||
exit 10
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
cat >> "$BACKUP_SCRIPT" <<'EOF'
|
|
||||||
for cmd in "$RSYNC_CMD" "$NC_CMD" "$AWK_CMD" "$NUMFMT_CMD" "$GREP_CMD" "$HOSTNAME_CMD" "$DATE_CMD" "$STAT_CMD" "$MV_CMD" "$TOUCH_CMD"; do
|
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Required command not found at '$cmd'" >> "$LOG_FILE"
|
echo "FATAL: Required command '$cmd' not found. Please install it." >> "$LOG_FILE"
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Required command not found at '$cmd'" "high"
|
send_notification "FAILURE" "FATAL: Required command '$cmd' not found."
|
||||||
exit 10
|
exit 10
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Pre-flight checks
|
# Ensure single instance
|
||||||
if [[ ! -f "$EXCLUDE_FILE" ]]; then
|
exec 200>"$LOCK_FILE"
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Exclude file not found at $EXCLUDE_FILE" >> "$LOG_FILE"
|
flock -n 200 || { echo "Backup already running."; exit 1; }
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Exclude file not found at $EXCLUDE_FILE" "high"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
if [[ "$LOCAL_DIR" != */ ]]; then
|
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: LOCAL_DIR must end with a trailing slash" >> "$LOG_FILE"
|
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "LOCAL_DIR must end with a trailing slash" "high"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Log rotation
|
# Log rotation
|
||||||
if [ -f "$LOG_FILE" ] && [ "$("$STAT_CMD" -c%s "$LOG_FILE")" -gt "$MAX_LOG_SIZE" ]; then
|
touch "$LOG_FILE"
|
||||||
"$MV_CMD" "$LOG_FILE" "${LOG_FILE}.$("$DATE_CMD" +%Y%m%d_%H%M%S)"
|
chmod 600 "$LOG_FILE"
|
||||||
"$TOUCH_CMD" "$LOG_FILE"
|
if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then
|
||||||
|
mv "$LOG_FILE" "${LOG_FILE}.1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Network connectivity check
|
echo "--- Starting Backup at $(date) ---" >> "$LOG_FILE"
|
||||||
DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2)
|
|
||||||
if ! "$NC_CMD" -z -w 5 "$DEST_HOST" "$SSH_PORT"; then
|
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Cannot reach $DEST_HOST on port $SSH_PORT" >> "$LOG_FILE"
|
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Cannot reach $DEST_HOST on port $SSH_PORT" "high"
|
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rsync backup
|
# Run rsync
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Starting rsync backup for $("$HOSTNAME_CMD")" >> "$LOG_FILE"
|
# The '-R' option preserves the full path from the source
|
||||||
if LC_ALL=C "$RSYNC_CMD" -avz --stats --delete --partial --timeout=60 --exclude-from="$EXCLUDE_FILE" -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "$BACKUP_DEST:$REMOTE_DIR" >> "$LOG_FILE" 2>&1; then
|
rsync_output=$(rsync -avzR --delete --stats --exclude-from="$EXCLUDE_FILE" \
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] SUCCESS: rsync completed successfully." >> "$LOG_FILE"
|
-e "ssh -p $SSH_PORT" "$LOCAL_DIR" "${REMOTE_DEST}:${REMOTE_PATH}" 2>&1)
|
||||||
BACKUP_STATS=$(format_backup_stats)
|
|
||||||
send_notification "✅ Backup SUCCESS: $("$HOSTNAME_CMD")" "rsync backup completed successfully.\n\n$BACKUP_STATS"
|
rsync_exit_code=$?
|
||||||
|
echo "$rsync_output" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Check status and send notification
|
||||||
|
if [[ $rsync_exit_code -eq 0 ]]; then
|
||||||
|
data_transferred=$(echo "$rsync_output" | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g')
|
||||||
|
human_readable=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$data_transferred")
|
||||||
|
message="Backup completed successfully.\\nData Transferred: ${human_readable}"
|
||||||
|
send_notification "SUCCESS" "$message"
|
||||||
|
echo "--- Backup SUCCEEDED at $(date) ---" >> "$LOG_FILE"
|
||||||
else
|
else
|
||||||
EXIT_CODE=$?
|
message="rsync failed with exit code ${rsync_exit_code}. Check log for details."
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FAILED: rsync exited with status code: $EXIT_CODE" >> "$LOG_FILE"
|
send_notification "FAILURE" "$message"
|
||||||
send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "rsync failed with exit code $EXIT_CODE. Check log: $LOG_FILE" "high"
|
echo "--- Backup FAILED at $(date) ---" >> "$LOG_FILE"
|
||||||
exit $EXIT_CODE
|
|
||||||
fi
|
fi
|
||||||
echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Run Finished" >> "$LOG_FILE"
|
|
||||||
EOF
|
EOF
|
||||||
chmod 700 "$BACKUP_SCRIPT" || {
|
|
||||||
print_error "Failed to set permissions on backup script."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
print_success "Backup script created at $BACKUP_SCRIPT."
|
|
||||||
|
|
||||||
# Add to crontab
|
chmod 700 "$BACKUP_SCRIPT_PATH"
|
||||||
print_info "Adding cron job for root..."
|
print_success "Backup script created."
|
||||||
CRON_FILE=$(mktemp)
|
|
||||||
crontab -u root -l > "$CRON_FILE" 2>/dev/null || true
|
# --- Configure Cron Job ---
|
||||||
if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then
|
print_info "Configuring root cron job..."
|
||||||
print_info "Cron job for $BACKUP_SCRIPT already exists. Updating schedule..."
|
# This robustly removes the old job (if any) and adds the new one
|
||||||
sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE"
|
(crontab -u root -l 2>/dev/null | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root -
|
||||||
fi
|
|
||||||
echo "$CRON_SCHEDULE $BACKUP_SCRIPT" >> "$CRON_FILE"
|
print_success "Backup cron job scheduled: $CRON_SCHEDULE"
|
||||||
crontab -u root "$CRON_FILE" || {
|
|
||||||
print_error "Failed to update crontab."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
rm -f "$CRON_FILE"
|
|
||||||
print_success "Cron job added for root: $CRON_SCHEDULE $BACKUP_SCRIPT"
|
|
||||||
log "Backup configuration completed."
|
log "Backup configuration completed."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user