API for auto-enrollment

Api
This commit is contained in:
9 Technology Group LTD
2025-11-15 00:08:37 +00:00
committed by GitHub
18 changed files with 840 additions and 697 deletions

270
agents/direct_host_auto_enroll.sh Executable file
View File

@@ -0,0 +1,270 @@
#!/bin/sh
# PatchMon Direct Host Auto-Enrollment Script
# POSIX-compliant shell script (works with dash, ash, bash, etc.)
# Usage: curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=direct-host&token_key=KEY&token_secret=SECRET" | sh
set -e
SCRIPT_VERSION="1.0.0"
# =============================================================================
# PatchMon Direct Host Auto-Enrollment Script
# =============================================================================
# This script automatically enrolls the current host into PatchMon for patch
# management.
#
# Usage:
# curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=direct-host&token_key=KEY&token_secret=SECRET" | sh
#
# With custom friendly name:
# curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=direct-host&token_key=KEY&token_secret=SECRET" | FRIENDLY_NAME="My Server" sh
#
# Requirements:
# - Run as root or with sudo
# - 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}"
FORCE_INSTALL="${FORCE_INSTALL:-false}"
FRIENDLY_NAME="${FRIENDLY_NAME:-}" # Optional: Custom friendly name for the host
# ===== 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() { printf "%b\n" "${GREEN}[INFO]${NC} $1"; }
warn() { printf "%b\n" "${YELLOW}[WARN]${NC} $1"; }
error() { printf "%b\n" "${RED}[ERROR]${NC} $1" >&2; exit 1; }
success() { printf "%b\n" "${GREEN}[SUCCESS]${NC} $1"; }
debug() { [ "${DEBUG:-false}" = "true" ] && printf "%b\n" "${BLUE}[DEBUG]${NC} $1" || true; }
# ===== BANNER =====
cat << "EOF"
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ ____ _ _ __ __ ║
| _ \ __ _| |_ ___| |__ | \/ | ___ _ __ ║
| |_) / _` | __/ __| '_ \| |\/| |/ _ \| '_ \
| __/ (_| | || (__| | | | | | | (_) | | | |
|_| \__,_|\__\___|_| |_|_| |_|\___/|_| |_|
║ ║
║ Direct Host 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 as root
if [ "$(id -u)" -ne 0 ]; then
error "This script must be run as root (use sudo)"
fi
# Check for required commands
for cmd in curl; do
if ! command -v $cmd >/dev/null 2>&1; then
error "Required command '$cmd' not found. Please install it first."
fi
done
info "Configuration validated successfully"
info "PatchMon Server: $PATCHMON_URL"
echo ""
# ===== GATHER HOST INFORMATION =====
info "Gathering host information..."
# Get hostname
hostname=$(hostname)
# Use FRIENDLY_NAME env var if provided, otherwise use hostname
if [ -n "$FRIENDLY_NAME" ]; then
friendly_name="$FRIENDLY_NAME"
info "Using custom friendly name: $friendly_name"
else
friendly_name="$hostname"
fi
# Try to get machine_id (optional, for tracking)
machine_id=""
if [ -f /etc/machine-id ]; then
machine_id=$(cat /etc/machine-id 2>/dev/null || echo "")
elif [ -f /var/lib/dbus/machine-id ]; then
machine_id=$(cat /var/lib/dbus/machine-id 2>/dev/null || echo "")
fi
# Get OS information
os_info="unknown"
if [ -f /etc/os-release ]; then
os_info=$(grep "^PRETTY_NAME=" /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo "unknown")
fi
# Get IP address (first non-loopback)
ip_address=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")
# Detect architecture
arch_raw=$(uname -m 2>/dev/null || echo "unknown")
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
info "Hostname: $hostname"
info "Friendly Name: $friendly_name"
info "IP Address: $ip_address"
info "OS: $os_info"
info "Architecture: $architecture"
if [ -n "$machine_id" ]; then
# POSIX-compliant substring (first 16 chars)
machine_id_short=$(printf "%.16s" "$machine_id")
info "Machine ID: ${machine_id_short}..."
else
info "Machine ID: (not available)"
fi
echo ""
# ===== CHECK IF AGENT ALREADY INSTALLED =====
info "Checking if agent is already configured..."
config_check=$(sh -c "
if [ -f /etc/patchmon/config.yml ] && [ -f /etc/patchmon/credentials.yml ]; then
if [ -f /usr/local/bin/patchmon-agent ]; then
# Try to ping using existing configuration
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
echo 'ping_success'
else
echo 'ping_failed'
fi
else
echo 'binary_missing'
fi
else
echo 'not_configured'
fi
" 2>/dev/null || echo "error")
if [ "$config_check" = "ping_success" ]; then
success "Host already enrolled and agent ping successful - nothing to do"
exit 0
elif [ "$config_check" = "ping_failed" ]; then
warn "Agent configuration exists but ping failed - will reinstall"
elif [ "$config_check" = "binary_missing" ]; then
warn "Config exists but agent binary missing - will reinstall"
elif [ "$config_check" = "not_configured" ]; then
info "Agent not yet configured - proceeding with enrollment"
else
warn "Could not check agent status - proceeding with enrollment"
fi
echo ""
# ===== ENROLL HOST =====
info "Enrolling $friendly_name in PatchMon..."
# Build JSON payload
json_payload=$(cat <<EOF
{
"friendly_name": "$friendly_name",
"metadata": {
"hostname": "$hostname",
"ip_address": "$ip_address",
"os_info": "$os_info",
"architecture": "$architecture"
}
}
EOF
)
# Add machine_id if available
if [ -n "$machine_id" ]; then
json_payload=$(echo "$json_payload" | sed "s/\"friendly_name\"/\"machine_id\": \"$machine_id\",\n \"friendly_name\"/")
fi
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 "$json_payload" \
"$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
# Use grep and cut instead of jq since jq may not be installed
api_id=$(echo "$body" | grep -o '"api_id":"[^"]*' | cut -d'"' -f4 || echo "")
api_key=$(echo "$body" | grep -o '"api_key":"[^"]*' | cut -d'"' -f4 || echo "")
if [ -z "$api_id" ] || [ -z "$api_key" ]; then
error "Failed to parse API credentials from response"
fi
success "Host enrolled successfully: $api_id"
echo ""
# ===== INSTALL AGENT =====
info "Installing PatchMon agent..."
# Build install URL with force flag and architecture
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"
# Download and execute installation script
install_exit_code=0
install_output=$(curl $CURL_FLAGS \
-H "X-API-ID: $api_id" \
-H "X-API-KEY: $api_key" \
"$install_url" | sh 2>&1) || install_exit_code=$?
# Check both exit code AND success message in output
if [ "$install_exit_code" -eq 0 ] || echo "$install_output" | grep -q "PatchMon Agent installation completed successfully"; then
success "Agent installed successfully"
else
error "Failed to install agent (exit: $install_exit_code)"
fi
else
printf "%b\n" "${RED}[ERROR]${NC} Failed to enroll $friendly_name - HTTP $http_code" >&2
printf "%b\n" "Response: $body" >&2
exit 1
fi
echo ""
success "Auto-enrollment complete!"
exit 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -19,20 +19,20 @@ NC='\033[0m' # No Color
# Functions
error() {
printf "%b\n" "${RED}ERROR: $1${NC}" >&2
printf "%b\n" "${RED}ERROR: $1${NC}" >&2
exit 1
}
info() {
printf "%b\n" "${BLUE} $1${NC}"
printf "%b\n" "${BLUE}INFO: $1${NC}"
}
success() {
printf "%b\n" "${GREEN} $1${NC}"
printf "%b\n" "${GREEN}SUCCESS: $1${NC}"
}
warning() {
printf "%b\n" "${YELLOW}⚠️ $1${NC}"
printf "%b\n" "${YELLOW}WARNING: $1${NC}"
}
# Check if running as root
@@ -42,7 +42,7 @@ fi
# Verify system datetime and timezone
verify_datetime() {
info "🕐 Verifying system datetime and timezone..."
info "Verifying system datetime and timezone..."
# Get current system time
system_time=$(date)
@@ -50,7 +50,7 @@ verify_datetime() {
# Display current datetime info
echo ""
printf "%b\n" "${BLUE}📅 Current System Date/Time:${NC}"
printf "%b\n" "${BLUE}Current System Date/Time:${NC}"
echo " • Date/Time: $system_time"
echo " • Timezone: $timezone"
echo ""
@@ -62,26 +62,26 @@ verify_datetime() {
read -r response
case "$response" in
[Yy]*)
success "Date/time verification passed"
success "Date/time verification passed"
echo ""
return 0
;;
*)
echo ""
printf "%b\n" "${RED}Date/time verification failed${NC}"
printf "%b\n" "${RED}Date/time verification failed${NC}"
echo ""
printf "%b\n" "${YELLOW}💡 Please fix the date/time and re-run the installation script:${NC}"
printf "%b\n" "${YELLOW}Please fix the date/time and re-run the installation script:${NC}"
echo " sudo timedatectl set-time 'YYYY-MM-DD HH:MM:SS'"
echo " sudo timedatectl set-timezone 'America/New_York' # or your timezone"
echo " sudo timedatectl list-timezones # to see available timezones"
echo ""
printf "%b\n" "${BLUE} After fixing the date/time, re-run this installation script.${NC}"
printf "%b\n" "${BLUE}After fixing the date/time, re-run this installation script.${NC}"
error "Installation cancelled - please fix date/time and re-run"
;;
esac
else
# Non-interactive (piped from curl) - show warning and continue
printf "%b\n" "${YELLOW}⚠️ Non-interactive installation detected${NC}"
printf "%b\n" "${YELLOW}Non-interactive installation detected${NC}"
echo ""
echo "Please verify the date/time shown above is correct."
echo "If the date/time is incorrect, it may cause issues with:"
@@ -89,8 +89,8 @@ verify_datetime() {
echo " • Scheduled updates"
echo " • Data synchronization"
echo ""
printf "%b\n" "${GREEN}Continuing with installation...${NC}"
success "Date/time verification completed (assumed correct)"
printf "%b\n" "${GREEN}Continuing with installation...${NC}"
success "Date/time verification completed (assumed correct)"
echo ""
fi
}
@@ -159,7 +159,7 @@ if [ -z "$ARCHITECTURE" ]; then
ARCHITECTURE="arm"
;;
*)
warning "⚠️ Unknown architecture '$arch_raw', defaulting to amd64"
warning "Unknown architecture '$arch_raw', defaulting to amd64"
ARCHITECTURE="amd64"
;;
esac
@@ -177,31 +177,21 @@ case "$*" in
esac
if [ "$FORCE_INSTALL" = "true" ]; then
FORCE_INSTALL="true"
warning "⚠️ Force mode enabled - will bypass broken packages"
warning "Force mode enabled - will bypass broken packages"
fi
# Get unique machine ID for this host
MACHINE_ID=$(get_machine_id)
export MACHINE_ID
info "🚀 Starting PatchMon Agent Installation..."
info "📋 Server: $PATCHMON_URL"
info "🔑 API ID: $(echo "$API_ID" | cut -c1-16)..."
info "🆔 Machine ID: $(echo "$MACHINE_ID" | cut -c1-16)..."
info "🏗️ Architecture: $ARCHITECTURE"
# Display diagnostic information
echo ""
printf "%b\n" "${BLUE}🔧 Installation Diagnostics:${NC}"
echo " • URL: $PATCHMON_URL"
echo " • CURL FLAGS: $CURL_FLAGS"
echo " • API ID: $(echo "$API_ID" | cut -c1-16)..."
echo " • API Key: $(echo "$API_KEY" | cut -c1-16)..."
echo " • Architecture: $ARCHITECTURE"
echo ""
info "Starting PatchMon Agent Installation..."
info "Server: $PATCHMON_URL"
info "API ID: $(echo "$API_ID" | cut -c1-16)..."
info "Machine ID: $(echo "$MACHINE_ID" | cut -c1-16)..."
info "Architecture: $ARCHITECTURE"
# Install required dependencies
info "📦 Installing required dependencies..."
info "Installing required dependencies..."
echo ""
# Function to check if a command exists
@@ -417,7 +407,7 @@ if command -v apt-get >/dev/null 2>&1; then
if [ "$FORCE_INSTALL" = "true" ]; then
warning "Detected broken packages on system - force mode will work around them"
else
warning "⚠️ Broken packages detected on system"
warning "Broken packages detected on system"
warning "If installation fails, retry with: curl -s {URL}/api/v1/hosts/install --force -H ..."
fi
fi
@@ -466,88 +456,88 @@ success "Dependencies installation completed"
echo ""
# Step 1: Handle existing configuration directory
info "📁 Setting up configuration directory..."
info "Setting up configuration directory..."
# Check if configuration directory already exists
if [ -d "/etc/patchmon" ]; then
warning "⚠️ Configuration directory already exists at /etc/patchmon"
warning "⚠️ Preserving existing configuration files"
warning "Configuration directory already exists at /etc/patchmon"
warning "Preserving existing configuration files"
# List existing files for user awareness
info "📋 Existing files in /etc/patchmon:"
info "Existing files in /etc/patchmon:"
ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do
echo " $line"
done
else
info "📁 Creating new configuration directory..."
info "Creating new configuration directory..."
mkdir -p /etc/patchmon
fi
# Check if agent is already configured and working (before we overwrite anything)
info "🔍 Checking if agent is already configured..."
info "Checking if agent is already configured..."
if [ -f /etc/patchmon/config.yml ] && [ -f /etc/patchmon/credentials.yml ]; then
if [ -f /usr/local/bin/patchmon-agent ]; then
info "📋 Found existing agent configuration"
info "🧪 Testing existing configuration with ping..."
info "Found existing agent configuration"
info "Testing existing configuration with ping..."
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "Agent is already configured and ping successful"
info "📋 Existing configuration is working - skipping installation"
success "Agent is already configured and ping successful"
info "Existing configuration is working - skipping installation"
info ""
info "If you want to reinstall, remove the configuration files first:"
info " sudo rm -f /etc/patchmon/config.yml /etc/patchmon/credentials.yml"
echo ""
exit 0
else
warning "⚠️ Agent configuration exists but ping failed"
warning "⚠️ Will move existing configuration and reinstall"
warning "Agent configuration exists but ping failed"
warning "Will move existing configuration and reinstall"
echo ""
fi
else
warning "⚠️ Configuration files exist but agent binary is missing"
warning "⚠️ Will move existing configuration and reinstall"
warning "Configuration files exist but agent binary is missing"
warning "Will move existing configuration and reinstall"
echo ""
fi
else
success "Agent not yet configured - proceeding with installation"
success "Agent not yet configured - proceeding with installation"
echo ""
fi
# Step 2: Create configuration files
info "🔐 Creating configuration files..."
info "Creating configuration files..."
# Check if config file already exists
if [ -f "/etc/patchmon/config.yml" ]; then
warning "⚠️ Config file already exists at /etc/patchmon/config.yml"
warning "⚠️ Moving existing file out of the way for fresh installation"
warning "Config file already exists at /etc/patchmon/config.yml"
warning "Moving existing file out of the way for fresh installation"
# Clean up old config backups (keep only last 3)
ls -t /etc/patchmon/config.yml.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
# Move existing file out of the way
mv /etc/patchmon/config.yml /etc/patchmon/config.yml.backup.$(date +%Y%m%d_%H%M%S)
info "📋 Moved existing config to: /etc/patchmon/config.yml.backup.$(date +%Y%m%d_%H%M%S)"
info "Moved existing config to: /etc/patchmon/config.yml.backup.$(date +%Y%m%d_%H%M%S)"
fi
# Check if credentials file already exists
if [ -f "/etc/patchmon/credentials.yml" ]; then
warning "⚠️ Credentials file already exists at /etc/patchmon/credentials.yml"
warning "⚠️ Moving existing file out of the way for fresh installation"
warning "Credentials file already exists at /etc/patchmon/credentials.yml"
warning "Moving existing file out of the way for fresh installation"
# Clean up old credential backups (keep only last 3)
ls -t /etc/patchmon/credentials.yml.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
# Move existing file out of the way
mv /etc/patchmon/credentials.yml /etc/patchmon/credentials.yml.backup.$(date +%Y%m%d_%H%M%S)
info "📋 Moved existing credentials to: /etc/patchmon/credentials.yml.backup.$(date +%Y%m%d_%H%M%S)"
info "Moved existing credentials to: /etc/patchmon/credentials.yml.backup.$(date +%Y%m%d_%H%M%S)"
fi
# Clean up old credentials file if it exists (from previous installations)
if [ -f "/etc/patchmon/credentials" ]; then
warning "⚠️ Found old credentials file, removing it..."
warning "Found old credentials file, removing it..."
rm -f /etc/patchmon/credentials
info "📋 Removed old credentials file"
info "Removed old credentials file"
fi
# Create main config file
@@ -574,29 +564,29 @@ chmod 600 /etc/patchmon/config.yml
chmod 600 /etc/patchmon/credentials.yml
# Step 3: Download the PatchMon agent binary using API credentials
info "📥 Downloading PatchMon agent binary..."
info "Downloading PatchMon agent binary..."
# Determine the binary filename based on architecture
BINARY_NAME="patchmon-agent-linux-${ARCHITECTURE}"
# Check if agent binary already exists
if [ -f "/usr/local/bin/patchmon-agent" ]; then
warning "⚠️ Agent binary already exists at /usr/local/bin/patchmon-agent"
warning "⚠️ Moving existing file out of the way for fresh installation"
warning "Agent binary already exists at /usr/local/bin/patchmon-agent"
warning "Moving existing file out of the way for fresh installation"
# Clean up old agent backups (keep only last 3)
ls -t /usr/local/bin/patchmon-agent.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
# Move existing file out of the way
mv /usr/local/bin/patchmon-agent /usr/local/bin/patchmon-agent.backup.$(date +%Y%m%d_%H%M%S)
info "📋 Moved existing agent to: /usr/local/bin/patchmon-agent.backup.$(date +%Y%m%d_%H%M%S)"
info "Moved existing agent to: /usr/local/bin/patchmon-agent.backup.$(date +%Y%m%d_%H%M%S)"
fi
# Clean up old shell script if it exists (from previous installations)
if [ -f "/usr/local/bin/patchmon-agent.sh" ]; then
warning "⚠️ Found old shell script agent, removing it..."
warning "Found old shell script agent, removing it..."
rm -f /usr/local/bin/patchmon-agent.sh
info "📋 Removed old shell script agent"
info "Removed old shell script agent"
fi
# Download the binary
@@ -610,30 +600,30 @@ chmod +x /usr/local/bin/patchmon-agent
# Get the agent version from the binary
AGENT_VERSION=$(/usr/local/bin/patchmon-agent version 2>/dev/null || echo "Unknown")
info "📋 Agent version: $AGENT_VERSION"
info "Agent version: $AGENT_VERSION"
# Handle existing log files and create log directory
info "📁 Setting up log directory..."
info "Setting up log directory..."
# Create log directory if it doesn't exist
mkdir -p /etc/patchmon/logs
# Handle existing log files
if [ -f "/etc/patchmon/logs/patchmon-agent.log" ]; then
warning "⚠️ Existing log file found at /etc/patchmon/logs/patchmon-agent.log"
warning "⚠️ Rotating log file for fresh start"
warning "Existing log file found at /etc/patchmon/logs/patchmon-agent.log"
warning "Rotating log file for fresh start"
# Rotate the log file
mv /etc/patchmon/logs/patchmon-agent.log /etc/patchmon/logs/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)
info "📋 Log file rotated to: /etc/patchmon/logs/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)"
info "Log file rotated to: /etc/patchmon/logs/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)"
fi
# Step 4: Test the configuration
info "🧪 Testing API credentials and connectivity..."
info "Testing API credentials and connectivity..."
if /usr/local/bin/patchmon-agent ping; then
success "TEST: API credentials are valid and server is reachable"
success "TEST: API credentials are valid and server is reachable"
else
error "Failed to validate API credentials or reach server"
error "Failed to validate API credentials or reach server"
fi
# Step 5: Setup service for WebSocket connection
@@ -641,16 +631,16 @@ fi
# Detect init system and create appropriate service
if command -v systemctl >/dev/null 2>&1; then
# Systemd is available
info "🔧 Setting up systemd service..."
info "Setting up systemd service..."
# Stop and disable existing service if it exists
if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then
warning "⚠️ Stopping existing PatchMon agent service..."
warning "Stopping existing PatchMon agent service..."
systemctl stop patchmon-agent.service
fi
if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then
warning "⚠️ Disabling existing PatchMon agent service..."
warning "Disabling existing PatchMon agent service..."
systemctl disable patchmon-agent.service
fi
@@ -680,9 +670,9 @@ EOF
# Clean up old crontab entries if they exist (from previous installations)
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
warning "⚠️ Found old crontab entries, removing them..."
warning "Found old crontab entries, removing them..."
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
info "📋 Removed old crontab entries"
info "Removed old crontab entries"
fi
# Reload systemd and enable/start the service
@@ -692,25 +682,25 @@ EOF
# Check if service started successfully
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon Agent service started successfully"
info "🔗 WebSocket connection established"
success "PatchMon Agent service started successfully"
info "WebSocket connection established"
else
warning "⚠️ Service may have failed to start. Check status with: systemctl status patchmon-agent"
warning "Service may have failed to start. Check status with: systemctl status patchmon-agent"
fi
SERVICE_TYPE="systemd"
elif [ -d /etc/init.d ] && command -v rc-service >/dev/null 2>&1; then
# OpenRC is available (Alpine Linux)
info "🔧 Setting up OpenRC service..."
info "Setting up OpenRC service..."
# Stop and disable existing service if it exists
if rc-service patchmon-agent status >/dev/null 2>&1; then
warning "⚠️ Stopping existing PatchMon agent service..."
warning "Stopping existing PatchMon agent service..."
rc-service patchmon-agent stop
fi
if rc-update show default 2>/dev/null | grep -q "patchmon-agent"; then
warning "⚠️ Disabling existing PatchMon agent service..."
warning "Disabling existing PatchMon agent service..."
rc-update del patchmon-agent default
fi
@@ -737,9 +727,9 @@ EOF
# Clean up old crontab entries if they exist (from previous installations)
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
warning "⚠️ Found old crontab entries, removing them..."
warning "Found old crontab entries, removing them..."
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
info "📋 Removed old crontab entries"
info "Removed old crontab entries"
fi
# Enable and start the service
@@ -748,40 +738,40 @@ EOF
# Check if service started successfully
if rc-service patchmon-agent status >/dev/null 2>&1; then
success "PatchMon Agent service started successfully"
info "🔗 WebSocket connection established"
success "PatchMon Agent service started successfully"
info "WebSocket connection established"
else
warning "⚠️ Service may have failed to start. Check status with: rc-service patchmon-agent status"
warning "Service may have failed to start. Check status with: rc-service patchmon-agent status"
fi
SERVICE_TYPE="openrc"
else
# No init system detected, use crontab as fallback
warning "⚠️ No init system detected (systemd or OpenRC). Using crontab for service management."
warning "No init system detected (systemd or OpenRC). Using crontab for service management."
# Clean up old crontab entries if they exist
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
warning "⚠️ Found old crontab entries, removing them..."
warning "Found old crontab entries, removing them..."
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
info "📋 Removed old crontab entries"
info "Removed old crontab entries"
fi
# Add crontab entry to run the agent
(crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/patchmon-agent serve >/dev/null 2>&1") | crontab -
info "📋 Added crontab entry for PatchMon agent"
info "Added crontab entry for PatchMon agent"
# Start the agent manually
/usr/local/bin/patchmon-agent serve >/dev/null 2>&1 &
success "PatchMon Agent started in background"
info "🔗 WebSocket connection established"
success "PatchMon Agent started in background"
info "WebSocket connection established"
SERVICE_TYPE="crontab"
fi
# Installation complete
success "🎉 PatchMon Agent installation completed successfully!"
success "PatchMon Agent installation completed successfully!"
echo ""
printf "%b\n" "${GREEN}📋 Installation Summary:${NC}"
printf "%b\n" "${GREEN}Installation Summary:${NC}"
echo " • Configuration directory: /etc/patchmon"
echo " • Agent binary installed: /usr/local/bin/patchmon-agent"
echo " • Architecture: $ARCHITECTURE"
@@ -801,16 +791,16 @@ echo " • Logs directory: /etc/patchmon/logs"
MOVED_FILES=$(ls /etc/patchmon/credentials.yml.backup.* /etc/patchmon/config.yml.backup.* /usr/local/bin/patchmon-agent.backup.* /etc/patchmon/logs/patchmon-agent.log.old.* /usr/local/bin/patchmon-agent.sh.backup.* /etc/patchmon/credentials.backup.* 2>/dev/null || true)
if [ -n "$MOVED_FILES" ]; then
echo ""
printf "%b\n" "${YELLOW}📋 Files Moved for Fresh Installation:${NC}"
printf "%b\n" "${YELLOW}Files Moved for Fresh Installation:${NC}"
echo "$MOVED_FILES" | while read -r moved_file; do
echo "$moved_file"
done
echo ""
printf "%b\n" "${BLUE}💡 Note: Old files are automatically cleaned up (keeping last 3)${NC}"
printf "%b\n" "${BLUE}Note: Old files are automatically cleaned up (keeping last 3)${NC}"
fi
echo ""
printf "%b\n" "${BLUE}🔧 Management Commands:${NC}"
printf "%b\n" "${BLUE}Management Commands:${NC}"
echo " • Test connection: /usr/local/bin/patchmon-agent ping"
echo " • Manual report: /usr/local/bin/patchmon-agent report"
echo " • Check status: /usr/local/bin/patchmon-agent diagnostics"
@@ -827,4 +817,4 @@ else
echo " • Restart service: pkill -f 'patchmon-agent serve' && /usr/local/bin/patchmon-agent serve &"
fi
echo ""
success "Your system is now being monitored by PatchMon!"
success "Your system is now being monitored by PatchMon!"

View File

@@ -1,6 +1,6 @@
{
"name": "patchmon-backend",
"version": "1.3.3",
"version": "1.3.4",
"description": "Backend API for Linux Patch Monitoring System",
"license": "AGPL-3.0",
"main": "src/server.js",

View File

@@ -0,0 +1,13 @@
-- Remove machine_id unique constraint and make it nullable
-- This allows multiple hosts with the same machine_id
-- Duplicate detection now relies on config.yml/credentials.yml checking instead
-- Drop the unique constraint
ALTER TABLE "hosts" DROP CONSTRAINT IF EXISTS "hosts_machine_id_key";
-- Make machine_id nullable
ALTER TABLE "hosts" ALTER COLUMN "machine_id" DROP NOT NULL;
-- Keep the index for query performance (but not unique)
CREATE INDEX IF NOT EXISTS "hosts_machine_id_idx" ON "hosts"("machine_id");

View File

@@ -81,7 +81,7 @@ model host_repositories {
model hosts {
id String @id
machine_id String @unique
machine_id String?
friendly_name String
ip String?
os_type String

View File

@@ -481,19 +481,22 @@ router.delete(
);
// ========== AUTO-ENROLLMENT ENDPOINTS (Used by Scripts) ==========
// Future integrations can follow this pattern:
// - /proxmox-lxc - Proxmox LXC containers
// - /vmware-esxi - VMware ESXi VMs
// - /docker - Docker containers
// - /kubernetes - Kubernetes pods
// - /aws-ec2 - AWS EC2 instances
// Universal script-serving endpoint with type parameter
// Supported types:
// - proxmox-lxc - Proxmox LXC containers
// - direct-host - Direct host enrollment
// Future types:
// - vmware-esxi - VMware ESXi VMs
// - docker - Docker containers
// - kubernetes - Kubernetes pods
// Serve the Proxmox LXC enrollment script with credentials injected
router.get("/proxmox-lxc", async (req, res) => {
// Serve auto-enrollment scripts with credentials injected
router.get("/script", async (req, res) => {
try {
// Get token from query params
// Get parameters from query params
const token_key = req.query.token_key;
const token_secret = req.query.token_secret;
const script_type = req.query.type;
if (!token_key || !token_secret) {
return res
@@ -501,6 +504,25 @@ router.get("/proxmox-lxc", async (req, res) => {
.json({ error: "Token key and secret required as query parameters" });
}
if (!script_type) {
return res.status(400).json({
error:
"Script type required as query parameter (e.g., ?type=proxmox-lxc or ?type=direct-host)",
});
}
// Map script types to script file paths
const scriptMap = {
"proxmox-lxc": "proxmox_auto_enroll.sh",
"direct-host": "direct_host_auto_enroll.sh",
};
if (!scriptMap[script_type]) {
return res.status(400).json({
error: `Invalid script type: ${script_type}. Supported types: ${Object.keys(scriptMap).join(", ")}`,
});
}
// Validate token
const token = await prisma.auto_enrollment_tokens.findUnique({
where: { token_key: token_key },
@@ -526,13 +548,13 @@ router.get("/proxmox-lxc", async (req, res) => {
const script_path = path.join(
__dirname,
"../../../agents/proxmox_auto_enroll.sh",
`../../../agents/${scriptMap[script_type]}`,
);
if (!fs.existsSync(script_path)) {
return res
.status(404)
.json({ error: "Proxmox enrollment script not found" });
return res.status(404).json({
error: `Enrollment script not found: ${scriptMap[script_type]}`,
});
}
let script = fs.readFileSync(script_path, "utf8");
@@ -591,11 +613,11 @@ export FORCE_INSTALL="${force_install ? "true" : "false"}"
res.setHeader("Content-Type", "text/plain");
res.setHeader(
"Content-Disposition",
'inline; filename="proxmox_auto_enroll.sh"',
`inline; filename="${scriptMap[script_type]}"`,
);
res.send(script);
} catch (error) {
console.error("Proxmox script serve error:", error);
console.error("Script serve error:", error);
res.status(500).json({ error: "Failed to serve enrollment script" });
}
});
@@ -609,8 +631,11 @@ router.post(
.isLength({ min: 1, max: 255 })
.withMessage("Friendly name is required"),
body("machine_id")
.optional()
.isLength({ min: 1, max: 255 })
.withMessage("Machine ID is required"),
.withMessage(
"Machine ID must be between 1 and 255 characters if provided",
),
body("metadata").optional().isObject(),
],
async (req, res) => {
@@ -626,24 +651,7 @@ router.post(
const api_id = `patchmon_${crypto.randomBytes(8).toString("hex")}`;
const api_key = crypto.randomBytes(32).toString("hex");
// Check if host already exists by machine_id (not hostname)
const existing_host = await prisma.hosts.findUnique({
where: { machine_id },
});
if (existing_host) {
return res.status(409).json({
error: "Host already exists",
host_id: existing_host.id,
api_id: existing_host.api_id,
machine_id: existing_host.machine_id,
friendly_name: existing_host.friendly_name,
message:
"This machine is already enrolled in PatchMon (matched by machine ID)",
});
}
// Create host
// Create host (no duplicate check - using config.yml checking instead)
const host = await prisma.hosts.create({
data: {
id: uuidv4(),
@@ -760,30 +768,7 @@ router.post(
try {
const { friendly_name, machine_id } = host_data;
if (!machine_id) {
results.failed.push({
friendly_name,
error: "Machine ID is required",
});
continue;
}
// Check if host already exists by machine_id
const existing_host = await prisma.hosts.findUnique({
where: { machine_id },
});
if (existing_host) {
results.skipped.push({
friendly_name,
machine_id,
reason: "Machine already enrolled",
api_id: existing_host.api_id,
});
continue;
}
// Generate credentials
// Generate credentials (no duplicate check - using config.yml checking instead)
const api_id = `patchmon_${crypto.randomBytes(8).toString("hex")}`;
const api_key = crypto.randomBytes(32).toString("hex");

View File

@@ -551,8 +551,11 @@ router.post(
updated_at: new Date(),
};
// Update machine_id if provided and current one is a placeholder
if (req.body.machineId && host.machine_id.startsWith("pending-")) {
// Update machine_id if provided and current one is a placeholder or null
if (
req.body.machineId &&
(host.machine_id === null || host.machine_id.startsWith("pending-"))
) {
updateData.machine_id = req.body.machineId;
}
@@ -1708,47 +1711,7 @@ ${archExport}
}
});
// Check if machine_id already exists (requires auth)
router.post("/check-machine-id", validateApiCredentials, async (req, res) => {
try {
const { machine_id } = req.body;
if (!machine_id) {
return res.status(400).json({
error: "machine_id is required",
});
}
// Check if a host with this machine_id exists
const existing_host = await prisma.hosts.findUnique({
where: { machine_id },
select: {
id: true,
friendly_name: true,
machine_id: true,
api_id: true,
status: true,
created_at: true,
},
});
if (existing_host) {
return res.status(200).json({
exists: true,
host: existing_host,
message: "This machine is already enrolled",
});
}
return res.status(200).json({
exists: false,
message: "Machine not yet enrolled",
});
} catch (error) {
console.error("Error checking machine_id:", error);
res.status(500).json({ error: "Failed to check machine_id" });
}
});
// Note: /check-machine-id endpoint removed - using config.yml checking method instead
// Serve the removal script (public endpoint - no authentication required)
router.get("/remove", async (_req, res) => {

View File

@@ -20,9 +20,7 @@ COPY --chown=node:node agents ./agents_backup
COPY --chown=node:node agents ./agents
COPY --chmod=755 docker/backend.docker-entrypoint.sh ./entrypoint.sh
WORKDIR /app/backend
RUN npm ci --ignore-scripts && npx prisma generate
RUN npm install --workspace=backend --ignore-scripts && cd backend && npx prisma generate
EXPOSE 3001
@@ -44,13 +42,11 @@ WORKDIR /app
COPY --chown=node:node package*.json ./
COPY --chown=node:node backend/ ./backend/
WORKDIR /app/backend
RUN npm cache clean --force &&\
rm -rf node_modules ~/.npm /root/.npm &&\
npm ci --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=3 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 &&\
PRISMA_CLI_BINARY_TYPE=binary npm run db:generate &&\
npm prune --omit=dev &&\
npm install --workspace=backend --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=3 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 &&\
cd backend && PRISMA_CLI_BINARY_TYPE=binary npm run db:generate &&\
cd .. && npm prune --omit=dev --workspace=backend &&\
npm cache clean --force
# Production stage

View File

@@ -6,7 +6,7 @@ WORKDIR /app
COPY package*.json ./
COPY frontend/ ./frontend/
RUN npm ci --ignore-scripts
RUN npm install --workspace=frontend --ignore-scripts
WORKDIR /app/frontend

View File

@@ -6,5 +6,5 @@ VITE_API_URL=http://localhost:3001/api/v1
# Application Metadata
VITE_APP_NAME=PatchMon
VITE_APP_VERSION=1.3.1
VITE_APP_VERSION=1.3.4

View File

@@ -1,7 +1,7 @@
{
"name": "patchmon-frontend",
"private": true,
"version": "1.3.3",
"version": "1.3.4",
"license": "AGPL-3.0",
"type": "module",
"scripts": {

File diff suppressed because it is too large Load Diff

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "patchmon",
"version": "1.3.2",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "patchmon",
"version": "1.3.2",
"version": "1.3.4",
"license": "AGPL-3.0",
"workspaces": [
"backend",
@@ -23,7 +23,7 @@
},
"backend": {
"name": "patchmon-backend",
"version": "1.3.2",
"version": "1.3.4",
"license": "AGPL-3.0",
"dependencies": {
"@bull-board/api": "^6.13.1",
@@ -59,7 +59,7 @@
},
"frontend": {
"name": "patchmon-frontend",
"version": "1.3.2",
"version": "1.3.4",
"license": "AGPL-3.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "patchmon",
"version": "1.3.3",
"version": "1.3.4",
"description": "Linux Patch Monitoring System",
"license": "AGPL-3.0",
"private": true,