mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-30 11:33:51 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			466 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/bin/bash
 | |
| set -eo pipefail  # Exit on error, pipe failures (removed -u as we handle unset vars explicitly)
 | |
| 
 | |
| # Trap to catch errors only (not normal exits)
 | |
| trap 'echo "[ERROR] Script failed at line $LINENO with exit code $?"' ERR
 | |
| 
 | |
| SCRIPT_VERSION="2.0.0"
 | |
| echo "[DEBUG] Script Version: $SCRIPT_VERSION ($(date +%Y-%m-%d\ %H:%M:%S))"
 | |
| 
 | |
| # =============================================================================
 | |
| # PatchMon Proxmox LXC Auto-Enrollment Script
 | |
| # =============================================================================
 | |
| # This script discovers LXC containers on a Proxmox host and automatically
 | |
| # enrolls them into PatchMon for patch management.
 | |
| #
 | |
| # Usage:
 | |
| #   1. Set environment variables or edit configuration below
 | |
| #   2. Run: bash proxmox_auto_enroll.sh
 | |
| #
 | |
| # Requirements:
 | |
| #   - Must run on Proxmox host (requires 'pct' command)
 | |
| #   - Auto-enrollment token from PatchMon
 | |
| #   - Network access to PatchMon server
 | |
| # =============================================================================
 | |
| 
 | |
| # ===== CONFIGURATION =====
 | |
| PATCHMON_URL="${PATCHMON_URL:-https://patchmon.example.com}"
 | |
| AUTO_ENROLLMENT_KEY="${AUTO_ENROLLMENT_KEY:-}"
 | |
| AUTO_ENROLLMENT_SECRET="${AUTO_ENROLLMENT_SECRET:-}"
 | |
| CURL_FLAGS="${CURL_FLAGS:--s}"
 | |
| DRY_RUN="${DRY_RUN:-false}"
 | |
| HOST_PREFIX="${HOST_PREFIX:-}"
 | |
| SKIP_STOPPED="${SKIP_STOPPED:-true}"
 | |
| PARALLEL_INSTALL="${PARALLEL_INSTALL:-false}"
 | |
| MAX_PARALLEL="${MAX_PARALLEL:-5}"
 | |
| FORCE_INSTALL="${FORCE_INSTALL:-false}"
 | |
| 
 | |
| # ===== COLOR OUTPUT =====
 | |
| RED='\033[0;31m'
 | |
| GREEN='\033[0;32m'
 | |
| YELLOW='\033[1;33m'
 | |
| BLUE='\033[0;34m'
 | |
| NC='\033[0m' # No Color
 | |
| 
 | |
| # ===== LOGGING FUNCTIONS =====
 | |
| info() { echo -e "${GREEN}[INFO]${NC} $1"; return 0; }
 | |
| warn() { echo -e "${YELLOW}[WARN]${NC} $1"; return 0; }
 | |
| error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
 | |
| success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; return 0; }
 | |
| debug() { [[ "${DEBUG:-false}" == "true" ]] && echo -e "${BLUE}[DEBUG]${NC} $1" || true; return 0; }
 | |
| 
 | |
| # ===== BANNER =====
 | |
| cat << "EOF"
 | |
| ╔═══════════════════════════════════════════════════════════════╗
 | |
| ║                                                               ║
 | |
| ║   ____       _       _     __  __                            ║
 | |
| ║  |  _ \ __ _| |_ ___| |__ |  \/  | ___  _ __                ║
 | |
