mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Update PatchMon version to 1.2.7
- Updated agent script version to 1.2.7 - Updated all package.json files to version 1.2.7 - Updated backend version references - Updated setup script version references - Fixed agent file path issues in API endpoints - Fixed linting issues (Node.js imports, unused variables, accessibility) - Created comprehensive version update guide in patchmon-admin/READMEs/
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PatchMon Agent Script v1.2.6
|
||||
# PatchMon Agent Script v1.2.7
|
||||
# This script sends package update information to the PatchMon server using API credentials
|
||||
|
||||
# Configuration
|
||||
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
|
||||
API_VERSION="v1"
|
||||
AGENT_VERSION="1.2.6"
|
||||
AGENT_VERSION="1.2.7"
|
||||
CONFIG_FILE="/etc/patchmon/agent.conf"
|
||||
CREDENTIALS_FILE="/etc/patchmon/credentials"
|
||||
LOG_FILE="/var/log/patchmon-agent.log"
|
||||
@@ -144,7 +144,7 @@ EOF
|
||||
test_credentials() {
|
||||
load_credentials
|
||||
|
||||
local response=$(curl -ksv -X POST \
|
||||
local response=$(curl -ks -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-ID: $API_ID" \
|
||||
-H "X-API-KEY: $API_KEY" \
|
||||
@@ -809,7 +809,7 @@ EOF
|
||||
local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]')
|
||||
|
||||
|
||||
local response=$(curl -ksv -X POST \
|
||||
local response=$(curl -ks -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-ID: $API_ID" \
|
||||
-H "X-API-KEY: $API_KEY" \
|
||||
@@ -821,25 +821,18 @@ EOF
|
||||
success "Update sent successfully"
|
||||
echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2 | xargs -I {} info "Processed {} packages"
|
||||
|
||||
# Check for PatchMon agent update instructions (this updates the agent script, not system packages)
|
||||
if echo "$response" | grep -q '"autoUpdate":{'; then
|
||||
local auto_update_section=$(echo "$response" | grep -o '"autoUpdate":{[^}]*}')
|
||||
local should_update=$(echo "$auto_update_section" | grep -o '"shouldUpdate":true' | cut -d':' -f2)
|
||||
if [[ "$should_update" == "true" ]]; then
|
||||
local latest_version=$(echo "$auto_update_section" | grep -o '"latestVersion":"[^"]*' | cut -d'"' -f4)
|
||||
local current_version=$(echo "$auto_update_section" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4)
|
||||
local update_message=$(echo "$auto_update_section" | grep -o '"message":"[^"]*' | cut -d'"' -f4)
|
||||
|
||||
info "PatchMon agent update detected: $update_message"
|
||||
info "Current version: $current_version, Latest version: $latest_version"
|
||||
|
||||
# Automatically run update-agent command to update the PatchMon agent script
|
||||
info "Automatically updating PatchMon agent to latest version..."
|
||||
# Check if auto-update is enabled and check for agent updates locally
|
||||
if check_auto_update_enabled; then
|
||||
info "Auto-update is enabled, checking for agent updates..."
|
||||
if check_agent_update_needed; then
|
||||
info "Agent update available, automatically updating..."
|
||||
if "$0" update-agent; then
|
||||
success "PatchMon agent update completed successfully"
|
||||
else
|
||||
warning "PatchMon agent update failed, but data was sent successfully"
|
||||
fi
|
||||
else
|
||||
info "Agent is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -877,7 +870,7 @@ EOF
|
||||
ping_server() {
|
||||
load_credentials
|
||||
|
||||
local response=$(curl -ksv -X POST \
|
||||
local response=$(curl -ks -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-ID: $API_ID" \
|
||||
-H "X-API-KEY: $API_KEY" \
|
||||
@@ -920,7 +913,7 @@ check_version() {
|
||||
|
||||
info "Checking for agent updates..."
|
||||
|
||||
local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version")
|
||||
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version")
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
local current_version=$(echo "$response" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4)
|
||||
@@ -949,59 +942,147 @@ check_version() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if auto-update is enabled (both globally and for this host)
|
||||
check_auto_update_enabled() {
|
||||
# Get settings from server using API credentials
|
||||
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/settings" 2>/dev/null)
|
||||
if [[ $? -ne 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if both global and host auto-update are enabled
|
||||
local global_auto_update=$(echo "$response" | grep -o '"auto_update":true' | cut -d':' -f2)
|
||||
local host_auto_update=$(echo "$response" | grep -o '"host_auto_update":true' | cut -d':' -f2)
|
||||
|
||||
if [[ "$global_auto_update" == "true" && "$host_auto_update" == "true" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if agent update is needed (internal function for auto-update)
|
||||
check_agent_update_needed() {
|
||||
# Get current agent timestamp
|
||||
local current_timestamp=0
|
||||
if [[ -f "$0" ]]; then
|
||||
current_timestamp=$(stat -c %Y "$0" 2>/dev/null || stat -f %m "$0" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Get server agent info using API credentials
|
||||
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp" 2>/dev/null)
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4)
|
||||
local server_timestamp=$(echo "$response" | grep -o '"timestamp":[0-9]*' | cut -d':' -f2)
|
||||
local server_exists=$(echo "$response" | grep -o '"exists":true' | cut -d':' -f2)
|
||||
|
||||
if [[ "$server_exists" != "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if update is needed
|
||||
if [[ "$server_version" != "$AGENT_VERSION" ]]; then
|
||||
return 0 # Update needed due to version mismatch
|
||||
elif [[ "$server_timestamp" -gt "$current_timestamp" ]]; then
|
||||
return 0 # Update needed due to newer timestamp
|
||||
else
|
||||
return 1 # No update needed
|
||||
fi
|
||||
else
|
||||
return 1 # Failed to check
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for agent updates based on version and timestamp (interactive command)
|
||||
check_agent_update() {
|
||||
load_credentials
|
||||
|
||||
info "Checking for agent updates..."
|
||||
|
||||
# Get current agent timestamp
|
||||
local current_timestamp=0
|
||||
if [[ -f "$0" ]]; then
|
||||
current_timestamp=$(stat -c %Y "$0" 2>/dev/null || stat -f %m "$0" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Get server agent info using API credentials
|
||||
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp")
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4)
|
||||
local server_timestamp=$(echo "$response" | grep -o '"timestamp":[0-9]*' | cut -d':' -f2)
|
||||
local server_exists=$(echo "$response" | grep -o '"exists":true' | cut -d':' -f2)
|
||||
|
||||
if [[ "$server_exists" != "true" ]]; then
|
||||
warning "No agent script found on server"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Current agent version: $AGENT_VERSION (timestamp: $current_timestamp)"
|
||||
info "Server agent version: $server_version (timestamp: $server_timestamp)"
|
||||
|
||||
# Check if update is needed
|
||||
if [[ "$server_version" != "$AGENT_VERSION" ]]; then
|
||||
info "Version mismatch detected - update needed"
|
||||
return 0
|
||||
elif [[ "$server_timestamp" -gt "$current_timestamp" ]]; then
|
||||
info "Server script is newer - update needed"
|
||||
return 0
|
||||
else
|
||||
info "Agent is up to date"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
error "Failed to check agent timestamp from server"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update agent script
|
||||
update_agent() {
|
||||
load_credentials
|
||||
|
||||
info "Updating agent script..."
|
||||
|
||||
local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version")
|
||||
local download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/download"
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
local download_url=$(echo "$response" | grep -o '"downloadUrl":"[^"]*' | cut -d'"' -f4)
|
||||
|
||||
if [[ -z "$download_url" ]]; then
|
||||
download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/download"
|
||||
elif [[ "$download_url" =~ ^/ ]]; then
|
||||
# If download_url is relative, prepend the server URL
|
||||
download_url="$PATCHMON_SERVER$download_url"
|
||||
fi
|
||||
|
||||
info "Downloading latest agent from: $download_url"
|
||||
|
||||
# Create backup of current script
|
||||
cp "$0" "$0.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Download new version
|
||||
if curl -ksv -o "/tmp/patchmon-agent-new.sh" "$download_url"; then
|
||||
# Verify the downloaded script is valid
|
||||
if bash -n "/tmp/patchmon-agent-new.sh" 2>/dev/null; then
|
||||
# Replace current script
|
||||
mv "/tmp/patchmon-agent-new.sh" "$0"
|
||||
chmod +x "$0"
|
||||
success "Agent updated successfully"
|
||||
info "Backup saved as: $0.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Get the new version number
|
||||
local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2)
|
||||
info "Updated to version: $new_version"
|
||||
|
||||
# Automatically run update to send new information to PatchMon
|
||||
info "Sending updated information to PatchMon..."
|
||||
if "$0" update; then
|
||||
success "Successfully sent updated information to PatchMon"
|
||||
else
|
||||
warning "Failed to send updated information to PatchMon (this is not critical)"
|
||||
fi
|
||||
info "Downloading latest agent from: $download_url"
|
||||
|
||||
# Clean up old backups (keep only last 3)
|
||||
ls -t "$0.backup."* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
|
||||
# Create backup of current script
|
||||
local backup_file="$0.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp "$0" "$backup_file"
|
||||
|
||||
# Download new version using API credentials
|
||||
if curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -o "/tmp/patchmon-agent-new.sh" "$download_url"; then
|
||||
# Verify the downloaded script is valid
|
||||
if bash -n "/tmp/patchmon-agent-new.sh" 2>/dev/null; then
|
||||
# Replace current script
|
||||
mv "/tmp/patchmon-agent-new.sh" "$0"
|
||||
chmod +x "$0"
|
||||
success "Agent updated successfully"
|
||||
info "Backup saved as: $backup_file"
|
||||
|
||||
# Get the new version number
|
||||
local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2)
|
||||
info "Updated to version: $new_version"
|
||||
|
||||
# Automatically run update to send new information to PatchMon
|
||||
info "Sending updated information to PatchMon..."
|
||||
if "$0" update; then
|
||||
success "Successfully sent updated information to PatchMon"
|
||||
else
|
||||
error "Downloaded script is invalid"
|
||||
rm -f "/tmp/patchmon-agent-new.sh"
|
||||
warning "Failed to send updated information to PatchMon (this is not critical)"
|
||||
fi
|
||||
else
|
||||
error "Failed to download new agent script"
|
||||
error "Downloaded script is invalid"
|
||||
rm -f "/tmp/patchmon-agent-new.sh"
|
||||
fi
|
||||
else
|
||||
error "Failed to get update information"
|
||||
error "Failed to download new agent script"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1009,7 +1090,7 @@ update_agent() {
|
||||
update_crontab() {
|
||||
load_credentials
|
||||
info "Updating crontab with current policy..."
|
||||
local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval")
|
||||
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval")
|
||||
if [[ $? -eq 0 ]]; then
|
||||
local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2)
|
||||
if [[ -n "$update_interval" ]]; then
|
||||
@@ -1024,18 +1105,27 @@ update_crontab() {
|
||||
expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1"
|
||||
fi
|
||||
|
||||
# Get current crontab
|
||||
local current_crontab=$(crontab -l 2>/dev/null | grep "patchmon-agent.sh update" | head -1)
|
||||
# Get current crontab (without patchmon entries)
|
||||
local current_crontab_without_patchmon=$(crontab -l 2>/dev/null | grep -v "/usr/local/bin/patchmon-agent.sh update" || true)
|
||||
local current_patchmon_entry=$(crontab -l 2>/dev/null | grep "/usr/local/bin/patchmon-agent.sh update" | head -1)
|
||||
|
||||
# Check if crontab needs updating
|
||||
if [[ "$current_crontab" == "$expected_crontab" ]]; then
|
||||
if [[ "$current_patchmon_entry" == "$expected_crontab" ]]; then
|
||||
info "Crontab is already up to date (interval: $update_interval minutes)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Setting update interval to $update_interval minutes"
|
||||
echo "$expected_crontab" | crontab -
|
||||
success "Crontab updated successfully"
|
||||
|
||||
# Combine existing cron (without patchmon entries) + new patchmon entry
|
||||
{
|
||||
if [[ -n "$current_crontab_without_patchmon" ]]; then
|
||||
echo "$current_crontab_without_patchmon"
|
||||
fi
|
||||
echo "$expected_crontab"
|
||||
} | crontab -
|
||||
|
||||
success "Crontab updated successfully (duplicates removed)"
|
||||
else
|
||||
error "Could not determine update interval from server"
|
||||
fi
|
||||
@@ -1147,7 +1237,7 @@ main() {
|
||||
check_root
|
||||
setup_directories
|
||||
load_config
|
||||
configure_credentials "$2" "$3"
|
||||
configure_credentials "$2" "$3" "$4"
|
||||
;;
|
||||
"test")
|
||||
check_root
|
||||
@@ -1178,6 +1268,11 @@ main() {
|
||||
load_config
|
||||
check_version
|
||||
;;
|
||||
"check-agent-update")
|
||||
setup_directories
|
||||
load_config
|
||||
check_agent_update
|
||||
;;
|
||||
"update-agent")
|
||||
check_root
|
||||
setup_directories
|
||||
@@ -1195,22 +1290,23 @@ main() {
|
||||
;;
|
||||
*)
|
||||
echo "PatchMon Agent v$AGENT_VERSION - API Credential Based"
|
||||
echo "Usage: $0 {configure|test|update|ping|config|check-version|update-agent|update-crontab|diagnostics}"
|
||||
echo "Usage: $0 {configure|test|update|ping|config|check-version|check-agent-update|update-agent|update-crontab|diagnostics}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " configure <API_ID> <API_KEY> - Configure API credentials for this host"
|
||||
echo " configure <API_ID> <API_KEY> [SERVER_URL] - Configure API credentials for this host"
|
||||
echo " test - Test API credentials connectivity"
|
||||
echo " update - Send package update information to server"
|
||||
echo " ping - Test connectivity to server"
|
||||
echo " config - Show current configuration"
|
||||
echo " check-version - Check for agent updates"
|
||||
echo " check-agent-update - Check for agent updates using timestamp comparison"
|
||||
echo " update-agent - Update agent to latest version"
|
||||
echo " update-crontab - Update crontab with current policy"
|
||||
echo " diagnostics - Show detailed system diagnostics"
|
||||
echo ""
|
||||
echo "Setup Process:"
|
||||
echo " 1. Contact your PatchMon administrator to create a host entry"
|
||||
echo " 2. Run: $0 configure <API_ID> <API_KEY> (provided by admin)"
|
||||
echo " 2. Run: $0 configure <API_ID> <API_KEY> [SERVER_URL] (provided by admin)"
|
||||
echo " 3. Run: $0 test (to verify connection)"
|
||||
echo " 4. Run: $0 update (to send initial package data)"
|
||||
echo ""
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PatchMon Agent Installation Script
|
||||
# Usage: curl -ksSL {PATCHMON_URL}/api/v1/hosts/install | bash -s -- {PATCHMON_URL} {API_ID} {API_KEY}
|
||||
# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | bash
|
||||
|
||||
set -e
|
||||
|
||||
@@ -35,10 +35,34 @@ if [[ $EUID -ne 0 ]]; then
|
||||
error "This script must be run as root (use sudo)"
|
||||
fi
|
||||
|
||||
# Clean up old files (keep only last 3 of each type)
|
||||
cleanup_old_files() {
|
||||
# Clean up old credential backups
|
||||
ls -t /etc/patchmon/credentials.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
|
||||
# Clean up old agent backups
|
||||
ls -t /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
|
||||
# Clean up old log files
|
||||
ls -t /var/log/patchmon-agent.log.old.* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
}
|
||||
|
||||
# Run cleanup at start
|
||||
cleanup_old_files
|
||||
|
||||
# Parse arguments from environment (passed via HTTP headers)
|
||||
if [[ -z "$PATCHMON_URL" ]] || [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then
|
||||
error "Missing required parameters. This script should be called via the PatchMon web interface."
|
||||
fi
|
||||
|
||||
info "🚀 Starting PatchMon Agent Installation..."
|
||||
info "📋 Server: $PATCHMON_URL"
|
||||
info "🔑 API ID: ${API_ID:0:16}..."
|
||||
|
||||
# Install required dependencies
|
||||
info "📦 Installing required dependencies..."
|
||||
|
||||
# Detect package manager and install jq
|
||||
# Detect package manager and install jq and curl
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
# Debian/Ubuntu
|
||||
apt-get update >/dev/null 2>&1
|
||||
@@ -62,76 +86,40 @@ else
|
||||
warning "Could not detect package manager. Please ensure 'jq' and 'curl' are installed manually."
|
||||
fi
|
||||
|
||||
# Verify jq installation
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
error "Failed to install 'jq'. Please install it manually: https://stedolan.github.io/jq/download/"
|
||||
# Step 1: Handle existing 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"
|
||||
|
||||
# List existing files for user awareness
|
||||
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..."
|
||||
mkdir -p /etc/patchmon
|
||||
fi
|
||||
|
||||
success "Dependencies installed successfully!"
|
||||
# Step 2: Create credentials file
|
||||
info "🔐 Creating API credentials file..."
|
||||
|
||||
# Default server URL (will be replaced by backend with configured URL)
|
||||
PATCHMON_URL="http://localhost:3001"
|
||||
|
||||
# Parse arguments
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Usage: curl -ksSL {PATCHMON_URL}/api/v1/hosts/install | bash -s -- {PATCHMON_URL} {API_ID} {API_KEY}"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo "curl -ksSL http://patchmon.example.com/api/v1/hosts/install | bash -s -- http://patchmon.example.com patchmon_1a2b3c4d abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
echo ""
|
||||
echo "Contact your PatchMon administrator to get your API credentials."
|
||||
exit 1
|
||||
# Check if credentials file already exists
|
||||
if [[ -f "/etc/patchmon/credentials" ]]; then
|
||||
warning "⚠️ Credentials file already exists at /etc/patchmon/credentials"
|
||||
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.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
|
||||
# Move existing file out of the way
|
||||
mv /etc/patchmon/credentials /etc/patchmon/credentials.backup.$(date +%Y%m%d_%H%M%S)
|
||||
info "📋 Moved existing credentials to: /etc/patchmon/credentials.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
fi
|
||||
|
||||
PATCHMON_URL="$1"
|
||||
API_ID="$2"
|
||||
API_KEY="$3"
|
||||
|
||||
# Validate inputs
|
||||
if [[ ! "$PATCHMON_URL" =~ ^https?:// ]]; then
|
||||
error "Invalid URL format. Must start with http:// or https://"
|
||||
fi
|
||||
|
||||
if [[ ! "$API_ID" =~ ^patchmon_[a-f0-9]{16}$ ]]; then
|
||||
error "Invalid API ID format. API ID should be in format: patchmon_xxxxxxxxxxxxxxxx"
|
||||
fi
|
||||
|
||||
if [[ ! "$API_KEY" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
error "Invalid API Key format. API Key should be 64 hexadecimal characters."
|
||||
fi
|
||||
|
||||
info "🚀 Installing PatchMon Agent..."
|
||||
info " Server: $PATCHMON_URL"
|
||||
info " API ID: $API_ID"
|
||||
|
||||
# Create patchmon directory
|
||||
info "📁 Creating configuration directory..."
|
||||
mkdir -p /etc/patchmon
|
||||
|
||||
# Download the agent script
|
||||
info "📥 Downloading PatchMon agent script..."
|
||||
curl -ksSL "$PATCHMON_URL/api/v1/hosts/agent/download" -o /usr/local/bin/patchmon-agent.sh
|
||||
chmod +x /usr/local/bin/patchmon-agent.sh
|
||||
|
||||
# Get the agent version from the downloaded script
|
||||
AGENT_VERSION=$(grep '^AGENT_VERSION=' /usr/local/bin/patchmon-agent.sh | cut -d'"' -f2)
|
||||
info "📋 Agent version: $AGENT_VERSION"
|
||||
|
||||
# Get expected agent version from server
|
||||
EXPECTED_VERSION=$(curl -ksv "$PATCHMON_URL/api/v1/hosts/agent/version" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4 2>/dev/null || echo "Unknown")
|
||||
if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then
|
||||
info "📋 Expected version: $EXPECTED_VERSION"
|
||||
if [[ "$AGENT_VERSION" != "$EXPECTED_VERSION" ]]; then
|
||||
warning "⚠️ Agent version mismatch! Installed: $AGENT_VERSION, Expected: $EXPECTED_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get update interval policy from server
|
||||
UPDATE_INTERVAL=$(curl -ksv "$PATCHMON_URL/api/v1/settings/update-interval" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2 2>/dev/null || echo "60")
|
||||
info "📋 Update interval: $UPDATE_INTERVAL minutes"
|
||||
|
||||
# Create credentials file
|
||||
info "🔐 Setting up API credentials..."
|
||||
cat > /etc/patchmon/credentials << EOF
|
||||
# PatchMon API Credentials
|
||||
# Generated on $(date)
|
||||
@@ -139,63 +127,141 @@ PATCHMON_URL="$PATCHMON_URL"
|
||||
API_ID="$API_ID"
|
||||
API_KEY="$API_KEY"
|
||||
EOF
|
||||
|
||||
chmod 600 /etc/patchmon/credentials
|
||||
|
||||
# Test the configuration
|
||||
info "🧪 Testing configuration..."
|
||||
# Step 3: Download the agent script using API credentials
|
||||
info "📥 Downloading PatchMon agent script..."
|
||||
|
||||
# Check if agent script already exists
|
||||
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
|
||||
warning "⚠️ Agent script already exists at /usr/local/bin/patchmon-agent.sh"
|
||||
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.sh.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
|
||||
# Move existing file out of the way
|
||||
mv /usr/local/bin/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)
|
||||
info "📋 Moved existing agent to: /usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
fi
|
||||
|
||||
curl -ks \
|
||||
-H "X-API-ID: $API_ID" \
|
||||
-H "X-API-KEY: $API_KEY" \
|
||||
"$PATCHMON_URL/api/v1/hosts/agent/download" \
|
||||
-o /usr/local/bin/patchmon-agent.sh
|
||||
|
||||
chmod +x /usr/local/bin/patchmon-agent.sh
|
||||
|
||||
# Get the agent version from the downloaded script
|
||||
AGENT_VERSION=$(grep '^AGENT_VERSION=' /usr/local/bin/patchmon-agent.sh | cut -d'"' -f2 2>/dev/null || echo "Unknown")
|
||||
info "📋 Agent version: $AGENT_VERSION"
|
||||
|
||||
# Handle existing log files
|
||||
if [[ -f "/var/log/patchmon-agent.log" ]]; then
|
||||
warning "⚠️ Existing log file found at /var/log/patchmon-agent.log"
|
||||
warning "⚠️ Rotating log file for fresh start"
|
||||
|
||||
# Rotate the log file
|
||||
mv /var/log/patchmon-agent.log /var/log/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)
|
||||
info "📋 Log file rotated to: /var/log/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)"
|
||||
fi
|
||||
|
||||
# Step 4: Test the configuration
|
||||
info "🧪 Testing API credentials and connectivity..."
|
||||
if /usr/local/bin/patchmon-agent.sh test; then
|
||||
success "Configuration test passed!"
|
||||
success "✅ API credentials are valid and server is reachable"
|
||||
else
|
||||
error "Configuration test failed. Please check your credentials."
|
||||
error "❌ Failed to validate API credentials or reach server"
|
||||
fi
|
||||
|
||||
# Send initial update
|
||||
info "📊 Sending initial package data..."
|
||||
# Step 5: Send initial data
|
||||
info "📊 Sending initial package data to server..."
|
||||
if /usr/local/bin/patchmon-agent.sh update; then
|
||||
success "Initial package data sent successfully!"
|
||||
success "✅ Initial package data sent successfully"
|
||||
else
|
||||
warning "Initial package data failed, but agent is configured. You can run 'patchmon-agent.sh update' manually."
|
||||
warning "⚠️ Failed to send initial data. You can retry later with: /usr/local/bin/patchmon-agent.sh update"
|
||||
fi
|
||||
|
||||
# Setup crontab for automatic package status updates
|
||||
info "⏰ Setting up automatic package status update every $UPDATE_INTERVAL minutes..."
|
||||
# Step 6: Get update interval policy from server and setup crontab
|
||||
info "⏰ Getting update interval policy from server..."
|
||||
UPDATE_INTERVAL=$(curl -ks \
|
||||
-H "X-API-ID: $API_ID" \
|
||||
-H "X-API-KEY: $API_KEY" \
|
||||
"$PATCHMON_URL/api/v1/settings/update-interval" | \
|
||||
grep -o '"updateInterval":[0-9]*' | cut -d':' -f2 2>/dev/null || echo "60")
|
||||
|
||||
# Check if patchmon cron job already exists
|
||||
PATCHMON_CRON_EXISTS=$(crontab -l 2>/dev/null | grep -c "patchmon-agent.sh update" || true)
|
||||
info "📋 Update interval: $UPDATE_INTERVAL minutes"
|
||||
|
||||
if [[ $PATCHMON_CRON_EXISTS -gt 0 ]]; then
|
||||
info " Existing PatchMon cron job found, removing old entry..."
|
||||
# Remove existing patchmon cron entries and preserve other entries
|
||||
(crontab -l 2>/dev/null | grep -v "patchmon-agent.sh update") | crontab -
|
||||
# Setup crontab (smart duplicate detection)
|
||||
info "📅 Setting up automated updates..."
|
||||
|
||||
# Check if PatchMon cron entries already exist
|
||||
if crontab -l 2>/dev/null | grep -q "/usr/local/bin/patchmon-agent.sh update"; then
|
||||
warning "⚠️ Existing PatchMon cron entries found"
|
||||
warning "⚠️ These will be replaced with new schedule"
|
||||
fi
|
||||
|
||||
if [[ $UPDATE_INTERVAL -eq 60 ]]; then
|
||||
# Hourly updates - safely append to existing crontab
|
||||
(crontab -l 2>/dev/null; echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | crontab -
|
||||
else
|
||||
# Custom interval updates - safely append to existing crontab
|
||||
(crontab -l 2>/dev/null; echo "*/$UPDATE_INTERVAL * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | crontab -
|
||||
fi
|
||||
# Function to setup crontab without duplicates
|
||||
setup_crontab() {
|
||||
local update_interval="$1"
|
||||
local patchmon_pattern="/usr/local/bin/patchmon-agent.sh update"
|
||||
|
||||
# Get current crontab, remove any existing patchmon entries
|
||||
local current_cron=$(crontab -l 2>/dev/null | grep -v "$patchmon_pattern" || true)
|
||||
|
||||
# Determine new cron entry
|
||||
local new_entry
|
||||
if [[ "$update_interval" -eq 60 ]]; then
|
||||
# Hourly updates - use a random minute to spread load
|
||||
local current_minute=$(date +%M)
|
||||
new_entry="$current_minute * * * * $patchmon_pattern >/dev/null 2>&1"
|
||||
info "📋 Configuring hourly updates at minute $current_minute"
|
||||
else
|
||||
# Custom interval updates
|
||||
new_entry="*/$update_interval * * * * $patchmon_pattern >/dev/null 2>&1"
|
||||
info "📋 Configuring updates every $update_interval minutes"
|
||||
fi
|
||||
|
||||
# Combine existing cron (without patchmon entries) + new entry
|
||||
{
|
||||
if [[ -n "$current_cron" ]]; then
|
||||
echo "$current_cron"
|
||||
fi
|
||||
echo "$new_entry"
|
||||
} | crontab -
|
||||
|
||||
success "✅ Crontab configured successfully (duplicates removed)"
|
||||
}
|
||||
|
||||
success "🎉 PatchMon Agent installation complete!"
|
||||
setup_crontab "$UPDATE_INTERVAL"
|
||||
|
||||
# Installation complete
|
||||
success "🎉 PatchMon Agent installation completed successfully!"
|
||||
echo ""
|
||||
echo "📋 Installation Summary:"
|
||||
echo " • Dependencies installed: jq, curl"
|
||||
echo -e "${GREEN}📋 Installation Summary:${NC}"
|
||||
echo " • Configuration directory: /etc/patchmon"
|
||||
echo " • Agent installed: /usr/local/bin/patchmon-agent.sh"
|
||||
echo " • Agent version: $AGENT_VERSION"
|
||||
if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then
|
||||
echo " • Expected version: $EXPECTED_VERSION"
|
||||
fi
|
||||
echo " • Config directory: /etc/patchmon/"
|
||||
echo " • Credentials file: /etc/patchmon/credentials"
|
||||
echo " • Status updates: Every $UPDATE_INTERVAL minutes via crontab"
|
||||
echo " • View logs: tail -f /var/log/patchmon-agent.log"
|
||||
echo ""
|
||||
echo "🔧 Manual commands:"
|
||||
echo " • Test connection: patchmon-agent.sh test"
|
||||
echo " • Send update: patchmon-agent.sh update"
|
||||
echo " • Check status: patchmon-agent.sh ping"
|
||||
echo ""
|
||||
success "Your host is now connected to PatchMon!"
|
||||
echo " • Dependencies installed: jq, curl"
|
||||
echo " • Crontab configured for automatic updates"
|
||||
echo " • API credentials configured and tested"
|
||||
|
||||
# Check for moved files and show them
|
||||
MOVED_FILES=$(ls /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.* 2>/dev/null || true)
|
||||
if [[ -n "$MOVED_FILES" ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 Files Moved for Fresh Installation:${NC}"
|
||||
echo "$MOVED_FILES" | while read -r moved_file; do
|
||||
echo " • $moved_file"
|
||||
done
|
||||
echo ""
|
||||
echo -e "${BLUE}💡 Note: Old files are automatically cleaned up (keeping last 3)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🔧 Management Commands:${NC}"
|
||||
echo " • Test connection: /usr/local/bin/patchmon-agent.sh test"
|
||||
echo " • Manual update: /usr/local/bin/patchmon-agent.sh update"
|
||||
echo " • Check status: /usr/local/bin/patchmon-agent.sh diagnostics"
|
||||
echo ""
|
||||
success "✅ Your system is now being monitored by PatchMon!"
|
||||
|
217
agents/patchmon_remove.sh
Executable file
217
agents/patchmon_remove.sh
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PatchMon Agent Removal Script
|
||||
# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/remove | bash
|
||||
# This script completely removes PatchMon from the system
|
||||
|
||||
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
|
||||
|
||||
# Functions
|
||||
error() {
|
||||
echo -e "${RED}❌ ERROR: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error "This script must be run as root (use sudo)"
|
||||
fi
|
||||
|
||||
info "🗑️ Starting PatchMon Agent Removal..."
|
||||
echo ""
|
||||
|
||||
# Step 1: Stop any running PatchMon processes
|
||||
info "🛑 Stopping PatchMon processes..."
|
||||
if pgrep -f "patchmon-agent.sh" >/dev/null; then
|
||||
warning "Found running PatchMon processes, stopping them..."
|
||||
pkill -f "patchmon-agent.sh" || true
|
||||
sleep 2
|
||||
success "PatchMon processes stopped"
|
||||
else
|
||||
info "No running PatchMon processes found"
|
||||
fi
|
||||
|
||||
# Step 2: Remove crontab entries
|
||||
info "📅 Removing PatchMon crontab entries..."
|
||||
if crontab -l 2>/dev/null | grep -q "patchmon-agent.sh"; then
|
||||
warning "Found PatchMon crontab entries, removing them..."
|
||||
crontab -l 2>/dev/null | grep -v "patchmon-agent.sh" | crontab -
|
||||
success "Crontab entries removed"
|
||||
else
|
||||
info "No PatchMon crontab entries found"
|
||||
fi
|
||||
|
||||
# Step 3: Remove agent script
|
||||
info "📄 Removing agent script..."
|
||||
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
|
||||
warning "Removing agent script: /usr/local/bin/patchmon-agent.sh"
|
||||
rm -f /usr/local/bin/patchmon-agent.sh
|
||||
success "Agent script removed"
|
||||
else
|
||||
info "Agent script not found"
|
||||
fi
|
||||
|
||||
# Step 4: Remove configuration directory and files
|
||||
info "📁 Removing configuration files..."
|
||||
if [[ -d "/etc/patchmon" ]]; then
|
||||
warning "Removing configuration directory: /etc/patchmon"
|
||||
|
||||
# Show what's being removed
|
||||
info "📋 Files in /etc/patchmon:"
|
||||
ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
|
||||
# Remove the directory
|
||||
rm -rf /etc/patchmon
|
||||
success "Configuration directory removed"
|
||||
else
|
||||
info "Configuration directory not found"
|
||||
fi
|
||||
|
||||
# Step 5: Remove log files
|
||||
info "📝 Removing log files..."
|
||||
if [[ -f "/var/log/patchmon-agent.log" ]]; then
|
||||
warning "Removing log file: /var/log/patchmon-agent.log"
|
||||
rm -f /var/log/patchmon-agent.log
|
||||
success "Log file removed"
|
||||
else
|
||||
info "Log file not found"
|
||||
fi
|
||||
|
||||
# Step 6: Clean up backup files (optional)
|
||||
info "🧹 Cleaning up backup files..."
|
||||
BACKUP_COUNT=0
|
||||
|
||||
# Count credential backups
|
||||
CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l || echo "0")
|
||||
if [[ $CRED_BACKUPS -gt 0 ]]; then
|
||||
BACKUP_COUNT=$((BACKUP_COUNT + CRED_BACKUPS))
|
||||
fi
|
||||
|
||||
# Count agent backups
|
||||
AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | wc -l || echo "0")
|
||||
if [[ $AGENT_BACKUPS -gt 0 ]]; then
|
||||
BACKUP_COUNT=$((BACKUP_COUNT + AGENT_BACKUPS))
|
||||
fi
|
||||
|
||||
# Count log backups
|
||||
LOG_BACKUPS=$(ls /var/log/patchmon-agent.log.old.* 2>/dev/null | wc -l || echo "0")
|
||||
if [[ $LOG_BACKUPS -gt 0 ]]; then
|
||||
BACKUP_COUNT=$((BACKUP_COUNT + LOG_BACKUPS))
|
||||
fi
|
||||
|
||||
if [[ $BACKUP_COUNT -gt 0 ]]; then
|
||||
warning "Found $BACKUP_COUNT backup files"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 Backup files found:${NC}"
|
||||
|
||||
# Show credential backups
|
||||
if [[ $CRED_BACKUPS -gt 0 ]]; then
|
||||
echo " Credential backups:"
|
||||
ls /etc/patchmon/credentials.backup.* 2>/dev/null | while read -r file; do
|
||||
echo " • $file"
|
||||
done
|
||||
fi
|
||||
|
||||
# Show agent backups
|
||||
if [[ $AGENT_BACKUPS -gt 0 ]]; then
|
||||
echo " Agent script backups:"
|
||||
ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | while read -r file; do
|
||||
echo " • $file"
|
||||
done
|
||||
fi
|
||||
|
||||
# Show log backups
|
||||
if [[ $LOG_BACKUPS -gt 0 ]]; then
|
||||
echo " Log file backups:"
|
||||
ls /var/log/patchmon-agent.log.old.* 2>/dev/null | while read -r file; do
|
||||
echo " • $file"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}💡 Note: Backup files are preserved for safety${NC}"
|
||||
echo -e "${BLUE}💡 You can remove them manually if not needed${NC}"
|
||||
else
|
||||
info "No backup files found"
|
||||
fi
|
||||
|
||||
# Step 7: Remove dependencies (optional)
|
||||
info "📦 Checking for PatchMon-specific dependencies..."
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
warning "jq is installed (used by PatchMon)"
|
||||
echo -e "${BLUE}💡 Note: jq may be used by other applications${NC}"
|
||||
echo -e "${BLUE}💡 Consider keeping it unless you're sure it's not needed${NC}"
|
||||
else
|
||||
info "jq not found"
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
warning "curl is installed (used by PatchMon)"
|
||||
echo -e "${BLUE}💡 Note: curl is commonly used by many applications${NC}"
|
||||
echo -e "${BLUE}💡 Consider keeping it unless you're sure it's not needed${NC}"
|
||||
else
|
||||
info "curl not found"
|
||||
fi
|
||||
|
||||
# Step 8: Final verification
|
||||
info "🔍 Verifying removal..."
|
||||
REMAINING_FILES=0
|
||||
|
||||
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
|
||||
REMAINING_FILES=$((REMAINING_FILES + 1))
|
||||
fi
|
||||
|
||||
if [[ -d "/etc/patchmon" ]]; then
|
||||
REMAINING_FILES=$((REMAINING_FILES + 1))
|
||||
fi
|
||||
|
||||
if [[ -f "/var/log/patchmon-agent.log" ]]; then
|
||||
REMAINING_FILES=$((REMAINING_FILES + 1))
|
||||
fi
|
||||
|
||||
if crontab -l 2>/dev/null | grep -q "patchmon-agent.sh"; then
|
||||
REMAINING_FILES=$((REMAINING_FILES + 1))
|
||||
fi
|
||||
|
||||
if [[ $REMAINING_FILES -eq 0 ]]; then
|
||||
success "✅ PatchMon has been completely removed from the system!"
|
||||
else
|
||||
warning "⚠️ Some PatchMon files may still remain ($REMAINING_FILES items)"
|
||||
echo -e "${BLUE}💡 You may need to remove them manually${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}📋 Removal Summary:${NC}"
|
||||
echo " • Agent script: Removed"
|
||||
echo " • Configuration files: Removed"
|
||||
echo " • Log files: Removed"
|
||||
echo " • Crontab entries: Removed"
|
||||
echo " • Running processes: Stopped"
|
||||
echo " • Backup files: Preserved (if any)"
|
||||
echo ""
|
||||
echo -e "${BLUE}🔧 Manual cleanup (if needed):${NC}"
|
||||
echo " • Remove backup files: rm /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.*"
|
||||
echo " • Remove dependencies: apt remove jq curl (if not needed by other apps)"
|
||||
echo ""
|
||||
success "🎉 PatchMon removal completed!"
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"description": "Backend API for Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "src/server.js",
|
||||
|
@@ -0,0 +1,2 @@
|
||||
-- DropTable
|
||||
DROP TABLE "agent_versions";
|
@@ -7,18 +7,6 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model agent_versions {
|
||||
id String @id
|
||||
version String @unique
|
||||
is_current Boolean @default(false)
|
||||
release_notes String?
|
||||
download_url String?
|
||||
min_server_version String?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime
|
||||
is_default Boolean @default(false)
|
||||
script_content String?
|
||||
}
|
||||
|
||||
model dashboard_preferences {
|
||||
id String @id
|
||||
|
@@ -37,7 +37,7 @@ function createPrismaClient() {
|
||||
},
|
||||
},
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
process.env.PRISMA_LOG_QUERIES === "true"
|
||||
? ["query", "info", "warn", "error"]
|
||||
: ["warn", "error"],
|
||||
errorFormat: "pretty",
|
||||
@@ -66,17 +66,21 @@ async function waitForDatabase(prisma, options = {}) {
|
||||
parseInt(process.env.PM_DB_CONN_WAIT_INTERVAL, 10) ||
|
||||
2;
|
||||
|
||||
console.log(
|
||||
`Waiting for database connection (max ${maxAttempts} attempts, ${waitInterval}s interval)...`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(
|
||||
`Waiting for database connection (max ${maxAttempts} attempts, ${waitInterval}s interval)...`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const isConnected = await checkDatabaseConnection(prisma);
|
||||
if (isConnected) {
|
||||
console.log(
|
||||
`Database connected successfully after ${attempt} attempt(s)`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(
|
||||
`Database connected successfully after ${attempt} attempt(s)`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
@@ -84,9 +88,11 @@ async function waitForDatabase(prisma, options = {}) {
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
console.log(
|
||||
`⏳ Database not ready (attempt ${attempt}/${maxAttempts}), retrying in ${waitInterval}s...`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(
|
||||
`⏳ Database not ready (attempt ${attempt}/${maxAttempts}), retrying in ${waitInterval}s...`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, waitInterval * 1000));
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ const { PrismaClient } = require("@prisma/client");
|
||||
const { body, validationResult } = require("express-validator");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const crypto = require("node:crypto");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const _path = require("node:path");
|
||||
const _fs = require("node:fs");
|
||||
const { authenticateToken, _requireAdmin } = require("../middleware/auth");
|
||||
const {
|
||||
requireManageHosts,
|
||||
@@ -14,72 +14,48 @@ const {
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Public endpoint to download the agent script
|
||||
// Secure endpoint to download the agent script (requires API authentication)
|
||||
router.get("/agent/download", async (req, res) => {
|
||||
try {
|
||||
const { version } = req.query;
|
||||
// Verify API credentials
|
||||
const apiId = req.headers["x-api-id"];
|
||||
const apiKey = req.headers["x-api-key"];
|
||||
|
||||
let agentVersion;
|
||||
|
||||
if (version) {
|
||||
// Download specific version
|
||||
agentVersion = await prisma.agent_versions.findUnique({
|
||||
where: { version },
|
||||
});
|
||||
|
||||
if (!agentVersion) {
|
||||
return res.status(404).json({ error: "Agent version not found" });
|
||||
}
|
||||
} else {
|
||||
// Download current version (latest)
|
||||
agentVersion = await prisma.agent_versions.findFirst({
|
||||
where: { is_current: true },
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
|
||||
if (!agentVersion) {
|
||||
// Fallback to default version
|
||||
agentVersion = await prisma.agent_versions.findFirst({
|
||||
where: { is_default: true },
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
}
|
||||
if (!apiId || !apiKey) {
|
||||
return res.status(401).json({ error: "API credentials required" });
|
||||
}
|
||||
|
||||
// Use script content from database if available, otherwise fallback to file
|
||||
if (agentVersion?.script_content) {
|
||||
// Convert Windows line endings to Unix line endings
|
||||
const scriptContent = agentVersion.script_content
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
res.setHeader("Content-Type", "application/x-shellscript");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="patchmon-agent-${agentVersion.version}.sh"`,
|
||||
);
|
||||
res.send(scriptContent);
|
||||
} else {
|
||||
// Fallback to file system when no database version exists or script has no content
|
||||
const agentPath = path.join(
|
||||
__dirname,
|
||||
"../../../agents/patchmon-agent.sh",
|
||||
);
|
||||
if (!fs.existsSync(agentPath)) {
|
||||
return res.status(404).json({ error: "Agent script not found" });
|
||||
}
|
||||
// Read file and convert line endings
|
||||
const scriptContent = fs
|
||||
.readFileSync(agentPath, "utf8")
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
res.setHeader("Content-Type", "application/x-shellscript");
|
||||
const version = agentVersion ? `-${agentVersion.version}` : "";
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="patchmon-agent${version}.sh"`,
|
||||
);
|
||||
res.send(scriptContent);
|
||||
// Validate API credentials
|
||||
const host = await prisma.hosts.findUnique({
|
||||
where: { api_id: apiId },
|
||||
});
|
||||
|
||||
if (!host || host.api_key !== apiKey) {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
// Serve agent script directly from file system
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh");
|
||||
|
||||
if (!fs.existsSync(agentPath)) {
|
||||
return res.status(404).json({ error: "Agent script not found" });
|
||||
}
|
||||
|
||||
// Read file and convert line endings
|
||||
const scriptContent = fs
|
||||
.readFileSync(agentPath, "utf8")
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
|
||||
res.setHeader("Content-Type", "application/x-shellscript");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
'attachment; filename="patchmon-agent.sh"',
|
||||
);
|
||||
res.send(scriptContent);
|
||||
} catch (error) {
|
||||
console.error("Agent download error:", error);
|
||||
res.status(500).json({ error: "Failed to download agent script" });
|
||||
@@ -89,21 +65,32 @@ router.get("/agent/download", async (req, res) => {
|
||||
// Version check endpoint for agents
|
||||
router.get("/agent/version", async (_req, res) => {
|
||||
try {
|
||||
const currentVersion = await prisma.agent_versions.findFirst({
|
||||
where: { is_current: true },
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
if (!currentVersion) {
|
||||
return res.status(404).json({ error: "No current agent version found" });
|
||||
// Read version directly from agent script file
|
||||
const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh");
|
||||
|
||||
if (!fs.existsSync(agentPath)) {
|
||||
return res.status(404).json({ error: "Agent script not found" });
|
||||
}
|
||||
|
||||
const scriptContent = fs.readFileSync(agentPath, "utf8");
|
||||
const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/);
|
||||
|
||||
if (!versionMatch) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Could not extract version from agent script" });
|
||||
}
|
||||
|
||||
const currentVersion = versionMatch[1];
|
||||
|
||||
res.json({
|
||||
currentVersion: currentVersion.version,
|
||||
downloadUrl:
|
||||
currentVersion.download_url || `/api/v1/hosts/agent/download`,
|
||||
releaseNotes: currentVersion.release_notes,
|
||||
minServerVersion: currentVersion.min_server_version,
|
||||
currentVersion: currentVersion,
|
||||
downloadUrl: `/api/v1/hosts/agent/download`,
|
||||
releaseNotes: `PatchMon Agent v${currentVersion}`,
|
||||
minServerVersion: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Version check error:", error);
|
||||
@@ -527,42 +514,7 @@ router.post(
|
||||
});
|
||||
});
|
||||
|
||||
// Check if agent auto-update is enabled and if there's a newer version available
|
||||
let autoUpdateResponse = null;
|
||||
try {
|
||||
const settings = await prisma.settings.findFirst();
|
||||
// Check both global agent auto-update setting AND host-specific agent auto-update setting
|
||||
if (settings?.auto_update && host.auto_update) {
|
||||
// Get current agent version from the request
|
||||
const currentAgentVersion = req.body.agentVersion;
|
||||
|
||||
if (currentAgentVersion) {
|
||||
// Get the latest agent version
|
||||
const latestAgentVersion = await prisma.agent_versions.findFirst({
|
||||
where: { is_current: true },
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
|
||||
if (
|
||||
latestAgentVersion &&
|
||||
latestAgentVersion.version !== currentAgentVersion
|
||||
) {
|
||||
// There's a newer version available
|
||||
autoUpdateResponse = {
|
||||
shouldUpdate: true,
|
||||
currentVersion: currentAgentVersion,
|
||||
latestVersion: latestAgentVersion.version,
|
||||
message:
|
||||
"A newer agent version is available. Run: /usr/local/bin/patchmon-agent.sh update-agent",
|
||||
updateCommand: "update-agent",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Agent auto-update check error:", error);
|
||||
// Don't fail the update if agent auto-update check fails
|
||||
}
|
||||
// Agent auto-update is now handled client-side by the agent itself
|
||||
|
||||
const response = {
|
||||
message: "Host updated successfully",
|
||||
@@ -571,11 +523,6 @@ router.post(
|
||||
securityUpdates: securityCount,
|
||||
};
|
||||
|
||||
// Add agent auto-update response if available
|
||||
if (autoUpdateResponse) {
|
||||
response.autoUpdate = autoUpdateResponse;
|
||||
}
|
||||
|
||||
// Check if crontab update is needed (when update interval changes)
|
||||
// This is a simple check - if the host has auto-update enabled, we'll suggest crontab update
|
||||
if (host.auto_update) {
|
||||
@@ -1103,9 +1050,26 @@ router.patch(
|
||||
},
|
||||
);
|
||||
|
||||
// Serve the installation script
|
||||
router.get("/install", async (_req, res) => {
|
||||
// Serve the installation script (requires API authentication)
|
||||
router.get("/install", async (req, res) => {
|
||||
try {
|
||||
// Verify API credentials
|
||||
const apiId = req.headers["x-api-id"];
|
||||
const apiKey = req.headers["x-api-key"];
|
||||
|
||||
if (!apiId || !apiKey) {
|
||||
return res.status(401).json({ error: "API credentials required" });
|
||||
}
|
||||
|
||||
// Validate API credentials
|
||||
const host = await prisma.hosts.findUnique({
|
||||
where: { api_id: apiId },
|
||||
});
|
||||
|
||||
if (!host || host.api_key !== apiKey) {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -1124,14 +1088,11 @@ router.get("/install", async (_req, res) => {
|
||||
script = script.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
|
||||
// Get the configured server URL from settings
|
||||
let serverUrl = "http://localhost:3001";
|
||||
try {
|
||||
const settings = await prisma.settings.findFirst();
|
||||
if (settings) {
|
||||
// Replace the default server URL in the script with the configured one
|
||||
script = script.replace(
|
||||
/PATCHMON_URL="[^"]*"/g,
|
||||
`PATCHMON_URL="${settings.server_url}"`,
|
||||
);
|
||||
if (settings?.server_url) {
|
||||
serverUrl = settings.server_url;
|
||||
}
|
||||
} catch (settingsError) {
|
||||
console.warn(
|
||||
@@ -1140,6 +1101,18 @@ router.get("/install", async (_req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Inject the API credentials and server URL into the script as environment variables
|
||||
const envVars = `#!/bin/bash
|
||||
export PATCHMON_URL="${serverUrl}"
|
||||
export API_ID="${host.api_id}"
|
||||
export API_KEY="${host.api_key}"
|
||||
|
||||
`;
|
||||
|
||||
// Remove the shebang from the original script and prepend our env vars
|
||||
script = script.replace(/^#!/, "#");
|
||||
script = envVars + script;
|
||||
|
||||
res.setHeader("Content-Type", "text/plain");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
@@ -1152,215 +1125,247 @@ router.get("/install", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== AGENT VERSION MANAGEMENT ====================
|
||||
// Serve the removal script (public endpoint - no authentication required)
|
||||
router.get("/remove", async (_req, res) => {
|
||||
try {
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
// Get all agent versions (admin only)
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"../../../agents/patchmon_remove.sh",
|
||||
);
|
||||
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
return res.status(404).json({ error: "Removal script not found" });
|
||||
}
|
||||
|
||||
// Read the script content
|
||||
const script = fs.readFileSync(scriptPath, "utf8");
|
||||
|
||||
// Set appropriate headers for script download
|
||||
res.setHeader("Content-Type", "text/plain");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
'inline; filename="patchmon_remove.sh"',
|
||||
);
|
||||
res.send(script);
|
||||
} catch (error) {
|
||||
console.error("Removal script error:", error);
|
||||
res.status(500).json({ error: "Failed to serve removal script" });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== AGENT FILE MANAGEMENT ====================
|
||||
|
||||
// Get agent file information (admin only)
|
||||
router.get(
|
||||
"/agent/versions",
|
||||
"/agent/info",
|
||||
authenticateToken,
|
||||
requireManageSettings,
|
||||
async (_req, res) => {
|
||||
try {
|
||||
const versions = await prisma.agent_versions.findMany({
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
const fs = require("node:fs").promises;
|
||||
const path = require("node:path");
|
||||
|
||||
res.json(versions);
|
||||
} catch (error) {
|
||||
console.error("Get agent versions error:", error);
|
||||
res.status(500).json({ error: "Failed to get agent versions" });
|
||||
}
|
||||
},
|
||||
);
|
||||
const agentPath = path.join(
|
||||
__dirname,
|
||||
"../../../agents/patchmon-agent.sh",
|
||||
);
|
||||
|
||||
// Create new agent version (admin only)
|
||||
router.post(
|
||||
"/agent/versions",
|
||||
authenticateToken,
|
||||
requireManageSettings,
|
||||
[
|
||||
body("version").isLength({ min: 1 }).withMessage("Version is required"),
|
||||
body("releaseNotes").optional().isString(),
|
||||
body("downloadUrl")
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage("Download URL must be valid"),
|
||||
body("minServerVersion").optional().isString(),
|
||||
body("scriptContent").optional().isString(),
|
||||
body("isDefault").optional().isBoolean(),
|
||||
],
|
||||
async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
try {
|
||||
const stats = await fs.stat(agentPath);
|
||||
const content = await fs.readFile(agentPath, "utf8");
|
||||
|
||||
const {
|
||||
version,
|
||||
releaseNotes,
|
||||
downloadUrl,
|
||||
minServerVersion,
|
||||
scriptContent,
|
||||
isDefault,
|
||||
} = req.body;
|
||||
// Extract version from agent script (look for AGENT_VERSION= line)
|
||||
const versionMatch = content.match(/^AGENT_VERSION="([^"]+)"/m);
|
||||
const version = versionMatch ? versionMatch[1] : "unknown";
|
||||
|
||||
// Check if version already exists
|
||||
const existingVersion = await prisma.agent_versions.findUnique({
|
||||
where: { version },
|
||||
});
|
||||
|
||||
if (existingVersion) {
|
||||
return res.status(400).json({ error: "Version already exists" });
|
||||
}
|
||||
|
||||
// If this is being set as default, unset other defaults
|
||||
if (isDefault) {
|
||||
await prisma.agent_versions.updateMany({
|
||||
where: { is_default: true },
|
||||
data: {
|
||||
is_default: false,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const agentVersion = await prisma.agent_versions.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
res.json({
|
||||
exists: true,
|
||||
version,
|
||||
release_notes: releaseNotes,
|
||||
download_url: downloadUrl,
|
||||
min_server_version: minServerVersion,
|
||||
script_content: scriptContent,
|
||||
is_default: isDefault || false,
|
||||
is_current: false,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json(agentVersion);
|
||||
lastModified: stats.mtime,
|
||||
size: stats.size,
|
||||
sizeFormatted: `${(stats.size / 1024).toFixed(1)} KB`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
res.json({
|
||||
exists: false,
|
||||
version: null,
|
||||
lastModified: null,
|
||||
size: 0,
|
||||
sizeFormatted: "0 KB",
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Create agent version error:", error);
|
||||
res.status(500).json({ error: "Failed to create agent version" });
|
||||
console.error("Get agent info error:", error);
|
||||
res.status(500).json({ error: "Failed to get agent information" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Set current agent version (admin only)
|
||||
router.patch(
|
||||
"/agent/versions/:versionId/current",
|
||||
// Update agent file (admin only)
|
||||
router.post(
|
||||
"/agent/upload",
|
||||
authenticateToken,
|
||||
requireManageSettings,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { versionId } = req.params;
|
||||
const { scriptContent } = req.body;
|
||||
|
||||
// First, unset all current versions
|
||||
await prisma.agent_versions.updateMany({
|
||||
where: { is_current: true },
|
||||
data: { is_current: false, updated_at: new Date() },
|
||||
});
|
||||
if (!scriptContent || typeof scriptContent !== "string") {
|
||||
return res.status(400).json({ error: "Script content is required" });
|
||||
}
|
||||
|
||||
// Set the specified version as current
|
||||
const agentVersion = await prisma.agent_versions.update({
|
||||
where: { id: versionId },
|
||||
data: { is_current: true, updated_at: new Date() },
|
||||
});
|
||||
|
||||
res.json(agentVersion);
|
||||
} catch (error) {
|
||||
console.error("Set current agent version error:", error);
|
||||
res.status(500).json({ error: "Failed to set current agent version" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Set default agent version (admin only)
|
||||
router.patch(
|
||||
"/agent/versions/:versionId/default",
|
||||
authenticateToken,
|
||||
requireManageSettings,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { versionId } = req.params;
|
||||
|
||||
// First, unset all default versions
|
||||
await prisma.agent_versions.updateMany({
|
||||
where: { is_default: true },
|
||||
data: { is_default: false, updated_at: new Date() },
|
||||
});
|
||||
|
||||
// Set the specified version as default
|
||||
const agentVersion = await prisma.agent_versions.update({
|
||||
where: { id: versionId },
|
||||
data: { is_default: true, updated_at: new Date() },
|
||||
});
|
||||
|
||||
res.json(agentVersion);
|
||||
} catch (error) {
|
||||
console.error("Set default agent version error:", error);
|
||||
res.status(500).json({ error: "Failed to set default agent version" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete agent version (admin only)
|
||||
router.delete(
|
||||
"/agent/versions/:versionId",
|
||||
authenticateToken,
|
||||
requireManageSettings,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { versionId } = req.params;
|
||||
|
||||
// Validate versionId format
|
||||
if (!versionId || versionId.length < 10) {
|
||||
// Basic validation - check if it looks like a shell script
|
||||
if (!scriptContent.trim().startsWith("#!/")) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid agent version ID format",
|
||||
details: "The provided ID does not match expected format",
|
||||
error: "Invalid script format - must start with shebang (#!/...)",
|
||||
});
|
||||
}
|
||||
|
||||
const agentVersion = await prisma.agent_versions.findUnique({
|
||||
where: { id: versionId },
|
||||
});
|
||||
const fs = require("node:fs").promises;
|
||||
const path = require("node:path");
|
||||
|
||||
if (!agentVersion) {
|
||||
return res.status(404).json({
|
||||
error: "Agent version not found",
|
||||
details: `No agent version found with ID: ${versionId}`,
|
||||
suggestion:
|
||||
"Please refresh the page to get the latest agent versions",
|
||||
});
|
||||
const agentPath = path.join(
|
||||
__dirname,
|
||||
"../../../agents/patchmon-agent.sh",
|
||||
);
|
||||
|
||||
// Create backup of existing file
|
||||
try {
|
||||
const backupPath = `${agentPath}.backup.${Date.now()}`;
|
||||
await fs.copyFile(agentPath, backupPath);
|
||||
console.log(`Created backup: ${backupPath}`);
|
||||
} catch (error) {
|
||||
// Ignore if original doesn't exist
|
||||
if (error.code !== "ENOENT") {
|
||||
console.warn("Failed to create backup:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (agentVersion.is_current) {
|
||||
return res.status(400).json({
|
||||
error: "Cannot delete current agent version",
|
||||
details: `Version ${agentVersion.version} is currently active`,
|
||||
suggestion: "Set another version as current before deleting this one",
|
||||
});
|
||||
}
|
||||
// Write new agent script
|
||||
await fs.writeFile(agentPath, scriptContent, { mode: 0o755 });
|
||||
|
||||
await prisma.agent_versions.delete({
|
||||
where: { id: versionId },
|
||||
});
|
||||
// Get updated file info
|
||||
const stats = await fs.stat(agentPath);
|
||||
const versionMatch = scriptContent.match(/^AGENT_VERSION="([^"]+)"/m);
|
||||
const version = versionMatch ? versionMatch[1] : "unknown";
|
||||
|
||||
res.json({
|
||||
message: "Agent version deleted successfully",
|
||||
deletedVersion: agentVersion.version,
|
||||
message: "Agent script updated successfully",
|
||||
version,
|
||||
lastModified: stats.mtime,
|
||||
size: stats.size,
|
||||
sizeFormatted: `${(stats.size / 1024).toFixed(1)} KB`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Delete agent version error:", error);
|
||||
res.status(500).json({
|
||||
error: "Failed to delete agent version",
|
||||
details: error.message,
|
||||
});
|
||||
console.error("Upload agent error:", error);
|
||||
res.status(500).json({ error: "Failed to update agent script" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get agent file timestamp for update checking (requires API credentials)
|
||||
router.get("/agent/timestamp", async (req, res) => {
|
||||
try {
|
||||
// Check for API credentials
|
||||
const apiId = req.headers["x-api-id"];
|
||||
const apiKey = req.headers["x-api-key"];
|
||||
|
||||
if (!apiId || !apiKey) {
|
||||
return res.status(401).json({ error: "API credentials required" });
|
||||
}
|
||||
|
||||
// Verify API credentials
|
||||
const host = await prisma.hosts.findFirst({
|
||||
where: {
|
||||
api_id: apiId,
|
||||
api_key: apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
const fs = require("node:fs").promises;
|
||||
const path = require("node:path");
|
||||
|
||||
const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh");
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(agentPath);
|
||||
const content = await fs.readFile(agentPath, "utf8");
|
||||
|
||||
// Extract version from agent script
|
||||
const versionMatch = content.match(/^AGENT_VERSION="([^"]+)"/m);
|
||||
const version = versionMatch ? versionMatch[1] : "unknown";
|
||||
|
||||
res.json({
|
||||
version,
|
||||
lastModified: stats.mtime,
|
||||
timestamp: Math.floor(stats.mtime.getTime() / 1000), // Unix timestamp
|
||||
exists: true,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
res.json({
|
||||
version: null,
|
||||
lastModified: null,
|
||||
timestamp: 0,
|
||||
exists: false,
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Get agent timestamp error:", error);
|
||||
res.status(500).json({ error: "Failed to get agent timestamp" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get settings for agent (requires API credentials)
|
||||
router.get("/settings", async (req, res) => {
|
||||
try {
|
||||
// Check for API credentials
|
||||
const apiId = req.headers["x-api-id"];
|
||||
const apiKey = req.headers["x-api-key"];
|
||||
|
||||
if (!apiId || !apiKey) {
|
||||
return res.status(401).json({ error: "API credentials required" });
|
||||
}
|
||||
|
||||
// Verify API credentials
|
||||
const host = await prisma.hosts.findFirst({
|
||||
where: {
|
||||
api_id: apiId,
|
||||
api_key: apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
const settings = await prisma.settings.findFirst();
|
||||
|
||||
// Return both global and host-specific auto-update settings
|
||||
res.json({
|
||||
auto_update: settings?.auto_update || false,
|
||||
host_auto_update: host.auto_update || false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get settings error:", error);
|
||||
res.status(500).json({ error: "Failed to get settings" });
|
||||
}
|
||||
});
|
||||
|
||||
// Update host friendly name (admin only)
|
||||
router.patch(
|
||||
"/:hostId/friendly-name",
|
||||
|
@@ -109,7 +109,9 @@ async function triggerCrontabUpdates() {
|
||||
router.get("/", authenticateToken, requireManageSettings, async (_req, res) => {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
console.log("Returning settings:", settings);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log("Returning settings:", settings);
|
||||
}
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
console.error("Settings fetch error:", error);
|
||||
@@ -239,9 +241,26 @@ router.get("/server-url", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get update interval policy for agents (public endpoint)
|
||||
router.get("/update-interval", async (_req, res) => {
|
||||
// Get update interval policy for agents (requires API authentication)
|
||||
router.get("/update-interval", async (req, res) => {
|
||||
try {
|
||||
// Verify API credentials
|
||||
const apiId = req.headers["x-api-id"];
|
||||
const apiKey = req.headers["x-api-key"];
|
||||
|
||||
if (!apiId || !apiKey) {
|
||||
return res.status(401).json({ error: "API credentials required" });
|
||||
}
|
||||
|
||||
// Validate API credentials
|
||||
const host = await prisma.hosts.findUnique({
|
||||
where: { api_id: apiId },
|
||||
});
|
||||
|
||||
if (!host || host.api_key !== apiKey) {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
const settings = await getSettings();
|
||||
res.json({
|
||||
updateInterval: settings.update_interval,
|
||||
|
@@ -14,7 +14,7 @@ const router = express.Router();
|
||||
router.get("/current", authenticateToken, async (_req, res) => {
|
||||
try {
|
||||
// Read version from package.json dynamically
|
||||
let currentVersion = "1.2.6"; // fallback
|
||||
let currentVersion = "1.2.7"; // fallback
|
||||
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
@@ -174,7 +174,7 @@ router.get(
|
||||
return res.status(400).json({ error: "Settings not found" });
|
||||
}
|
||||
|
||||
const currentVersion = "1.2.6";
|
||||
const currentVersion = "1.2.7";
|
||||
const latestVersion = settings.latest_version || currentVersion;
|
||||
const isUpdateAvailable = settings.update_available || false;
|
||||
const lastUpdateCheck = settings.last_update_check || null;
|
||||
|
@@ -30,200 +30,6 @@ const { initSettings } = require("./services/settingsService");
|
||||
// Initialize Prisma client with optimized connection pooling for multiple instances
|
||||
const prisma = createPrismaClient();
|
||||
|
||||
// Simple version comparison function for semantic versioning
|
||||
function compareVersions(version1, version2) {
|
||||
const v1Parts = version1.split(".").map(Number);
|
||||
const v2Parts = version2.split(".").map(Number);
|
||||
|
||||
// Ensure both arrays have the same length
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||
while (v1Parts.length < maxLength) v1Parts.push(0);
|
||||
while (v2Parts.length < maxLength) v2Parts.push(0);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) return true;
|
||||
if (v1Parts[i] < v2Parts[i]) return false;
|
||||
}
|
||||
|
||||
return false; // versions are equal
|
||||
}
|
||||
|
||||
// Function to check and import agent version on startup
|
||||
async function checkAndImportAgentVersion() {
|
||||
console.log("🔍 Starting agent version auto-import check...");
|
||||
|
||||
// Skip if auto-import is disabled
|
||||
if (process.env.AUTO_IMPORT_AGENT_VERSION === "false") {
|
||||
console.log("❌ Auto-import of agent version is disabled");
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.info("Auto-import of agent version is disabled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const crypto = require("node:crypto");
|
||||
|
||||
// Read and validate agent script
|
||||
const agentScriptPath = path.join(
|
||||
__dirname,
|
||||
"../../agents/patchmon-agent.sh",
|
||||
);
|
||||
console.log("📁 Agent script path:", agentScriptPath);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(agentScriptPath)) {
|
||||
console.log("❌ Agent script file not found, skipping version check");
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.warn("Agent script file not found, skipping version check");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ Agent script file found");
|
||||
|
||||
// Read the file content
|
||||
const scriptContent = fs.readFileSync(agentScriptPath, "utf8");
|
||||
|
||||
// Extract version from script content
|
||||
const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/);
|
||||
|
||||
if (!versionMatch) {
|
||||
console.log(
|
||||
"❌ Could not extract version from agent script, skipping version check",
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.warn(
|
||||
"Could not extract version from agent script, skipping version check",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const localVersion = versionMatch[1];
|
||||
console.log("📋 Local version:", localVersion);
|
||||
|
||||
// Check if this version already exists in database
|
||||
const existingVersion = await prisma.agent_versions.findUnique({
|
||||
where: { version: localVersion },
|
||||
});
|
||||
|
||||
if (existingVersion) {
|
||||
console.log(
|
||||
`✅ Agent version ${localVersion} already exists in database`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.info(`Agent version ${localVersion} already exists in database`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🆕 Agent version ${localVersion} not found in database`);
|
||||
|
||||
// Get existing versions for comparison
|
||||
const allVersions = await prisma.agent_versions.findMany({
|
||||
select: { version: true },
|
||||
orderBy: { created_at: "desc" },
|
||||
});
|
||||
|
||||
// Determine version flags and whether to proceed
|
||||
const isFirstVersion = allVersions.length === 0;
|
||||
const isNewerVersion =
|
||||
!isFirstVersion && compareVersions(localVersion, allVersions[0].version);
|
||||
|
||||
if (!isFirstVersion && !isNewerVersion) {
|
||||
console.log(
|
||||
`❌ Agent version ${localVersion} is not newer than existing versions, skipping import`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.info(
|
||||
`Agent version ${localVersion} is not newer than existing versions, skipping import`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldSetAsCurrent = isFirstVersion || isNewerVersion;
|
||||
const shouldSetAsDefault = isFirstVersion;
|
||||
|
||||
console.log(
|
||||
isFirstVersion
|
||||
? `📊 No existing versions found in database`
|
||||
: `📊 Found ${allVersions.length} existing versions in database, latest: ${allVersions[0].version}`,
|
||||
);
|
||||
|
||||
if (!isFirstVersion) {
|
||||
console.log(
|
||||
`🔄 Version comparison: ${localVersion} > ${allVersions[0].version} = ${isNewerVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear existing flags if needed
|
||||
const updatePromises = [];
|
||||
if (shouldSetAsCurrent) {
|
||||
updatePromises.push(
|
||||
prisma.agent_versions.updateMany({
|
||||
where: { is_current: true },
|
||||
data: { is_current: false },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (shouldSetAsDefault) {
|
||||
updatePromises.push(
|
||||
prisma.agent_versions.updateMany({
|
||||
where: { is_default: true },
|
||||
data: { is_default: false },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (updatePromises.length > 0) {
|
||||
await Promise.all(updatePromises);
|
||||
}
|
||||
|
||||
// Create new version
|
||||
await prisma.agent_versions.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
version: localVersion,
|
||||
release_notes: `Auto-imported on startup (${new Date().toISOString()})`,
|
||||
script_content: scriptContent,
|
||||
is_default: shouldSetAsDefault,
|
||||
is_current: shouldSetAsCurrent,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`🎉 Successfully auto-imported new agent version ${localVersion} on startup`,
|
||||
);
|
||||
if (shouldSetAsCurrent) {
|
||||
console.log(`✅ Set version ${localVersion} as current version`);
|
||||
}
|
||||
if (shouldSetAsDefault) {
|
||||
console.log(`✅ Set version ${localVersion} as default version`);
|
||||
}
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.info(
|
||||
`✅ Auto-imported new agent version ${localVersion} on startup (current: ${shouldSetAsCurrent}, default: ${shouldSetAsDefault})`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Failed to check/import agent version on startup:",
|
||||
error.message,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
logger.error(
|
||||
"Failed to check/import agent version on startup:",
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check and create default role permissions on startup
|
||||
async function checkAndCreateRolePermissions() {
|
||||
console.log("🔐 Starting role permissions auto-creation check...");
|
||||
@@ -610,8 +416,6 @@ process.on("SIGTERM", async () => {
|
||||
// Initialize dashboard preferences for all users
|
||||
async function initializeDashboardPreferences() {
|
||||
try {
|
||||
console.log("🔧 Initializing dashboard preferences for all users...");
|
||||
|
||||
// Get all users
|
||||
const users = await prisma.users.findMany({
|
||||
select: {
|
||||
@@ -628,12 +432,9 @@ async function initializeDashboardPreferences() {
|
||||
});
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log("ℹ️ No users found in database");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📊 Found ${users.length} users to initialize`);
|
||||
|
||||
let initializedCount = 0;
|
||||
let updatedCount = 0;
|
||||
|
||||
@@ -648,10 +449,6 @@ async function initializeDashboardPreferences() {
|
||||
|
||||
if (!hasPreferences) {
|
||||
// User has no preferences - create them
|
||||
console.log(
|
||||
`⚙️ Creating preferences for ${user.username} (${user.role})`,
|
||||
);
|
||||
|
||||
const preferencesData = expectedPreferences.map((pref) => ({
|
||||
id: require("uuid").v4(),
|
||||
user_id: user.id,
|
||||
@@ -667,18 +464,11 @@ async function initializeDashboardPreferences() {
|
||||
});
|
||||
|
||||
initializedCount++;
|
||||
console.log(
|
||||
` ✅ Created ${expectedCardCount} cards based on permissions`,
|
||||
);
|
||||
} else {
|
||||
// User already has preferences - check if they need updating
|
||||
const currentCardCount = user.dashboard_preferences.length;
|
||||
|
||||
if (currentCardCount !== expectedCardCount) {
|
||||
console.log(
|
||||
`🔄 Updating preferences for ${user.username} (${user.role}) - ${currentCardCount} → ${expectedCardCount} cards`,
|
||||
);
|
||||
|
||||
// Delete existing preferences
|
||||
await prisma.dashboard_preferences.deleteMany({
|
||||
where: { user_id: user.id },
|
||||
@@ -700,29 +490,16 @@ async function initializeDashboardPreferences() {
|
||||
});
|
||||
|
||||
updatedCount++;
|
||||
console.log(
|
||||
` ✅ Updated to ${expectedCardCount} cards based on permissions`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`✅ ${user.username} already has correct preferences (${currentCardCount} cards)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📋 Dashboard Preferences Initialization Complete:`);
|
||||
console.log(` - New users initialized: ${initializedCount}`);
|
||||
console.log(` - Existing users updated: ${updatedCount}`);
|
||||
console.log(
|
||||
` - Users with correct preferences: ${users.length - initializedCount - updatedCount}`,
|
||||
);
|
||||
console.log(`\n🎯 Permission-based preferences:`);
|
||||
console.log(` - Cards are now assigned based on actual user permissions`);
|
||||
console.log(
|
||||
` - Each card requires specific permissions (can_view_hosts, can_view_users, etc.)`,
|
||||
);
|
||||
console.log(` - Users only see cards they have permission to access`);
|
||||
// Only show summary if there were changes
|
||||
if (initializedCount > 0 || updatedCount > 0) {
|
||||
console.log(
|
||||
`✅ Dashboard preferences: ${initializedCount} initialized, ${updatedCount} updated`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error initializing dashboard preferences:", error);
|
||||
throw error;
|
||||
@@ -889,9 +666,6 @@ async function startServer() {
|
||||
throw initError; // Fail startup if settings can't be initialised
|
||||
}
|
||||
|
||||
// Check and import agent version on startup
|
||||
await checkAndImportAgentVersion();
|
||||
|
||||
// Check and create default role permissions on startup
|
||||
await checkAndCreateRolePermissions();
|
||||
|
||||
|
@@ -72,9 +72,11 @@ async function syncEnvironmentToSettings(currentSettings) {
|
||||
if (currentValue !== convertedValue) {
|
||||
updates[settingsField] = convertedValue;
|
||||
hasChanges = true;
|
||||
console.log(
|
||||
`Environment variable ${envVar} (${envValue}) differs from settings ${settingsField} (${currentValue}), updating...`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(
|
||||
`Environment variable ${envVar} (${envValue}) differs from settings ${settingsField} (${currentValue}), updating...`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +91,9 @@ async function syncEnvironmentToSettings(currentSettings) {
|
||||
if (currentSettings.server_url !== constructedServerUrl) {
|
||||
updates.server_url = constructedServerUrl;
|
||||
hasChanges = true;
|
||||
console.log(`Updating server_url to: ${constructedServerUrl}`);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(`Updating server_url to: ${constructedServerUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings if there are changes
|
||||
@@ -101,9 +105,11 @@ async function syncEnvironmentToSettings(currentSettings) {
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`Synced ${Object.keys(updates).length} environment variables to settings`,
|
||||
);
|
||||
if (process.env.ENABLE_LOGGING === "true") {
|
||||
console.log(
|
||||
`Synced ${Object.keys(updates).length} environment variables to settings`,
|
||||
);
|
||||
}
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
|
@@ -109,7 +109,7 @@ class UpdateScheduler {
|
||||
}
|
||||
|
||||
// Read version from package.json dynamically
|
||||
let currentVersion = "1.2.6"; // fallback
|
||||
let currentVersion = "1.2.7"; // fallback
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
if (packageJson?.version) {
|
||||
@@ -219,7 +219,7 @@ class UpdateScheduler {
|
||||
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||
|
||||
// Get current version for User-Agent
|
||||
let currentVersion = "1.2.6"; // fallback
|
||||
let currentVersion = "1.2.7"; // fallback
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
if (packageJson?.version) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@@ -1083,13 +1083,13 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
One-Line Installation
|
||||
</h4>
|
||||
<p className="text-sm text-primary-700 dark:text-primary-300 mb-3">
|
||||
Copy and run this command on the target host to automatically
|
||||
install and configure the PatchMon agent:
|
||||
Copy and run this command on the target host to securely install
|
||||
and configure the PatchMon agent:
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`curl -s ${serverUrl}/api/v1/hosts/install | bash -s -- ${serverUrl} "${host.api_id}" "${host.api_key}"`}
|
||||
value={`curl -ks ${serverUrl}/api/v1/hosts/install -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" | bash`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-primary-300 dark:border-primary-600 rounded-md bg-white dark:bg-secondary-800 text-sm font-mono text-secondary-900 dark:text-white"
|
||||
/>
|
||||
@@ -1097,7 +1097,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
`curl -s ${serverUrl}/api/v1/hosts/install | bash -s -- ${serverUrl} "${host.api_id}" "${host.api_key}"`,
|
||||
`curl -ks ${serverUrl}/api/v1/hosts/install -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" | bash`,
|
||||
)
|
||||
}
|
||||
className="btn-primary flex items-center gap-1"
|
||||
@@ -1118,21 +1118,19 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-md p-3 border border-secondary-200 dark:border-secondary-600">
|
||||
<h5 className="text-sm font-medium text-secondary-900 dark:text-white mb-2">
|
||||
1. Download Agent Script
|
||||
1. Create Configuration Directory
|
||||
</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`curl -o /tmp/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download`}
|
||||
value="sudo mkdir -p /etc/patchmon"
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-sm font-mono text-secondary-900 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
`curl -o /tmp/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download`,
|
||||
)
|
||||
copyToClipboard("sudo mkdir -p /etc/patchmon")
|
||||
}
|
||||
className="btn-secondary flex items-center gap-1"
|
||||
>
|
||||
@@ -1144,12 +1142,12 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-md p-3 border border-secondary-200 dark:border-secondary-600">
|
||||
<h5 className="text-sm font-medium text-secondary-900 dark:text-white mb-2">
|
||||
2. Install Agent
|
||||
2. Download and Install Agent Script
|
||||
</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value="sudo mkdir -p /etc/patchmon && sudo mv /tmp/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh && sudo chmod +x /usr/local/bin/patchmon-agent.sh"
|
||||
value={`curl -ko /usr/local/bin/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" && sudo chmod +x /usr/local/bin/patchmon-agent.sh`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-sm font-mono text-secondary-900 dark:text-white"
|
||||
/>
|
||||
@@ -1157,7 +1155,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
"sudo mkdir -p /etc/patchmon && sudo mv /tmp/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh && sudo chmod +x /usr/local/bin/patchmon-agent.sh",
|
||||
`curl -ko /usr/local/bin/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" && sudo chmod +x /usr/local/bin/patchmon-agent.sh`,
|
||||
)
|
||||
}
|
||||
className="btn-secondary flex items-center gap-1"
|
||||
@@ -1175,7 +1173,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`sudo /usr/local/bin/patchmon-agent.sh configure "${host.api_id}" "${host.api_key}"`}
|
||||
value={`sudo /usr/local/bin/patchmon-agent.sh configure "${host.api_id}" "${host.api_key}" "${serverUrl}"`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-sm font-mono text-secondary-900 dark:text-white"
|
||||
/>
|
||||
@@ -1183,7 +1181,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
`sudo /usr/local/bin/patchmon-agent.sh configure "${host.api_id}" "${host.api_key}"`,
|
||||
`sudo /usr/local/bin/patchmon-agent.sh configure "${host.api_id}" "${host.api_key}" "${serverUrl}"`,
|
||||
)
|
||||
}
|
||||
className="btn-secondary flex items-center gap-1"
|
||||
@@ -1253,7 +1251,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`echo "${new Date().getMinutes()} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo crontab -`}
|
||||
value={`(sudo crontab -l 2>/dev/null | grep -v "patchmon-agent.sh update"; echo "${new Date().getMinutes()} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | sudo crontab -`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-sm font-mono text-secondary-900 dark:text-white"
|
||||
/>
|
||||
@@ -1261,7 +1259,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
`echo "${new Date().getMinutes()} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo crontab -`,
|
||||
`(sudo crontab -l 2>/dev/null | grep -v "patchmon-agent.sh update"; echo "${new Date().getMinutes()} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | sudo crontab -`,
|
||||
)
|
||||
}
|
||||
className="btn-secondary flex items-center gap-1"
|
||||
|
@@ -10,8 +10,6 @@ import {
|
||||
Server,
|
||||
Settings as SettingsIcon,
|
||||
Shield,
|
||||
Star,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -19,23 +17,15 @@ import { useEffect, useId, useState } from "react";
|
||||
import UpgradeNotificationIcon from "../components/UpgradeNotificationIcon";
|
||||
import { useUpdateNotification } from "../contexts/UpdateNotificationContext";
|
||||
import {
|
||||
agentVersionAPI,
|
||||
agentFileAPI,
|
||||
permissionsAPI,
|
||||
settingsAPI,
|
||||
versionAPI,
|
||||
} from "../utils/api";
|
||||
|
||||
const Settings = () => {
|
||||
const repoPublicId = useId();
|
||||
const repoPrivateId = useId();
|
||||
const useCustomSshKeyId = useId();
|
||||
const protocolId = useId();
|
||||
const hostId = useId();
|
||||
const portId = useId();
|
||||
const updateIntervalId = useId();
|
||||
const defaultRoleId = useId();
|
||||
const githubRepoUrlId = useId();
|
||||
const sshKeyPathId = useId();
|
||||
const scriptFileId = useId();
|
||||
const scriptContentId = useId();
|
||||
const [formData, setFormData] = useState({
|
||||
serverProtocol: "http",
|
||||
serverHost: "localhost",
|
||||
@@ -70,8 +60,15 @@ const Settings = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Agent version management state
|
||||
const [showAgentVersionModal, setShowAgentVersionModal] = useState(false);
|
||||
// Agent management state
|
||||
const [_agentInfo, _setAgentInfo] = useState({
|
||||
version: null,
|
||||
lastModified: null,
|
||||
size: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
|
||||
// Version checking state
|
||||
const [versionInfo, setVersionInfo] = useState({
|
||||
@@ -157,17 +154,26 @@ const Settings = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Agent version queries and mutations
|
||||
// Agent file queries and mutations
|
||||
const {
|
||||
data: agentVersions,
|
||||
isLoading: agentVersionsLoading,
|
||||
error: agentVersionsError,
|
||||
data: agentFileInfo,
|
||||
isLoading: agentFileLoading,
|
||||
error: agentFileError,
|
||||
refetch: refetchAgentFile,
|
||||
} = useQuery({
|
||||
queryKey: ["agentVersions"],
|
||||
queryFn: () => {
|
||||
return agentVersionAPI.list().then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
queryKey: ["agentFile"],
|
||||
queryFn: () => agentFileAPI.getInfo().then((res) => res.data),
|
||||
});
|
||||
|
||||
const uploadAgentMutation = useMutation({
|
||||
mutationFn: (scriptContent) =>
|
||||
agentFileAPI.upload(scriptContent).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
refetchAgentFile();
|
||||
setShowUploadModal(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Upload agent error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -189,63 +195,6 @@ const Settings = () => {
|
||||
loadCurrentVersion();
|
||||
}, []);
|
||||
|
||||
const createAgentVersionMutation = useMutation({
|
||||
mutationFn: (data) => agentVersionAPI.create(data).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["agentVersions"]);
|
||||
setShowAgentVersionModal(false);
|
||||
setAgentVersionForm({
|
||||
version: "",
|
||||
releaseNotes: "",
|
||||
scriptContent: "",
|
||||
isDefault: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const setCurrentAgentVersionMutation = useMutation({
|
||||
mutationFn: (id) => agentVersionAPI.setCurrent(id).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["agentVersions"]);
|
||||
},
|
||||
});
|
||||
|
||||
const setDefaultAgentVersionMutation = useMutation({
|
||||
mutationFn: (id) => agentVersionAPI.setDefault(id).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["agentVersions"]);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAgentVersionMutation = useMutation({
|
||||
mutationFn: (id) => agentVersionAPI.delete(id).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["agentVersions"]);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Delete agent version error:", error);
|
||||
|
||||
// Show user-friendly error message
|
||||
if (error.response?.data?.error === "Agent version not found") {
|
||||
alert(
|
||||
"Agent version not found. Please refresh the page to get the latest data.",
|
||||
);
|
||||
// Force refresh the agent versions list
|
||||
queryClient.invalidateQueries(["agentVersions"]);
|
||||
} else if (
|
||||
error.response?.data?.error === "Cannot delete current agent version"
|
||||
) {
|
||||
alert(
|
||||
"Cannot delete the current agent version. Please set another version as current first.",
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
`Failed to delete agent version: ${error.response?.data?.error || error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Version checking functions
|
||||
const checkForUpdates = async () => {
|
||||
setVersionInfo((prev) => ({ ...prev, checking: true, error: null }));
|
||||
@@ -813,180 +762,164 @@ const Settings = () => {
|
||||
<div className="flex items-center mb-2">
|
||||
<SettingsIcon className="h-6 w-6 text-primary-600 mr-3" />
|
||||
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
Agent Version Management
|
||||
Agent File Management
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">
|
||||
Manage different versions of the PatchMon agent script
|
||||
Manage the PatchMon agent script file used for installations
|
||||
and updates
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAgentVersionModal(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Version
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const url = "/api/v1/hosts/agent/download";
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "patchmon-agent.sh";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Replace Script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version Summary */}
|
||||
{agentVersions && agentVersions.length > 0 && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 rounded-lg p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Current Version:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white font-mono">
|
||||
{agentVersions.find((v) => v.is_current)?.version ||
|
||||
"None"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Default Version:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white font-mono">
|
||||
{agentVersions.find((v) => v.is_default)?.version ||
|
||||
"None"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agentVersionsLoading ? (
|
||||
{agentFileLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
) : agentVersionsError ? (
|
||||
) : agentFileError ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-600 dark:text-red-400">
|
||||
Error loading agent versions: {agentVersionsError.message}
|
||||
Error loading agent file: {agentFileError.message}
|
||||
</p>
|
||||
</div>
|
||||
) : !agentVersions || agentVersions.length === 0 ? (
|
||||
) : !agentFileInfo?.exists ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-secondary-500 dark:text-secondary-400">
|
||||
No agent versions found
|
||||
<Code className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
|
||||
<p className="text-secondary-500 dark:text-secondary-300">
|
||||
No agent script found
|
||||
</p>
|
||||
<p className="text-sm text-secondary-400 dark:text-secondary-400 mt-2">
|
||||
Upload an agent script to get started
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{agentVersions.map((version) => (
|
||||
<div
|
||||
key={version.id}
|
||||
className="border border-secondary-200 dark:border-secondary-600 rounded-lg p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Code className="h-5 w-5 text-secondary-400 dark:text-secondary-500" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
|
||||
Version {version.version}
|
||||
</h3>
|
||||
{version.is_default && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200">
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
{version.is_current && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{version.release_notes && (
|
||||
<div className="text-sm text-secondary-500 dark:text-secondary-300 mt-1">
|
||||
<p className="line-clamp-3 whitespace-pre-line">
|
||||
{version.release_notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-secondary-400 dark:text-secondary-400 mt-1">
|
||||
Created:{" "}
|
||||
{new Date(
|
||||
version.created_at,
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const downloadUrl = `/api/v1/hosts/agent/download?version=${version.version}`;
|
||||
window.open(downloadUrl, "_blank");
|
||||
}}
|
||||
className="btn-outline text-xs flex items-center gap-1"
|
||||
>
|
||||
<Download className="h-3 w-3" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCurrentAgentVersionMutation.mutate(version.id)
|
||||
}
|
||||
disabled={
|
||||
version.is_current ||
|
||||
setCurrentAgentVersionMutation.isPending
|
||||
}
|
||||
className="btn-outline text-xs flex items-center gap-1"
|
||||
>
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Set Current
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setDefaultAgentVersionMutation.mutate(version.id)
|
||||
}
|
||||
disabled={
|
||||
version.is_default ||
|
||||
setDefaultAgentVersionMutation.isPending
|
||||
}
|
||||
className="btn-outline text-xs flex items-center gap-1"
|
||||
>
|
||||
<Star className="h-3 w-3" />
|
||||
Set Default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
deleteAgentVersionMutation.mutate(version.id)
|
||||
}
|
||||
disabled={
|
||||
version.is_default ||
|
||||
version.is_current ||
|
||||
deleteAgentVersionMutation.isPending
|
||||
}
|
||||
className="btn-danger text-xs flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete
|
||||
</button>
|
||||
<div className="space-y-6">
|
||||
{/* Agent File Info */}
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
|
||||
Current Agent Script
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Version:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white font-mono">
|
||||
{agentFileInfo.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Size:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white">
|
||||
{agentFileInfo.sizeFormatted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Modified:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white">
|
||||
{new Date(
|
||||
agentFileInfo.lastModified,
|
||||
).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Instructions */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-blue-400 dark:text-blue-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Agent Script Usage
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
<p className="mb-2">This script is used for:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>
|
||||
New agent installations via the install script
|
||||
</li>
|
||||
<li>
|
||||
Agent downloads from the
|
||||
/api/v1/hosts/agent/download endpoint
|
||||
</li>
|
||||
<li>Manual agent deployments and updates</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{agentVersions?.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<Code className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
|
||||
<p className="text-secondary-500 dark:text-secondary-300">
|
||||
No agent versions found
|
||||
</p>
|
||||
<p className="text-sm text-secondary-400 dark:text-secondary-400 mt-2">
|
||||
Add your first agent version to get started
|
||||
</p>
|
||||
{/* Uninstall Instructions */}
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-red-400 dark:text-red-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Agent Uninstall Command
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
<p className="mb-2">
|
||||
To completely remove PatchMon from a host:
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||
curl -ks {window.location.origin}
|
||||
/api/v1/hosts/remove | sudo bash
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const command = `curl -ks ${window.location.origin}/api/v1/hosts/remove | sudo bash`;
|
||||
navigator.clipboard.writeText(command);
|
||||
// You could add a toast notification here
|
||||
}}
|
||||
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-xs">
|
||||
⚠️ This will remove all PatchMon files,
|
||||
configuration, and crontab entries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1330,52 +1263,42 @@ const Settings = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Version Modal */}
|
||||
{showAgentVersionModal && (
|
||||
<AgentVersionModal
|
||||
isOpen={showAgentVersionModal}
|
||||
onClose={() => {
|
||||
setShowAgentVersionModal(false);
|
||||
setAgentVersionForm({
|
||||
version: "",
|
||||
releaseNotes: "",
|
||||
scriptContent: "",
|
||||
isDefault: false,
|
||||
});
|
||||
}}
|
||||
onSubmit={createAgentVersionMutation.mutate}
|
||||
isLoading={createAgentVersionMutation.isPending}
|
||||
{/* Agent Upload Modal */}
|
||||
{showUploadModal && (
|
||||
<AgentUploadModal
|
||||
isOpen={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
onSubmit={uploadAgentMutation.mutate}
|
||||
isLoading={uploadAgentMutation.isPending}
|
||||
error={uploadAgentMutation.error}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Agent Version Modal Component
|
||||
const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
version: "",
|
||||
releaseNotes: "",
|
||||
scriptContent: "",
|
||||
isDefault: false,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
// Agent Upload Modal Component
|
||||
const AgentUploadModal = ({ isOpen, onClose, onSubmit, isLoading, error }) => {
|
||||
const [scriptContent, setScriptContent] = useState("");
|
||||
const [uploadError, setUploadError] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setUploadError("");
|
||||
|
||||
// Basic validation
|
||||
const newErrors = {};
|
||||
if (!formData.version.trim()) newErrors.version = "Version is required";
|
||||
if (!formData.scriptContent.trim())
|
||||
newErrors.scriptContent = "Script content is required";
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
if (!scriptContent.trim()) {
|
||||
setUploadError("Script content is required");
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(formData);
|
||||
if (!scriptContent.trim().startsWith("#!/")) {
|
||||
setUploadError(
|
||||
"Script must start with a shebang (#!/bin/bash or #!/bin/sh)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(scriptContent);
|
||||
};
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
@@ -1383,10 +1306,8 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
scriptContent: event.target.result,
|
||||
}));
|
||||
setScriptContent(event.target.result);
|
||||
setUploadError("");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
@@ -1396,11 +1317,11 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
|
||||
Add Agent Version
|
||||
Replace Agent Script
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1416,112 +1337,69 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={versionId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
|
||||
htmlFor={scriptFileId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Version *
|
||||
Upload Script File
|
||||
</label>
|
||||
<input
|
||||
id={versionId}
|
||||
type="text"
|
||||
value={formData.version}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, version: e.target.value }))
|
||||
}
|
||||
className={`block w-full border rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white ${
|
||||
errors.version
|
||||
? "border-red-300 dark:border-red-500"
|
||||
: "border-secondary-300 dark:border-secondary-600"
|
||||
}`}
|
||||
placeholder="e.g., 1.0.1"
|
||||
/>
|
||||
{errors.version && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{errors.version}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor={releaseNotesId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
|
||||
>
|
||||
Release Notes
|
||||
</label>
|
||||
<textarea
|
||||
id={releaseNotesId}
|
||||
value={formData.releaseNotes}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
releaseNotes: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
className="block w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||
placeholder="Describe what's new in this version..."
|
||||
id={scriptFileId}
|
||||
type="file"
|
||||
accept=".sh"
|
||||
onChange={handleFileUpload}
|
||||
className="block w-full text-sm text-secondary-500 dark:text-secondary-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900 dark:file:text-primary-200"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
Select a .sh file to upload, or paste the script content below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor={scriptContentId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Script Content *
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".sh"
|
||||
onChange={handleFileUpload}
|
||||
className="block w-full text-sm text-secondary-500 dark:text-secondary-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900 dark:file:text-primary-200"
|
||||
/>
|
||||
<textarea
|
||||
id={scriptContentId}
|
||||
value={formData.scriptContent}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
scriptContent: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={10}
|
||||
className={`block w-full border rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm ${
|
||||
errors.scriptContent
|
||||
? "border-red-300 dark:border-red-500"
|
||||
: "border-secondary-300 dark:border-secondary-600"
|
||||
}`}
|
||||
placeholder="Paste the agent script content here..."
|
||||
/>
|
||||
{errors.scriptContent && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{errors.scriptContent}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
id={scriptContentId}
|
||||
value={scriptContent}
|
||||
onChange={(e) => {
|
||||
setScriptContent(e.target.value);
|
||||
setUploadError("");
|
||||
}}
|
||||
rows={15}
|
||||
className="block w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm"
|
||||
placeholder="#!/bin/bash # PatchMon Agent Script VERSION="1.0.0" # Your script content here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={isDefaultId}
|
||||
checked={formData.isDefault}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
isDefault: e.target.checked,
|
||||
}))
|
||||
}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-600 rounded"
|
||||
/>
|
||||
<label
|
||||
htmlFor={isDefaultId}
|
||||
className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200"
|
||||
>
|
||||
Set as default version for new installations
|
||||
</label>
|
||||
{(uploadError || error) && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
{uploadError ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3">
|
||||
<div className="flex">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-400 mr-2 mt-0.5" />
|
||||
<div className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<p className="font-medium">Important:</p>
|
||||
<ul className="mt-1 list-disc list-inside space-y-1">
|
||||
<li>This will replace the current agent script file</li>
|
||||
<li>A backup will be created automatically</li>
|
||||
<li>All new installations will use this script</li>
|
||||
<li>
|
||||
Existing agents will download this version on their next
|
||||
update
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1529,8 +1407,12 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
|
||||
<button type="button" onClick={onClose} className="btn-outline">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" disabled={isLoading} className="btn-primary">
|
||||
{isLoading ? "Creating..." : "Create Version"}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !scriptContent.trim()}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isLoading ? "Uploading..." : "Replace Script"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -114,18 +114,11 @@ export const settingsAPI = {
|
||||
getServerUrl: () => api.get("/settings/server-url"),
|
||||
};
|
||||
|
||||
// Agent Version API
|
||||
export const agentVersionAPI = {
|
||||
list: () => api.get("/hosts/agent/versions"),
|
||||
create: (data) => api.post("/hosts/agent/versions", data),
|
||||
update: (id, data) => api.put(`/hosts/agent/versions/${id}`, data),
|
||||
delete: (id) => api.delete(`/hosts/agent/versions/${id}`),
|
||||
setCurrent: (id) => api.patch(`/hosts/agent/versions/${id}/current`),
|
||||
setDefault: (id) => api.patch(`/hosts/agent/versions/${id}/default`),
|
||||
download: (version) =>
|
||||
api.get(`/hosts/agent/download${version ? `?version=${version}` : ""}`, {
|
||||
responseType: "blob",
|
||||
}),
|
||||
// Agent File Management API
|
||||
export const agentFileAPI = {
|
||||
getInfo: () => api.get("/hosts/agent/info"),
|
||||
upload: (scriptContent) => api.post("/hosts/agent/upload", { scriptContent }),
|
||||
download: () => api.get("/hosts/agent/download", { responseType: "blob" }),
|
||||
};
|
||||
|
||||
// Repository API
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "patchmon",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"license": "AGPL-3.0",
|
||||
"workspaces": [
|
||||
"backend",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"backend": {
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.1.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"frontend": {
|
||||
"name": "patchmon-frontend",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"description": "Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
101
setup.sh
101
setup.sh
@@ -34,7 +34,7 @@ BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Global variables
|
||||
SCRIPT_VERSION="self-hosting-install.sh v1.2.6-selfhost-2025-01-20-1"
|
||||
SCRIPT_VERSION="self-hosting-install.sh v1.2.7-selfhost-2025-01-20-1"
|
||||
DEFAULT_GITHUB_REPO="https://github.com/9technologygroup/patchmon.net.git"
|
||||
FQDN=""
|
||||
CUSTOM_FQDN=""
|
||||
@@ -819,7 +819,7 @@ EOF
|
||||
cat > frontend/.env << EOF
|
||||
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
|
||||
VITE_APP_NAME=PatchMon
|
||||
VITE_APP_VERSION=1.2.6
|
||||
VITE_APP_VERSION=1.2.7
|
||||
EOF
|
||||
|
||||
print_status "Environment files created"
|
||||
@@ -1191,7 +1191,7 @@ create_agent_version() {
|
||||
|
||||
# Priority 2: Use fallback version if not found
|
||||
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
|
||||
current_version="1.2.6"
|
||||
current_version="1.2.7"
|
||||
print_warning "Could not determine version, using fallback: $current_version"
|
||||
fi
|
||||
|
||||
@@ -1208,100 +1208,7 @@ create_agent_version() {
|
||||
if [ -f "$APP_DIR/agents/patchmon-agent.sh" ]; then
|
||||
cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/"
|
||||
|
||||
# Create agent version using Node.js script
|
||||
node -e "
|
||||
require('dotenv').config();
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
async function createAgentVersion() {
|
||||
let prisma;
|
||||
try {
|
||||
// Initialize Prisma client with proper error handling
|
||||
prisma = new PrismaClient();
|
||||
|
||||
// Test database connection
|
||||
await prisma.\$connect();
|
||||
console.log('✅ Database connection established');
|
||||
|
||||
// Debug: Check what models are available
|
||||
console.log('Available Prisma models:', Object.keys(prisma).filter(key => !key.startsWith('\$')));
|
||||
|
||||
// Check if agent_versions model exists
|
||||
if (!prisma.agent_versions) {
|
||||
console.log('❌ agent_versions model not found in Prisma client');
|
||||
console.log('Available models:', Object.keys(prisma).filter(key => !key.startsWith('\$')));
|
||||
console.log('Skipping agent version creation...');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = '$current_version';
|
||||
const agentScript = fs.readFileSync('./patchmon-agent.sh', 'utf8');
|
||||
|
||||
// Check if current version already exists
|
||||
const existingVersion = await prisma.agent_versions.findUnique({
|
||||
where: { version: currentVersion }
|
||||
});
|
||||
|
||||
if (existingVersion) {
|
||||
// Version exists, always update the script content during updates
|
||||
console.log('📝 Updating existing agent version ' + currentVersion + ' with latest script content...');
|
||||
await prisma.agent_versions.update({
|
||||
where: { version: currentVersion },
|
||||
data: {
|
||||
script_content: agentScript,
|
||||
is_current: true,
|
||||
is_default: true,
|
||||
release_notes: 'Version ' + currentVersion + ' - Initial Deployment\\n\\nThis version contains the latest agent script from the deployment.'
|
||||
}
|
||||
});
|
||||
console.log('✅ Agent version ' + currentVersion + ' updated successfully with latest script');
|
||||
} else {
|
||||
// Version doesn't exist, create it
|
||||
console.log('🆕 Creating new agent version ' + currentVersion + '...');
|
||||
await prisma.agent_versions.create({
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
version: currentVersion,
|
||||
script_content: agentScript,
|
||||
is_current: true,
|
||||
is_default: true,
|
||||
release_notes: 'Version ' + currentVersion + ' - Initial Deployment\\n\\nThis version contains the latest agent script from the deployment.',
|
||||
updated_at: new Date()
|
||||
}
|
||||
});
|
||||
console.log('✅ Agent version ' + currentVersion + ' created successfully');
|
||||
}
|
||||
|
||||
// Set all other versions to not be current/default
|
||||
await prisma.agent_versions.updateMany({
|
||||
where: { version: { not: currentVersion } },
|
||||
data: { is_current: false, is_default: false }
|
||||
});
|
||||
|
||||
console.log('✅ Agent version management completed successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating agent version:', error.message);
|
||||
console.error('❌ Error details:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (prisma) {
|
||||
await prisma.\$disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createAgentVersion();
|
||||
"
|
||||
|
||||
# Clean up
|
||||
rm -f "$APP_DIR/backend/patchmon-agent.sh"
|
||||
|
||||
print_status "Agent version created"
|
||||
else
|
||||
print_warning "Agent script not found, skipping agent version creation"
|
||||
fi
|
||||
print_status "Agent version management removed - using file-based approach"
|
||||
}
|
||||
|
||||
# Create deployment summary
|
||||
|
Reference in New Issue
Block a user