#!/bin/bash set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Script configuration SCRIPT_VERSION="1.1.0" CONFIG_DIR="/etc/proxmox-backup-client" TARGETS_DIR="$CONFIG_DIR/targets" LOG_FILE="/var/log/pbs-client-installer.log" # Helper functions log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" } error() { echo -e "${RED}[ERROR]${NC} $1" >&2 echo "[ERROR] $1" >> "$LOG_FILE" } warn() { echo -e "${YELLOW}[WARN]${NC} $1" echo "[WARN] $1" >> "$LOG_FILE" } info() { echo -e "${BLUE}[INFO]${NC} $1" } prompt() { local prompt_text="$1" local default_value="$2" local user_input if [ -n "$default_value" ]; then read -p "$(echo -e ${BLUE}${prompt_text}${NC} [${default_value}]: )" user_input echo "${user_input:-$default_value}" else read -p "$(echo -e ${BLUE}${prompt_text}${NC}: )" user_input echo "$user_input" fi } prompt_password() { local prompt_text="$1" local user_input read -sp "$(echo -e ${BLUE}${prompt_text}${NC}: )" user_input echo >&2 # Output newline to stderr so it doesn't get captured echo "$user_input" } # Multi-target helper functions list_targets() { if [ ! -d "$TARGETS_DIR" ]; then return 1 fi local targets=() for config in "$TARGETS_DIR"/*.conf; do if [ -f "$config" ]; then targets+=("$(basename "$config" .conf)") fi done if [ ${#targets[@]} -eq 0 ]; then return 1 fi printf '%s\n' "${targets[@]}" } validate_target_name() { local name="$1" # Check if name is empty if [ -z "$name" ]; then error "Target name cannot be empty" return 1 fi # Check if name contains only alphanumeric, dash, underscore if ! [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then error "Target name can only contain letters, numbers, dash, and underscore" return 1 fi return 0 } target_exists() { local name="$1" [ -f "$TARGETS_DIR/$name.conf" ] } get_target_config_path() { local name="$1" echo "$TARGETS_DIR/$name.conf" } migrate_legacy_config() { # Check if old single-target config exists if [ -f "$CONFIG_DIR/config" ] && [ ! -d "$TARGETS_DIR" ]; then log "Migrating legacy configuration to multi-target system..." mkdir -p "$TARGETS_DIR" mv "$CONFIG_DIR/config" "$TARGETS_DIR/default.conf" # Rename old backup script if it exists if [ -f "$CONFIG_DIR/backup.sh" ]; then mv "$CONFIG_DIR/backup.sh" "$CONFIG_DIR/backup-default.sh" fi # Rename old services if they exist if [ -f "/etc/systemd/system/pbs-backup.service" ]; then systemctl stop pbs-backup.service pbs-backup.timer 2>/dev/null || true systemctl disable pbs-backup.service pbs-backup.timer 2>/dev/null || true mv /etc/systemd/system/pbs-backup.service /etc/systemd/system/pbs-backup-default.service mv /etc/systemd/system/pbs-backup-manual.service /etc/systemd/system/pbs-backup-default-manual.service 2>/dev/null || true mv /etc/systemd/system/pbs-backup.timer /etc/systemd/system/pbs-backup-default.timer # Update service files to point to new backup script sed -i 's|/etc/proxmox-backup-client/backup.sh|/etc/proxmox-backup-client/backup-default.sh|g' \ /etc/systemd/system/pbs-backup-default.service \ /etc/systemd/system/pbs-backup-default-manual.service 2>/dev/null || true systemctl daemon-reload systemctl enable pbs-backup-default.timer systemctl start pbs-backup-default.timer log "Legacy configuration migrated to 'default' target" fi fi } show_targets_list() { echo echo "════════════════════════════════════════" echo " Configured Backup Targets" echo "════════════════════════════════════════" echo if ! list_targets >/dev/null 2>&1; then warn "No backup targets configured" return 1 fi list_targets | while read -r target; do local config_file="$(get_target_config_path "$target")" if [ -f "$config_file" ]; then source "$config_file" echo "Target: $target" echo " Server: ${PBS_SERVER:-unknown}:${PBS_PORT:-8007}" echo " Datastore: ${PBS_DATASTORE:-unknown}" echo " Type: ${BACKUP_TYPE:-unknown}" echo " Schedule: ${TIMER_SCHEDULE:-unknown}" # Check if timer is active if systemctl is-enabled "pbs-backup-${target}.timer" &>/dev/null; then if systemctl is-active "pbs-backup-${target}.timer" &>/dev/null; then echo " Status: ✓ Active" else echo " Status: ✗ Inactive" fi else echo " Status: ✗ Disabled" fi echo fi done } add_target() { echo log "Adding new backup target..." echo # Get target name while true; do TARGET_NAME=$(prompt "Enter target name (e.g., 'offsite', 'local', 'backup1')" "") if ! validate_target_name "$TARGET_NAME"; then continue fi if target_exists "$TARGET_NAME"; then error "Target '$TARGET_NAME' already exists" continue fi break done # Run interactive config for this target interactive_config_for_target "$TARGET_NAME" } edit_target() { echo if ! list_targets >/dev/null 2>&1; then error "No targets configured" return 1 fi echo "Available targets:" list_targets | nl echo TARGET_NAME=$(prompt "Enter target name to edit" "") if ! validate_target_name "$TARGET_NAME"; then return 1 fi if ! target_exists "$TARGET_NAME"; then error "Target '$TARGET_NAME' does not exist" return 1 fi log "Editing target: $TARGET_NAME" echo echo "What would you like to edit?" echo " 1) Connection only (server/credentials)" echo " 2) Backup settings (type/schedule/retention)" echo " 3) Full reconfiguration" echo " 4) Cancel" local EDIT_CHOICE=$(prompt "Select option [1/2/3/4]" "1") case "$EDIT_CHOICE" in 1) reconfigure_connection_for_target "$TARGET_NAME" ;; 2) reconfigure_backup_settings_for_target "$TARGET_NAME" ;; 3) interactive_config_for_target "$TARGET_NAME" ;; 4) info "Cancelled" return 0 ;; *) error "Invalid option" return 1 ;; esac } delete_target() { echo if ! list_targets >/dev/null 2>&1; then error "No targets configured" return 1 fi echo "Available targets:" list_targets | nl echo TARGET_NAME=$(prompt "Enter target name to delete" "") if ! validate_target_name "$TARGET_NAME"; then return 1 fi if ! target_exists "$TARGET_NAME"; then error "Target '$TARGET_NAME' does not exist" return 1 fi warn "This will delete target '$TARGET_NAME' and stop all associated backups" local CONFIRM=$(prompt "Are you sure? Type 'yes' to confirm" "no") if [ "$CONFIRM" != "yes" ]; then info "Cancelled" return 0 fi log "Deleting target: $TARGET_NAME" # Stop and disable services systemctl stop "pbs-backup-${TARGET_NAME}.service" "pbs-backup-${TARGET_NAME}.timer" 2>/dev/null || true systemctl disable "pbs-backup-${TARGET_NAME}.service" "pbs-backup-${TARGET_NAME}.timer" 2>/dev/null || true # Remove files rm -f "/etc/systemd/system/pbs-backup-${TARGET_NAME}.service" rm -f "/etc/systemd/system/pbs-backup-${TARGET_NAME}-manual.service" rm -f "/etc/systemd/system/pbs-backup-${TARGET_NAME}.timer" rm -f "$CONFIG_DIR/backup-${TARGET_NAME}.sh" rm -f "$(get_target_config_path "$TARGET_NAME")" systemctl daemon-reload log "Target '$TARGET_NAME' deleted successfully" } show_target_detail() { local target="$1" if ! target_exists "$target"; then error "Target '$target' does not exist" return 1 fi local config_file="$(get_target_config_path "$target")" source "$config_file" echo echo "════════════════════════════════════════" echo " Target: $target" echo "════════════════════════════════════════" echo echo "Connection:" echo " Server: ${PBS_SERVER}:${PBS_PORT}" echo " Datastore: ${PBS_DATASTORE}" echo " Repository: ${PBS_REPOSITORY}" echo echo "Backup Configuration:" echo " Type: ${BACKUP_TYPE}" if [[ "$BACKUP_TYPE" == "files" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Paths: ${BACKUP_PATHS}" echo " Exclusions: ${EXCLUDE_PATTERNS}" fi if [[ "$BACKUP_TYPE" == "block" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Block Device: ${BLOCK_DEVICE}" fi echo echo "Schedule:" if [[ "$BACKUP_TYPE" == "both" ]]; then echo " Files: ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR})" echo " Block: Weekly on Sunday" else echo " ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR})" fi echo echo "Retention:" echo " Last: ${KEEP_LAST}" echo " Daily: ${KEEP_DAILY}" echo " Weekly: ${KEEP_WEEKLY}" echo " Monthly: ${KEEP_MONTHLY}" echo # Show timer status if systemctl is-enabled "pbs-backup-${target}.timer" &>/dev/null; then echo "Next scheduled backup:" systemctl list-timers "pbs-backup-${target}.timer" --no-pager 2>/dev/null || true else warn "Timer not enabled for this target" fi } # Check if running as root check_root() { if [ "$EUID" -ne 0 ]; then error "This script must be run as root (use sudo)" exit 1 fi } # Detect Linux distribution detect_distro() { log "Detecting Linux distribution..." if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID OS_VERSION=$VERSION_ID OS_PRETTY=$PRETTY_NAME elif [ -f /etc/lsb-release ]; then . /etc/lsb-release OS=$DISTRIB_ID OS_VERSION=$DISTRIB_RELEASE OS_PRETTY=$DISTRIB_DESCRIPTION else error "Cannot detect Linux distribution" exit 1 fi info "Detected: $OS_PRETTY" # Normalize OS names OS=$(echo "$OS" | tr '[:upper:]' '[:lower:]') } # Install PBS client on Ubuntu install_ubuntu() { log "Installing Proxmox Backup Client on Ubuntu $OS_VERSION..." # Determine which repository to use if [[ "$OS_VERSION" == "24.04" ]] || [[ "$OS_VERSION" > "24" ]]; then REPO="bookworm" GPG_FILE="proxmox-release-bookworm.gpg" elif [[ "$OS_VERSION" == "22.04" ]]; then REPO="bullseye" GPG_FILE="proxmox-release-bullseye.gpg" # Need to add Focal security repo for libssl1.1 NEED_FOCAL=true elif [[ "$OS_VERSION" == "20.04" ]]; then REPO="bullseye" GPG_FILE="proxmox-release-bullseye.gpg" else error "Unsupported Ubuntu version: $OS_VERSION" exit 1 fi # Download and install GPG key log "Downloading GPG key..." wget -q "https://enterprise.proxmox.com/debian/${GPG_FILE}" \ -O "/etc/apt/trusted.gpg.d/${GPG_FILE}" # Add PBS client repository log "Adding PBS client repository..." echo "deb [arch=amd64] http://download.proxmox.com/debian/pbs-client ${REPO} main" > \ /etc/apt/sources.list.d/pbs-client.list # Add Focal security repo if needed (Ubuntu 22.04) if [ "$NEED_FOCAL" = true ]; then log "Adding Ubuntu Focal security repository for libssl1.1..." echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu focal-security main" > \ /etc/apt/sources.list.d/focal-security.list fi # Update and install log "Updating package lists..." apt-get update -qq log "Installing proxmox-backup-client..." apt-get install -y proxmox-backup-client log "PBS client installed successfully" } # Install PBS client on Debian install_debian() { log "Installing Proxmox Backup Client on Debian $OS_VERSION..." # Determine repository based on Debian version case "$OS_VERSION" in 12*) REPO="bookworm" GPG_FILE="proxmox-release-bookworm.gpg" ;; 11*) REPO="bullseye" GPG_FILE="proxmox-release-bullseye.gpg" ;; 10*) REPO="buster" GPG_FILE="proxmox-release-buster.gpg" ;; *) error "Unsupported Debian version: $OS_VERSION" exit 1 ;; esac # Download and install GPG key log "Downloading GPG key..." wget -q "https://enterprise.proxmox.com/debian/${GPG_FILE}" \ -O "/etc/apt/trusted.gpg.d/${GPG_FILE}" # Add PBS client repository log "Adding PBS client repository..." echo "deb [arch=amd64] http://download.proxmox.com/debian/pbs-client ${REPO} main" > \ /etc/apt/sources.list.d/pbs-client.list # Update and install log "Updating package lists..." apt-get update -qq log "Installing proxmox-backup-client..." apt-get install -y proxmox-backup-client log "PBS client installed successfully" } # Install PBS client on Arch Linux install_arch() { log "Installing Proxmox Backup Client on Arch Linux..." # Check if yay is installed if ! command -v yay &> /dev/null; then error "yay AUR helper is required but not installed" info "Install yay first: https://github.com/Jguer/yay" exit 1 fi # Get the user who invoked sudo if [ -n "$SUDO_USER" ]; then REAL_USER="$SUDO_USER" else error "Cannot determine original user. Run with sudo." exit 1 fi log "Installing proxmox-backup-client-bin from AUR..." sudo -u "$REAL_USER" yay -S --noconfirm proxmox-backup-client-bin log "PBS client installed successfully" } # Main installation function install_pbs_client() { case "$OS" in ubuntu) install_ubuntu ;; debian) install_debian ;; arch|archlinux) install_arch ;; *) error "Unsupported distribution: $OS" info "Supported distributions: Ubuntu, Debian, Arch Linux" exit 1 ;; esac # Verify installation if command -v proxmox-backup-client &> /dev/null; then local version=$(proxmox-backup-client version 2>/dev/null | head -n1) log "Installation verified: $version" return 0 else error "Installation verification failed" return 1 fi } # Reconfigure connection only (PBS server and credentials) reconfigure_connection() { log "Reconfiguring PBS server connection..." echo echo "======================================" echo " PBS Server Connection Setup" echo "======================================" echo # PBS Server details PBS_SERVER=$(prompt "Enter PBS server IP/hostname" "192.168.1.181") PBS_PORT=$(prompt "Enter PBS server port" "8007") PBS_DATASTORE=$(prompt "Enter datastore name" "backups") echo info "Authentication Method:" echo " 1) Username + Password" echo " 2) API Token (recommended for automation)" AUTH_METHOD=$(prompt "Select authentication method [1/2]" "2") if [ "$AUTH_METHOD" = "1" ]; then PBS_USERNAME=$(prompt "Enter username" "root") PBS_REALM=$(prompt "Enter realm" "pam") PBS_PASSWORD=$(prompt_password "Enter password") PBS_REPOSITORY="${PBS_USERNAME}@${PBS_REALM}@${PBS_SERVER}:${PBS_PORT}:${PBS_DATASTORE}" else PBS_USERNAME=$(prompt "Enter username" "backup") PBS_REALM=$(prompt "Enter realm" "pam") PBS_TOKEN_NAME=$(prompt "Enter token name" "backup-token") PBS_TOKEN_SECRET=$(prompt_password "Enter token secret") PBS_REPOSITORY="${PBS_USERNAME}@${PBS_REALM}!${PBS_TOKEN_NAME}@${PBS_SERVER}:${PBS_PORT}:${PBS_DATASTORE}" PBS_PASSWORD="$PBS_TOKEN_SECRET" fi # Test the new connection if ! test_connection; then error "Connection test failed with new credentials" RETRY=$(prompt "Try again? (yes/no)" "yes") if [[ "$RETRY" == "yes" ]]; then reconfigure_connection return else error "Cannot proceed without successful connection" exit 1 fi fi # Load existing configuration and update only connection details if [ -f "$CONFIG_DIR/config" ]; then log "Loading existing backup configuration..." source "$CONFIG_DIR/config" else error "No existing configuration found. Please run full configuration." exit 1 fi # Update config file with new connection details log "Updating configuration file..." # Strip any trailing newlines from password (defensive fix) PBS_PASSWORD_CLEAN=$(echo -n "$PBS_PASSWORD" | tr -d '\n\r') cat > "$CONFIG_DIR/config" </dev/null || echo "") if [ -n "$ROOT_DEVICE" ] && [ -b "$ROOT_DEVICE" ]; then BLOCK_DEVICE="$ROOT_DEVICE" fi fi BLOCK_DEVICE=$(prompt "Enter block device to backup" "$BLOCK_DEVICE") # Verify it's a block device if [ ! -b "$BLOCK_DEVICE" ]; then warn "Warning: $BLOCK_DEVICE does not exist or is not a block device" info "Available block devices:" lsblk -d -n -o NAME,SIZE,TYPE | grep disk | awk '{print " /dev/"$1" ("$2")"}' echo CONTINUE=$(prompt "Continue anyway? (yes/no)" "no") if [[ "$CONTINUE" != "yes" ]]; then error "Configuration cancelled" return 1 fi fi fi echo info "Backup Schedule:" echo " 1) Hourly" echo " 2) Daily (recommended)" echo " 3) Weekly" echo " 4) Custom" SCHEDULE_TYPE=$(prompt "Select schedule type [1/2/3/4]" "2") case "$SCHEDULE_TYPE" in 1) TIMER_SCHEDULE="hourly" TIMER_ONCALENDAR="hourly" ;; 2) BACKUP_HOUR=$(prompt "Enter hour for daily backup (0-23)" "2") TIMER_SCHEDULE="daily" TIMER_ONCALENDAR="*-*-* ${BACKUP_HOUR}:00:00" ;; 3) BACKUP_DOW=$(prompt "Enter day of week (Mon-Sun)" "Sun") BACKUP_HOUR=$(prompt "Enter hour (0-23)" "2") TIMER_SCHEDULE="weekly" TIMER_ONCALENDAR="${BACKUP_DOW} *-*-* ${BACKUP_HOUR}:00:00" ;; 4) TIMER_ONCALENDAR=$(prompt "Enter systemd OnCalendar format" "*-*-* 02:00:00") TIMER_SCHEDULE="custom" ;; esac echo # Retention policy info "Retention Policy:" KEEP_LAST=$(prompt "Keep last N backups" "${KEEP_LAST:-3}") KEEP_DAILY=$(prompt "Keep daily backups for N days" "${KEEP_DAILY:-7}") KEEP_WEEKLY=$(prompt "Keep weekly backups for N weeks" "${KEEP_WEEKLY:-4}") KEEP_MONTHLY=$(prompt "Keep monthly backups for N months" "${KEEP_MONTHLY:-6}") # Restore connection details PBS_REPOSITORY="$SAVED_PBS_REPOSITORY" PBS_PASSWORD="$SAVED_PBS_PASSWORD" # Regenerate systemd service with new settings echo log "Updating systemd service configuration..." create_systemd_service # Restart services log "Restarting backup timer..." systemctl daemon-reload systemctl restart pbs-backup.timer echo log "Backup settings updated successfully!" echo info "New Configuration:" echo " Backup Type: ${BACKUP_TYPE}" if [[ "$BACKUP_TYPE" == "files" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Backup Paths: ${BACKUP_PATHS}" fi if [[ "$BACKUP_TYPE" == "block" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Block Device: ${BLOCK_DEVICE}" fi if [[ "$BACKUP_TYPE" == "both" ]]; then echo " Schedule: Files ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR}), Block device weekly (Sunday)" else echo " Schedule: ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR})" fi echo " Retention: Last=${KEEP_LAST} Daily=${KEEP_DAILY} Weekly=${KEEP_WEEKLY} Monthly=${KEEP_MONTHLY}" echo # Show service status info "Backup Service Status:" echo "────────────────────────────────────────────────────────────" systemctl status pbs-backup.timer --no-pager -l || true echo "────────────────────────────────────────────────────────────" echo info "Next scheduled backup:" systemctl list-timers pbs-backup.timer --no-pager || true } # Per-target wrapper functions interactive_config_for_target() { local target_name="$1" TARGET_NAME="$target_name" log "Configuring backup target: $target_name" # Run standard interactive config interactive_config # Create systemd service for this target create_systemd_service_for_target "$target_name" # Test connection if ! test_connection; then error "Connection test failed" return 1 fi log "Target '$target_name' configured successfully!" } reconfigure_connection_for_target() { local target_name="$1" TARGET_NAME="$target_name" # Load existing config if [ ! -f "$(get_target_config_path "$target_name")" ]; then error "Target '$target_name' not found" return 1 fi source "$(get_target_config_path "$target_name")" # Run standard reconfigure_connection reconfigure_connection # Update systemd service create_systemd_service_for_target "$target_name" # Restart timer systemctl daemon-reload systemctl restart "pbs-backup-${target_name}.timer" log "Connection reconfigured for target '$target_name'" } reconfigure_backup_settings_for_target() { local target_name="$1" TARGET_NAME="$target_name" # Load existing config if [ ! -f "$(get_target_config_path "$target_name")" ]; then error "Target '$target_name' not found" return 1 fi source "$(get_target_config_path "$target_name")" # Run standard reconfigure_backup_settings reconfigure_backup_settings } run_backup_for_target() { local target_name="$1" if ! target_exists "$target_name"; then error "Target '$target_name' does not exist" return 1 fi info "Running FULL backup for target: $target_name" echo # Show real-time progress echo "╔════════════════════════════════════════════════════════════╗" echo "║ Backup Progress (Live) - Target: $target_name" echo "║ Press Ctrl+C to exit (backup continues in background) ║" echo "╚════════════════════════════════════════════════════════════╝" echo # Start the manual backup (forces full backup) systemctl start "pbs-backup-${target_name}-manual.service" & # Wait a brief moment for service to register sleep 0.5 # Monitor backup in background and kill journalctl when done ( while systemctl is-active --quiet "pbs-backup-${target_name}-manual.service"; do sleep 2 done # Service finished, kill the journal follow pkill -P $$ journalctl 2>/dev/null ) & MONITOR_PID=$! # Follow logs in foreground from the start journalctl -u "pbs-backup-${target_name}-manual.service" -f --since "2 seconds ago" || true # Wait for monitor to finish wait $MONITOR_PID 2>/dev/null # Clear any leftover output echo echo "════════════════════════════════════════════════════════════" # Check final status if systemctl status "pbs-backup-${target_name}-manual.service" | grep -q "Active: failed"; then echo error "Backup failed!" echo info "Check full logs with:" echo " sudo journalctl -u pbs-backup-${target_name}-manual.service -n 50" else echo log "Backup completed successfully!" fi } # Interactive configuration interactive_config() { log "Starting interactive configuration..." echo echo "======================================" echo " PBS Client Configuration" echo "======================================" echo # PBS Server details PBS_SERVER=$(prompt "Enter PBS server IP/hostname" "192.168.1.181") PBS_PORT=$(prompt "Enter PBS server port" "8007") PBS_DATASTORE=$(prompt "Enter datastore name" "backups") echo info "Authentication Method:" echo " 1) Username + Password" echo " 2) API Token (recommended for automation)" AUTH_METHOD=$(prompt "Select authentication method [1/2]" "2") if [ "$AUTH_METHOD" = "1" ]; then PBS_USERNAME=$(prompt "Enter username" "root") PBS_REALM=$(prompt "Enter realm" "pam") PBS_PASSWORD=$(prompt_password "Enter password") PBS_REPOSITORY="${PBS_USERNAME}@${PBS_REALM}@${PBS_SERVER}:${PBS_PORT}:${PBS_DATASTORE}" else PBS_USERNAME=$(prompt "Enter username" "backup") PBS_REALM=$(prompt "Enter realm" "pam") PBS_TOKEN_NAME=$(prompt "Enter token name" "backup-token") PBS_TOKEN_SECRET=$(prompt_password "Enter token secret") PBS_REPOSITORY="${PBS_USERNAME}@${PBS_REALM}!${PBS_TOKEN_NAME}@${PBS_SERVER}:${PBS_PORT}:${PBS_DATASTORE}" PBS_PASSWORD="$PBS_TOKEN_SECRET" fi echo # Backup type selection info "Backup Type:" echo " 1) File-level only (.pxar) - Fast, efficient, selective restore" echo " 2) Block device only (.img) - Full disk image, bootable as VM" echo " 3) Both (Hybrid) - Daily files + Weekly block device (recommended)" BACKUP_TYPE_CHOICE=$(prompt "Select backup type [1/2/3]" "3") case "$BACKUP_TYPE_CHOICE" in 1) BACKUP_TYPE="files" ;; 2) BACKUP_TYPE="block" ;; 3) BACKUP_TYPE="both" ;; *) warn "Invalid choice, defaulting to hybrid (both)" BACKUP_TYPE="both" ;; esac echo # Backup configuration info "Backup Configuration:" if [[ "$BACKUP_TYPE" == "files" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then BACKUP_PATHS=$(prompt "Enter paths to backup (space-separated)" "/") EXCLUDE_PATTERNS=$(prompt "Enter exclusion patterns (space-separated)" "/tmp /var/tmp /var/cache /proc /sys /dev /run") fi if [[ "$BACKUP_TYPE" == "block" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo info "Block Device Configuration:" # Try to auto-detect root device # Strip subvolume notation [/@], partition numbers, and trailing 'p' ROOT_DEVICE=$(findmnt -n -o SOURCE / | sed 's/\[.*\]$//' | sed 's/[0-9]*$//' | sed 's/p$//' 2>/dev/null || echo "") if [ -n "$ROOT_DEVICE" ] && [ -b "$ROOT_DEVICE" ]; then info "Auto-detected root device: $ROOT_DEVICE" BLOCK_DEVICE=$(prompt "Enter block device to backup" "$ROOT_DEVICE") else if [ -n "$ROOT_DEVICE" ]; then warn "Detected device $ROOT_DEVICE is not a valid block device" fi info "Common devices: /dev/sda, /dev/nvme0n1, /dev/vda" BLOCK_DEVICE=$(prompt "Enter block device to backup" "/dev/sda") fi # Verify it's a block device if [ ! -b "$BLOCK_DEVICE" ]; then warn "Warning: $BLOCK_DEVICE does not exist or is not a block device" info "Available block devices:" lsblk -d -n -o NAME,SIZE,TYPE | grep disk | awk '{print " /dev/"$1" ("$2")"}' echo CONTINUE=$(prompt "Continue anyway? (yes/no)" "no") if [[ "$CONTINUE" != "yes" ]]; then error "Installation cancelled" exit 1 fi fi fi echo info "Backup Schedule:" echo " 1) Hourly" echo " 2) Daily (recommended)" echo " 3) Weekly" echo " 4) Custom" SCHEDULE_TYPE=$(prompt "Select schedule type [1/2/3/4]" "2") case "$SCHEDULE_TYPE" in 1) TIMER_SCHEDULE="hourly" TIMER_ONCALENDAR="hourly" ;; 2) BACKUP_HOUR=$(prompt "Enter hour for daily backup (0-23)" "2") TIMER_SCHEDULE="daily" TIMER_ONCALENDAR="*-*-* ${BACKUP_HOUR}:00:00" ;; 3) BACKUP_DOW=$(prompt "Enter day of week (Mon-Sun)" "Sun") BACKUP_HOUR=$(prompt "Enter hour (0-23)" "2") TIMER_SCHEDULE="weekly" TIMER_ONCALENDAR="${BACKUP_DOW} *-*-* ${BACKUP_HOUR}:00:00" ;; 4) TIMER_ONCALENDAR=$(prompt "Enter systemd OnCalendar format" "*-*-* 02:00:00") TIMER_SCHEDULE="custom" ;; esac echo # Retention policy info "Retention Policy:" KEEP_LAST=$(prompt "Keep last N backups" "3") KEEP_DAILY=$(prompt "Keep daily backups for N days" "7") KEEP_WEEKLY=$(prompt "Keep weekly backups for N weeks" "4") KEEP_MONTHLY=$(prompt "Keep monthly backups for N months" "6") # Encryption echo ENABLE_ENCRYPTION=$(prompt "Enable encryption? (yes/no)" "no") } # Create encryption key create_encryption_key() { if [[ "$ENABLE_ENCRYPTION" == "yes" ]]; then log "Creating encryption key..." # Create config directory for root mkdir -p /root/.config/proxmox-backup # Generate key without password (for automated backups) if proxmox-backup-client key create --kdf none 2>/dev/null; then log "Encryption key created successfully" # Create paper backup local paper_key="/root/pbs-encryption-key-$(date +%Y%m%d).txt" proxmox-backup-client key paperkey --output-format text > "$paper_key" warn "IMPORTANT: Encryption key paper backup saved to: $paper_key" warn "Print this file and store it securely. Lost keys = permanent data loss!" # Copy to config dir for service cp /root/.config/proxmox-backup/encryption-key.json "$CONFIG_DIR/" else error "Failed to create encryption key" return 1 fi fi } # Test connection to PBS test_connection() { log "Testing connection to PBS server..." export PBS_REPOSITORY="$PBS_REPOSITORY" export PBS_PASSWORD="$PBS_PASSWORD" # Step 1: Verify server is reachable info "Step 1/3: Checking if server is reachable..." if ! timeout 5 bash -c "curl -sk --max-time 5 https://${PBS_SERVER}:${PBS_PORT} >/dev/null 2>&1"; then echo error "Server is not reachable at https://${PBS_SERVER}:${PBS_PORT}" error "Possible issues:" error " - PBS server is down or unreachable" error " - Wrong IP/hostname" error " - Firewall blocking port ${PBS_PORT}" error " - Network connectivity issues" echo info "Troubleshooting:" echo " 1. Verify server is reachable: ping ${PBS_SERVER}" echo " 2. Test HTTPS connection: curl -k https://${PBS_SERVER}:${PBS_PORT}" echo " 3. Check if PBS web interface is accessible in browser" return 1 fi log "Server is reachable" # Step 2: Test authentication and handle SSL fingerprint info "Step 2/3: Testing authentication..." # Use the 'login' command which tests authentication and stores a ticket # Automatically accept SSL fingerprint by piping 'y' local test_output if test_output=$(echo "y" | timeout 15 proxmox-backup-client login 2>&1); then # Check if fingerprint was accepted if echo "$test_output" | grep -q "fingerprint:"; then local fingerprint=$(echo "$test_output" | grep "fingerprint:" | head -1 | awk '{print $2}') info "SSL fingerprint accepted: ${fingerprint}" fi log "Authentication successful" else local exit_code=$? echo if [ $exit_code -eq 124 ]; then error "Authentication test timed out" error "This might indicate a network issue or server problem" else error "Authentication failed" # Show the actual error from PBS client if echo "$test_output" | grep -q "error\|Error"; then echo echo "$test_output" | grep -i "error" | head -5 echo fi fi error "Possible issues:" error " - Invalid credentials (username/password/token)" error " - Datastore '${PBS_DATASTORE}' does not exist" error " - User lacks permissions for the datastore" error " - API token is not properly formatted" error " - SSL certificate issues" echo info "Troubleshooting:" echo " 1. Verify credentials in PBS web interface" echo " 2. Check datastore name: ${PBS_DATASTORE}" echo " 3. Verify user has backup permissions" echo " 4. For API tokens, format is: username@realm!tokenname" echo " 5. Try the test-connection.sh script for detailed diagnostics" echo info "Debug info:" echo " Repository: ${PBS_REPOSITORY}" return 1 fi # Step 3: Verify datastore access by listing backup groups info "Step 3/3: Verifying datastore access..." if timeout 15 proxmox-backup-client list 2>/dev/null; then log "Datastore access verified" log "Connection test successful!" return 0 else warn "Could not list backup groups (this is normal if no backups exist yet)" log "Connection test successful!" return 0 fi } # Create systemd service create_systemd_service() { log "Creating systemd service and timer..." # Create config directory mkdir -p "$CONFIG_DIR" # Strip any trailing newlines from password (defensive fix) PBS_PASSWORD_CLEAN=$(echo -n "$PBS_PASSWORD" | tr -d '\n\r') # Save configuration cat > "$CONFIG_DIR/config" < "$CONFIG_DIR/backup.sh" <<'EOFSCRIPT' #!/bin/bash set -e # Check if forcing full backup (for manual runs) FORCE_FULL="${1:-no}" # Load configuration source /etc/proxmox-backup-client/config # Export for PBS client export PBS_REPOSITORY export PBS_PASSWORD HOSTNAME=$(hostname) BACKUP_SUCCESS=true log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" } # Function to create file-level backup backup_files() { log "Starting file-level backup (.pxar)..." BACKUP_CMD="proxmox-backup-client backup" # Add each path as separate archive for path in $BACKUP_PATHS; do # Sanitize path for archive name archive_name=$(echo "$path" | sed 's/^\///' | sed 's/\//-/g') [ -z "$archive_name" ] && archive_name="root" BACKUP_CMD="$BACKUP_CMD ${archive_name}.pxar:${path}" # Add --include-dev for mount points if mountpoint -q "$path" 2>/dev/null; then BACKUP_CMD="$BACKUP_CMD --include-dev ${path}" fi done # Add exclusions for pattern in $EXCLUDE_PATTERNS; do BACKUP_CMD="$BACKUP_CMD --exclude=${pattern}" done # Add other options BACKUP_CMD="$BACKUP_CMD --skip-lost-and-found --change-detection-mode=metadata" # Execute backup if eval $BACKUP_CMD; then log "File-level backup completed successfully" return 0 else log "File-level backup FAILED" >&2 return 1 fi } # Function to create block device backup backup_block_device() { log "Starting block device backup (.img)..." if [ -z "$BLOCK_DEVICE" ]; then log "ERROR: BLOCK_DEVICE not configured" >&2 return 1 fi if [ ! -b "$BLOCK_DEVICE" ]; then log "ERROR: $BLOCK_DEVICE is not a block device" >&2 return 1 fi # Get device size DEVICE_SIZE=$(blockdev --getsize64 "$BLOCK_DEVICE" 2>/dev/null || echo "0") DEVICE_SIZE_GB=$((DEVICE_SIZE / 1024 / 1024 / 1024)) log "Backing up device: $BLOCK_DEVICE (${DEVICE_SIZE_GB}GB)" log "This may take a while..." # Sanitize device name for archive DEVICE_NAME=$(basename "$BLOCK_DEVICE") # Create block device backup if proxmox-backup-client backup "${DEVICE_NAME}.img:${BLOCK_DEVICE}"; then log "Block device backup completed successfully" return 0 else log "Block device backup FAILED" >&2 return 1 fi } # Main backup execution log "Starting backup for ${HOSTNAME}" log "Backup type: ${BACKUP_TYPE}" case "$BACKUP_TYPE" in files) backup_files || BACKUP_SUCCESS=false ;; block) backup_block_device || BACKUP_SUCCESS=false ;; both) # Do file backup first (faster, more frequent) backup_files || BACKUP_SUCCESS=false # Do block device backup on Sunday OR if manually forced if [ "$FORCE_FULL" = "yes" ]; then log "Manual full backup - including block device" backup_block_device || BACKUP_SUCCESS=false elif [ "$(date +%u)" -eq 7 ]; then log "Weekly block device backup day (Sunday)" backup_block_device || BACKUP_SUCCESS=false else log "Skipping block device backup (runs weekly on Sunday, or use 'Run backup now' for immediate full backup)" fi ;; *) log "ERROR: Invalid BACKUP_TYPE: ${BACKUP_TYPE}" >&2 exit 1 ;; esac # Prune old backups if [ "$BACKUP_SUCCESS" = true ]; then log "Pruning old backups..." proxmox-backup-client prune "host/${HOSTNAME}" \ --keep-last $KEEP_LAST \ --keep-daily $KEEP_DAILY \ --keep-weekly $KEEP_WEEKLY \ --keep-monthly $KEEP_MONTHLY log "Backup and prune completed successfully" else log "Backup FAILED" >&2 exit 1 fi EOFSCRIPT chmod 700 "$CONFIG_DIR/backup.sh" # Create systemd service file (for scheduled backups) cat > /etc/systemd/system/pbs-backup.service < /etc/systemd/system/pbs-backup-manual.service < /etc/systemd/system/pbs-backup.timer < "$(get_target_config_path "$target_name")" < "$CONFIG_DIR/backup-${target_name}.sh" </dev/null; then BACKUP_CMD="\$BACKUP_CMD --include-dev \${path}" fi done # Add exclusions for pattern in \$EXCLUDE_PATTERNS; do BACKUP_CMD="\$BACKUP_CMD --exclude=\${pattern}" done # Add other options BACKUP_CMD="\$BACKUP_CMD --skip-lost-and-found --change-detection-mode=metadata" # Execute backup if eval \$BACKUP_CMD; then log "File-level backup completed successfully" return 0 else log "File-level backup FAILED" >&2 return 1 fi } # Function to create block device backup backup_block_device() { log "Starting block device backup (.img) for target: $target_name" if [ -z "\$BLOCK_DEVICE" ]; then log "ERROR: BLOCK_DEVICE not configured" >&2 return 1 fi if [ ! -b "\$BLOCK_DEVICE" ]; then log "ERROR: \$BLOCK_DEVICE is not a block device" >&2 return 1 fi # Get device size DEVICE_SIZE=\$(blockdev --getsize64 "\$BLOCK_DEVICE" 2>/dev/null || echo "0") DEVICE_SIZE_GB=\$((DEVICE_SIZE / 1024 / 1024 / 1024)) log "Backing up device: \$BLOCK_DEVICE (\${DEVICE_SIZE_GB}GB)" log "This may take a while..." # Sanitize device name for archive DEVICE_NAME=\$(basename "\$BLOCK_DEVICE") # Create block device backup if proxmox-backup-client backup "\${DEVICE_NAME}.img:\${BLOCK_DEVICE}"; then log "Block device backup completed successfully" return 0 else log "Block device backup FAILED" >&2 return 1 fi } # Main backup execution log "Starting backup for \${HOSTNAME} - Target: $target_name" log "Backup type: \${BACKUP_TYPE}" case "\$BACKUP_TYPE" in files) backup_files || BACKUP_SUCCESS=false ;; block) backup_block_device || BACKUP_SUCCESS=false ;; both) # Do file backup first (faster, more frequent) backup_files || BACKUP_SUCCESS=false # Do block device backup on Sunday OR if manually forced if [ "\$FORCE_FULL" = "yes" ]; then log "Manual full backup - including block device" backup_block_device || BACKUP_SUCCESS=false elif [ "\$(date +%u)" -eq 7 ]; then log "Weekly block device backup day (Sunday)" backup_block_device || BACKUP_SUCCESS=false else log "Skipping block device backup (runs weekly on Sunday, or use 'Run backup now' for immediate full backup)" fi ;; *) log "ERROR: Invalid BACKUP_TYPE: \${BACKUP_TYPE}" >&2 exit 1 ;; esac # Prune old backups if [ "\$BACKUP_SUCCESS" = true ]; then log "Pruning old backups..." proxmox-backup-client prune "host/\${HOSTNAME}" \\ --keep-last \$KEEP_LAST \\ --keep-daily \$KEEP_DAILY \\ --keep-weekly \$KEEP_WEEKLY \\ --keep-monthly \$KEEP_MONTHLY log "Backup and prune completed successfully" else log "Backup FAILED" >&2 exit 1 fi EOFSCRIPT chmod 700 "$CONFIG_DIR/backup-${target_name}.sh" # Create systemd service file (for scheduled backups) cat > "/etc/systemd/system/pbs-backup-${target_name}.service" < "/etc/systemd/system/pbs-backup-${target_name}-manual.service" < "/etc/systemd/system/pbs-backup-${target_name}.timer" </dev/null ) & MONITOR_PID=$! # Follow logs in foreground from the start (will be killed by monitor when backup completes) journalctl -u pbs-backup-manual.service -f --since "2 seconds ago" || true # Wait for monitor to finish wait $MONITOR_PID 2>/dev/null # Clear any leftover output echo echo "════════════════════════════════════════════════════════════" # Check final status if systemctl status pbs-backup-manual.service | grep -q "Active: failed"; then echo error "Backup failed!" else echo log "Backup completed successfully!" fi fi } # Show summary and next steps show_summary() { echo echo "======================================" echo " Installation Complete!" echo "======================================" echo info "Configuration Summary:" echo " PBS Server: ${PBS_SERVER}:${PBS_PORT}" echo " Datastore: ${PBS_DATASTORE}" echo " Repository: ${PBS_REPOSITORY}" echo " Backup Type: ${BACKUP_TYPE}" if [[ "$BACKUP_TYPE" == "files" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Backup Paths: ${BACKUP_PATHS}" fi if [[ "$BACKUP_TYPE" == "block" ]] || [[ "$BACKUP_TYPE" == "both" ]]; then echo " Block Device: ${BLOCK_DEVICE}" fi if [[ "$BACKUP_TYPE" == "both" ]]; then echo " Schedule: Files ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR}), Block device weekly (Sunday)" else echo " Schedule: ${TIMER_SCHEDULE} (${TIMER_ONCALENDAR})" fi echo info "Useful Commands:" echo " Check timer status: sudo systemctl status pbs-backup.timer" echo " Check service logs: sudo journalctl -u pbs-backup.service" echo " Run backup now: sudo systemctl start pbs-backup.service" echo " List backups: sudo -E proxmox-backup-client snapshot list" echo " Disable backups: sudo systemctl disable pbs-backup.timer" echo info "Configuration Files:" echo " Config: $CONFIG_DIR/config" echo " Backup script: $CONFIG_DIR/backup.sh" echo " Service: /etc/systemd/system/pbs-backup.service" echo " Timer: /etc/systemd/system/pbs-backup.timer" if [[ "$ENABLE_ENCRYPTION" == "yes" ]]; then echo warn "IMPORTANT: Backup your encryption key!" echo " Key location: /root/.config/proxmox-backup/encryption-key.json" echo " Paper backup: /root/pbs-encryption-key-*.txt" fi echo log "Setup completed successfully!" } # Main script execution main() { echo echo "╔════════════════════════════════════════╗" echo "║ Proxmox Backup Client Installer ║" echo "║ Version: ${SCRIPT_VERSION} ║" echo "╚════════════════════════════════════════╝" echo # Create log file touch "$LOG_FILE" # Check root check_root # Detect distribution detect_distro # Migrate legacy configuration if needed migrate_legacy_config # Check if PBS client is already installed if command -v proxmox-backup-client &> /dev/null; then warn "Proxmox Backup Client is already installed" # Check if any targets exist if list_targets >/dev/null 2>&1; then show_targets_list echo echo "════════════════════════════════════════" echo " Multi-Target Backup Management" echo "════════════════════════════════════════" echo echo "What would you like to do?" echo " 1) List all backup targets" echo " 2) Add new backup target" echo " 3) Edit existing target" echo " 4) Delete target" echo " 5) Run backup now (select target)" echo " 6) View target details" echo " 7) Reinstall PBS client" echo " 8) Exit" ACTION=$(prompt "Select option [1-8]" "1") case "$ACTION" in 1) info "Reconfiguring connection only..." reconfigure_connection # Skip most of the setup, just restart services systemctl daemon-reload systemctl restart pbs-backup.timer log "Configuration updated and services restarted!" echo info "Your backup schedule and settings remain unchanged." exit 0 ;; 2) info "Proceeding with full reconfiguration..." # Continue to interactive_config below ;; 3) info "Reinstalling PBS client..." install_pbs_client # Continue to interactive_config below ;; 4) info "Running FULL backup now (files + block device)..." echo # Show real-time progress echo "╔════════════════════════════════════════════════════════════╗" echo "║ Backup Progress (Live) ║" echo "║ Press Ctrl+C to exit (backup continues in background) ║" echo "╚════════════════════════════════════════════════════════════╝" echo # Start the manual backup (forces full backup) systemctl start pbs-backup-manual.service & # Wait a brief moment for service to register sleep 0.5 # Monitor backup in background and kill journalctl when done ( while systemctl is-active --quiet pbs-backup-manual.service; do sleep 2 done # Service finished, kill the journal follow pkill -P $$ journalctl 2>/dev/null ) & MONITOR_PID=$! # Follow logs in foreground from the start (will be killed by monitor when backup completes) journalctl -u pbs-backup-manual.service -f --since "2 seconds ago" || true # Wait for monitor to finish wait $MONITOR_PID 2>/dev/null # Clear any leftover output echo echo "════════════════════════════════════════════════════════════" # Check final status if systemctl status pbs-backup-manual.service | grep -q "Active: failed"; then echo error "Backup failed!" echo info "Check full logs with:" echo " sudo journalctl -u pbs-backup.service -n 50" else echo log "Backup completed successfully!" echo info "View backup snapshots with:" echo " export PBS_REPOSITORY='$PBS_REPOSITORY'" echo " export PBS_PASSWORD='***'" echo " sudo -E proxmox-backup-client snapshot list" fi exit 0 ;; 5) info "Modifying backup schedule and type..." reconfigure_backup_settings exit 0 ;; 6) info "Exiting without changes" exit 0 ;; *) error "Invalid option" exit 1 ;; esac else # PBS client installed but no config info "No existing configuration found" echo echo "What would you like to do?" echo " 1) Configure PBS client" echo " 2) Reinstall and configure" echo " 3) Exit" ACTION=$(prompt "Select option [1/2/3]" "1") case "$ACTION" in 1) info "Proceeding with configuration..." # Continue to interactive_config below ;; 2) info "Reinstalling PBS client..." install_pbs_client # Continue to interactive_config below ;; 3) info "Exiting without changes" exit 0 ;; *) error "Invalid option" exit 1 ;; esac fi else # PBS client not installed install_pbs_client fi # Interactive configuration interactive_config # Create encryption key create_encryption_key # Test connection if ! test_connection; then error "Cannot proceed without successful connection to PBS" exit 1 fi # Create systemd service create_systemd_service # Offer to run backup now run_backup_now # Show summary show_summary } # Run main function main "$@"