| ║  | |_) / _` | __/ __| '_ \| |\/| |/ _ \| '_ \               ║
 | |
| ║  |  __/ (_| | || (__| | | | |  | | (_) | | | |              ║
 | |
| ║  |_|   \__,_|\__\___|_| |_|_|  |_|\___/|_| |_|              ║
 | |
| ║                                                               ║
 | |
| ║         Proxmox LXC Auto-Enrollment Script                   ║
 | |
| ║                                                               ║
 | |
| ╚═══════════════════════════════════════════════════════════════╝
 | |
| EOF
 | |
| echo ""
 | |
| 
 | |
| # ===== VALIDATION =====
 | |
| info "Validating configuration..."
 | |
| 
 | |
| if [[ -z "$AUTO_ENROLLMENT_KEY" ]] || [[ -z "$AUTO_ENROLLMENT_SECRET" ]]; then
 | |
|     error "AUTO_ENROLLMENT_KEY and AUTO_ENROLLMENT_SECRET must be set"
 | |
| fi
 | |
| 
 | |
| if [[ -z "$PATCHMON_URL" ]]; then
 | |
|     error "PATCHMON_URL must be set"
 | |
| fi
 | |
| 
 | |
| # Check if running on Proxmox
 | |
| if ! command -v pct &> /dev/null; then
 | |
|     error "This script must run on a Proxmox host (pct command not found)"
 | |
| fi
 | |
| 
 | |
| # Check for required commands
 | |
| for cmd in curl jq; do
 | |
|     if ! command -v $cmd &> /dev/null; then
 | |
|         error "Required command '$cmd' not found. Please install it first."
 | |
|     fi
 | |
| done
 | |
| 
 | |
| info "Configuration validated successfully"
 | |
| info "PatchMon Server: $PATCHMON_URL"
 | |
| info "Dry Run Mode: $DRY_RUN"
 | |
| info "Skip Stopped Containers: $SKIP_STOPPED"
 | |
| echo ""
 | |
| 
 | |
| # ===== DISCOVER LXC CONTAINERS =====
 | |
| info "Discovering LXC containers..."
 | |
| lxc_list=$(pct list | tail -n +2)  # Skip header
 | |
| 
 | |
| if [[ -z "$lxc_list" ]]; then
 | |
|     warn "No LXC containers found on this Proxmox host"
 | |
|     exit 0
 | |
| fi
 | |
| 
 | |
| # Count containers
 | |
| total_containers=$(echo "$lxc_list" | wc -l)
 | |
| info "Found $total_containers LXC container(s)"
 | |
| echo ""
 | |
| 
 | |
| info "Initializing statistics..."
 | |
| # ===== STATISTICS =====
 | |
| enrolled_count=0
 | |
| skipped_count=0
 | |
| failed_count=0
 | |
| 
 | |
| # Track containers with dpkg errors for later recovery
 | |
| declare -A dpkg_error_containers
 | |
| 
 | |
| # Track all failed containers for summary
 | |
| declare -A failed_containers
 | |
| info "Statistics initialized"
 | |
| 
 | |
| # ===== PROCESS CONTAINERS =====
 | |
| info "Starting container processing loop..."
 | |
| while IFS= read -r line; do
 | |
|     info "[DEBUG] Read line from lxc_list"
 | |
|     vmid=$(echo "$line" | awk '{print $1}')
 | |
|     status=$(echo "$line" | awk '{print $2}')
 | |
|     name=$(echo "$line" | awk '{print $3}')
 | |
| 
 | |
|     info "Processing LXC $vmid: $name (status: $status)"
 | |
| 
 | |
|     # Skip stopped containers if configured
 | |
|     if [[ "$status" != "running" ]] && [[ "$SKIP_STOPPED" == "true" ]]; then
 | |
|         warn "  Skipping $name - container not running"
 | |
|         ((skipped_count++)) || true
 | |
|         echo ""
 | |
|         continue
 | |
|     fi
 | |
| 
 | |
|     # Check if container is stopped
 | |
|     if [[ "$status" != "running" ]]; then
 | |
|         warn "  Container $name is stopped - cannot gather info or install agent"
 | |
|         ((skipped_count++)) || true
 | |
|         echo ""
 | |
|         continue
 | |
|     fi
 | |
| 
 | |
|     # Get container details
 | |
|     debug "  Gathering container information..."
 | |
|     hostname=$(timeout 5 pct exec "$vmid" -- hostname 2>/dev/null </dev/null || echo "$name")
 | |
|     ip_address=$(timeout 5 pct exec "$vmid" -- hostname -I 2>/dev/null </dev/null | awk '{print $1}' || echo "unknown")
 | |
|     os_info=$(timeout 5 pct exec "$vmid" -- cat /etc/os-release 2>/dev/null </dev/null | grep "^PRETTY_NAME=" | cut -d'"' -f2 || echo "unknown")
 | |
|     
 | |
|     # Detect container architecture
 | |
|     debug "  Detecting container architecture..."
 | |
|     arch_raw=$(timeout 5 pct exec "$vmid" -- uname -m 2>/dev/null </dev/null || echo "unknown")
 | |
|     
 | |
|     # Map architecture to supported values
 | |
|     case "$arch_raw" in
 | |
|         "x86_64")
 | |
|             architecture="amd64"
 | |
|             ;;
 | |
|         "i386"|"i686")
 | |
|             architecture="386"
 | |
|             ;;
 | |
|         "aarch64"|"arm64")
 | |
|             architecture="arm64"
 | |
|             ;;
 | |
|         "armv7l"|"armv6l"|"arm")
 | |
|             architecture="arm"
 | |
|             ;;
 | |
|         *)
 | |
|             warn "  ⚠ Unknown architecture '$arch_raw', defaulting to amd64"
 | |
|             architecture="amd64"
 | |
|             ;;
 | |
|     esac
 | |
|     
 | |
|     debug "  Detected architecture: $arch_raw -> $architecture"
 | |
|     
 | |
|     # Get machine ID from container
 | |
|     machine_id=$(timeout 5 pct exec "$vmid" -- bash -c "cat /etc/machine-id 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null || echo 'proxmox-lxc-$vmid-'$(cat /proc/sys/kernel/random/uuid)" </dev/null 2>/dev/null || echo "proxmox-lxc-$vmid-unknown")
 | |
| 
 | |
|     friendly_name="${HOST_PREFIX}${hostname}"
 | |
| 
 | |
|     info "  Hostname: $hostname"
 | |
|     info "  IP Address: $ip_address"
 | |
|     info "  OS: $os_info"
 | |
|     info "  Architecture: $architecture ($arch_raw)"
 | |
|     info "  Machine ID: ${machine_id:0:16}..."
 | |
| 
 | |
|     if [[ "$DRY_RUN" == "true" ]]; then
 | |
|         info "  [DRY RUN] Would enroll: $friendly_name"
 | |
|         ((enrolled_count++)) || true
 | |
|         echo ""
 | |
|         continue
 | |
|     fi
 | |
| 
 | |
|     # Call PatchMon auto-enrollment API
 | |
|     info "  Enrolling $friendly_name in PatchMon..."
 | |
|     
 | |
|     response=$(curl $CURL_FLAGS -X POST \
 | |
|         -H "X-Auto-Enrollment-Key: $AUTO_ENROLLMENT_KEY" \
 | |
|         -H "X-Auto-Enrollment-Secret: $AUTO_ENROLLMENT_SECRET" \
 | |
|         -H "Content-Type: application/json" \
 | |
|         -d "{
 | |
|             \"friendly_name\": \"$friendly_name\",
 | |
|             \"machine_id\": \"$machine_id\",
 | |
|             \"metadata\": {
 | |
|                 \"vmid\": \"$vmid\",
 | |
|                 \"proxmox_node\": \"$(hostname)\",
 | |
|                 \"ip_address\": \"$ip_address\",
 | |
|                 \"os_info\": \"$os_info\"
 | |
|             }
 | |
|         }" \
 | |
|         "$PATCHMON_URL/api/v1/auto-enrollment/enroll" \
 | |
|         -w "\n%{http_code}" 2>&1)
 | |
| 
 | |
|     http_code=$(echo "$response" | tail -n 1)
 | |
|     body=$(echo "$response" | sed '$d')
 | |
| 
 | |
|     if [[ "$http_code" == "201" ]]; then
 | |
|         api_id=$(echo "$body" | jq -r '.host.api_id' 2>/dev/null || echo "")
 | |
|         api_key=$(echo "$body" | jq -r '.host.api_key' 2>/dev/null || echo "")
 | |
| 
 | |
|         if [[ -z "$api_id" ]] || [[ -z "$api_key" ]]; then
 | |
|             error "  Failed to parse API credentials from response"
 | |
|         fi
 | |
| 
 | |
|         info "  ✓ Host enrolled successfully: $api_id"
 | |
| 
 | |
|         # Ensure curl is installed in the container
 | |
|         info "  Checking for curl in container..."
 | |
|         curl_check=$(timeout 10 pct exec "$vmid" -- bash -c "command -v curl >/dev/null 2>&1 && echo 'installed' || echo 'missing'" 2>/dev/null </dev/null || echo "error")
 | |
|         
 | |
|         if [[ "$curl_check" == "missing" ]]; then
 | |
|             info "  Installing curl in container..."
 | |
|             
 | |
|             # Detect package manager and install curl
 | |
|             curl_install_output=$(timeout 60 pct exec "$vmid" -- bash -c "
 | |
|                 if command -v apt-get >/dev/null 2>&1; then
 | |
|                     export DEBIAN_FRONTEND=noninteractive
 | |
|                     apt-get update -qq && apt-get install -y -qq curl
 | |
|                 elif command -v yum >/dev/null 2>&1; then
 | |
|                     yum install -y -q curl
 | |
|                 elif command -v dnf >/dev/null 2>&1; then
 | |
|                     dnf install -y -q curl
 | |
|                 elif command -v apk >/dev/null 2>&1; then
 | |
|                     apk add --no-cache curl
 | |
|                 else
 | |
|                     echo 'ERROR: No supported package manager found'
 | |
|                     exit 1
 | |
|                 fi
 | |
|             " 2>&1 </dev/null) || true
 | |
|             
 | |
|             if [[ "$curl_install_output" == *"ERROR: No supported package manager"* ]]; then
 | |
|                 warn "  ✗ Could not install curl - no supported package manager found"
 | |
|                 failed_containers["$vmid"]="$friendly_name|No package manager for curl|$curl_install_output"
 | |
|                 ((failed_count++)) || true
 | |
|                 echo ""
 | |
|                 sleep 1
 | |
|                 continue
 | |
|             else
 | |
|                 info "  ✓ curl installed successfully"
 | |
|             fi
 | |
|         else
 | |
|             info "  ✓ curl already installed"
 | |
|         fi
 | |
| 
 | |
|         # Install PatchMon agent in container
 | |
|         info "  Installing PatchMon agent..."
 | |
|         
 | |
|         # Build install URL with force flag and architecture if enabled
 | |
|         install_url="$PATCHMON_URL/api/v1/hosts/install?arch=$architecture"
 | |
|         if [[ "$FORCE_INSTALL" == "true" ]]; then
 | |
|             install_url="$install_url&force=true"
 | |
|             info "  Using force mode - will bypass broken packages"
 | |
|         fi
 | |
|         info "  Using architecture: $architecture"
 | |
|         
 | |
|         # Reset exit code for this container
 | |
|         install_exit_code=0
 | |
|         
 | |
|         # Download and execute in separate steps to avoid stdin issues with piping
 | |
|         install_output=$(timeout 180 pct exec "$vmid" -- bash -c "
 | |
|             cd /tmp
 | |
|             curl $CURL_FLAGS \
 | |
|                 -H \"X-API-ID: $api_id\" \
 | |
|                 -H \"X-API-KEY: $api_key\" \
 | |
|                 -o patchmon-install.sh \
 | |
|                 '$install_url' && \
 | |
|             bash patchmon-install.sh && \
 | |
|             rm -f patchmon-install.sh
 | |
|         " 2>&1 </dev/null) || install_exit_code=$?
 | |
| 
 | |
|         # Check both exit code AND success message in output for reliability
 | |
|         if [[ $install_exit_code -eq 0 ]] || [[ "$install_output" == *"PatchMon Agent installation completed successfully"* ]]; then
 | |
|             info "  ✓ Agent installed successfully in $friendly_name"
 | |
|             ((enrolled_count++)) || true
 | |
|         elif [[ $install_exit_code -eq 124 ]]; then
 | |
|             warn "  ⏱ Agent installation timed out (>180s) in $friendly_name"
 | |
|             info "  Install output: $install_output"
 | |
|             # Store failure details
 | |
|             failed_containers["$vmid"]="$friendly_name|Timeout (>180s)|$install_output"
 | |
|             ((failed_count++)) || true
 | |
|         else
 | |
|             # Check if it's a dpkg error
 | |
|             if [[ "$install_output" == *"dpkg was interrupted"* ]] || [[ "$install_output" == *"dpkg --configure -a"* ]]; then
 | |
|                 warn "  ⚠ Failed due to dpkg error in $friendly_name (can be fixed)"
 | |
|                 dpkg_error_containers["$vmid"]="$friendly_name:$api_id:$api_key"
 | |
|                 # Store failure details
 | |
|                 failed_containers["$vmid"]="$friendly_name|dpkg error|$install_output"
 | |
|             else
 | |
|                 warn "  ✗ Failed to install agent in $friendly_name (exit: $install_exit_code)"
 | |
|                 # Store failure details
 | |
|                 failed_containers["$vmid"]="$friendly_name|Exit code $install_exit_code|$install_output"
 | |
|             fi
 | |
|             info "  Install output: $install_output"
 | |
|             ((failed_count++)) || true
 | |
|         fi
 | |
| 
 | |
|     elif [[ "$http_code" == "409" ]]; then
 | |
|         warn "  ⊘ Host $friendly_name already enrolled - skipping"
 | |
|         ((skipped_count++)) || true
 | |
|     elif [[ "$http_code" == "429" ]]; then
 | |
|         error "  ✗ Rate limit exceeded - maximum hosts per day reached"
 | |
|         failed_containers["$vmid"]="$friendly_name|Rate limit exceeded|$body"
 | |
|         ((failed_count++)) || true
 | |
|     else
 | |
|         error "  ✗ Failed to enroll $friendly_name - HTTP $http_code"
 | |
|         debug "  Response: $body"
 | |
|         failed_containers["$vmid"]="$friendly_name|HTTP $http_code enrollment failed|$body"
 | |
|         ((failed_count++)) || true
 | |
|     fi
 | |
| 
 | |
|     echo ""
 | |
|     sleep 1  # Rate limiting between containers
 | |
| 
 | |
| done <<< "$lxc_list"
 | |
| 
 | |
| # ===== SUMMARY =====
 | |
| echo ""
 | |
| echo "╔═══════════════════════════════════════════════════════════════╗"
 | |
| echo "║                     ENROLLMENT SUMMARY                        ║"
 | |
| echo "╚═══════════════════════════════════════════════════════════════╝"
 | |
| echo ""
 | |
| info "Total Containers Found: $total_containers"
 | |
| info "Successfully Enrolled:  $enrolled_count"
 | |
| info "Skipped:                $skipped_count"
 | |
| info "Failed:                 $failed_count"
 | |
| echo ""
 | |
| 
 | |
| # ===== FAILURE DETAILS =====
 | |
| if [[ ${#failed_containers[@]} -gt 0 ]]; then
 | |
|     echo "╔═══════════════════════════════════════════════════════════════╗"
 | |
|     echo "║                     FAILURE DETAILS                           ║"
 | |
|     echo "╚═══════════════════════════════════════════════════════════════╝"
 | |
|     echo ""
 | |
|     
 | |
|     for vmid in "${!failed_containers[@]}"; do
 | |
|         IFS='|' read -r name reason output <<< "${failed_containers[$vmid]}"
 | |
|         
 | |
|         warn "Container $vmid: $name"
 | |
|         info "  Reason: $reason"
 | |
|         info "  Last 5 lines of output:"
 | |
|         
 | |
|         # Get last 5 lines of output
 | |
|         last_5_lines=$(echo "$output" | tail -n 5)
 | |
|         
 | |
|         # Display each line with proper indentation
 | |
|         while IFS= read -r line; do
 | |
|             echo "    $line"
 | |
|         done <<< "$last_5_lines"
 | |
|         
 | |
|         echo ""
 | |
|     done
 | |
| fi
 | |
| 
 | |
| if [[ "$DRY_RUN" == "true" ]]; then
 | |
|     warn "This was a DRY RUN - no actual changes were made"
 | |
|     warn "Set DRY_RUN=false to perform actual enrollment"
 | |
| fi
 | |
| 
 | |
| # ===== DPKG ERROR RECOVERY =====
 | |
| if [[ ${#dpkg_error_containers[@]} -gt 0 ]]; then
 | |
|     echo ""
 | |
|     echo "╔═══════════════════════════════════════════════════════════════╗"
 | |
|     echo "║              DPKG ERROR RECOVERY AVAILABLE                    ║"
 | |
|     echo "╚═══════════════════════════════════════════════════════════════╝"
 | |
|     echo ""
 | |
|     warn "Detected ${#dpkg_error_containers[@]} container(s) with dpkg errors:"
 | |
|     for vmid in "${!dpkg_error_containers[@]}"; do
 | |
|         IFS=':' read -r name api_id api_key <<< "${dpkg_error_containers[$vmid]}"
 | |
|         info "  • Container $vmid: $name"
 | |
|     done
 | |
|     echo ""
 | |
|     
 | |
|     # Ask user if they want to fix dpkg errors
 | |
|     read -p "Would you like to fix dpkg errors and retry installation? (y/N): " -n 1 -r
 | |
|     echo ""
 | |
|     
 | |
|     if [[ $REPLY =~ ^[Yy]$ ]]; then
 | |
|         echo ""
 | |
|         info "Starting dpkg recovery process..."
 | |
|         echo ""
 | |
|         
 | |
|         recovered_count=0
 | |
|         
 | |
|         for vmid in "${!dpkg_error_containers[@]}"; do
 | |
|             IFS=':' read -r name api_id api_key <<< "${dpkg_error_containers[$vmid]}"
 | |
|             
 | |
|             info "Fixing dpkg in container $vmid ($name)..."
 | |
|             
 | |
|             # Run dpkg --configure -a
 | |
|             dpkg_output=$(timeout 60 pct exec "$vmid" -- dpkg --configure -a 2>&1 </dev/null || true)
 | |
|             
 | |
|             if [[ $? -eq 0 ]]; then
 | |
|                 info "  ✓ dpkg fixed successfully"
 | |
|                 
 | |
|                 # Retry agent installation
 | |
|                 info "  Retrying agent installation..."
 | |
|                 
 | |
|                 install_exit_code=0
 | |
|                 install_output=$(timeout 180 pct exec "$vmid" -- bash -c "
 | |
|                     cd /tmp
 | |
|                     curl $CURL_FLAGS \
 | |
|                         -H \"X-API-ID: $api_id\" \
 | |
|                         -H \"X-API-KEY: $api_key\" \
 | |
|                         -o patchmon-install.sh \
 | |
|                         '$PATCHMON_URL/api/v1/hosts/install?arch=$architecture' && \
 | |
|                     bash patchmon-install.sh && \
 | |
|                     rm -f patchmon-install.sh
 | |
|                 " 2>&1 </dev/null) || install_exit_code=$?
 | |
|                 
 | |
|                 if [[ $install_exit_code -eq 0 ]] || [[ "$install_output" == *"PatchMon Agent installation completed successfully"* ]]; then
 | |
|                     info "  ✓ Agent installed successfully in $name"
 | |
|                     ((recovered_count++)) || true
 | |
|                     ((enrolled_count++)) || true
 | |
|                     ((failed_count--)) || true
 | |
|                 else
 | |
|                     warn "  ✗ Agent installation still failed (exit: $install_exit_code)"
 | |
|                 fi
 | |
|             else
 | |
|                 warn "  ✗ Failed to fix dpkg in $name"
 | |
|                 info "  dpkg output: $dpkg_output"
 | |
|             fi
 | |
|             
 | |
|             echo ""
 | |
|         done
 | |
|         
 | |
|         echo ""
 | |
|         info "Recovery complete: $recovered_count container(s) recovered"
 | |
|         echo ""
 | |
|     fi
 | |
| fi
 | |
| 
 | |
| if [[ $failed_count -gt 0 ]]; then
 | |
|     warn "Some containers failed to enroll. Check the logs above for details."
 | |
|     exit 1
 | |
| fi
 | |
| 
 | |
| info "Auto-enrollment complete! ✓"
 | |
| exit 0
 | |
| 
 |