Merge pull request #183 from PatchMon/feature/go-agent

Improved detection logic and upgrade mechanism using intermeditary sc…
This commit is contained in:
9 Technology Group LTD
2025-10-19 18:01:34 +01:00
committed by GitHub
43 changed files with 423 additions and 653 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -80,171 +80,6 @@ load_legacy_credentials() {
fi
}
# Convert legacy credentials to YAML format
convert_credentials_to_yaml() {
local yaml_file="/etc/patchmon/credentials.yml"
info "Converting credentials to YAML format..."
cat > "$yaml_file" << EOF
api_id: "$API_ID"
api_key: "$API_KEY"
EOF
chmod 600 "$yaml_file"
success "Credentials converted to YAML format"
}
# Create Go agent configuration
create_go_agent_config() {
local config_file="/etc/patchmon/config.yml"
info "Creating Go agent configuration..."
cat > "$config_file" << EOF
patchmon_server: "$PATCHMON_SERVER"
api_version: "$API_VERSION"
credentials_file: "/etc/patchmon/credentials.yml"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
log_level: "info"
EOF
chmod 644 "$config_file"
success "Go agent configuration created"
}
# Download Go agent binary
download_go_agent() {
local arch=$(uname -m)
local goos="linux"
local goarch=""
# Map architecture
case "$arch" in
"x86_64")
goarch="amd64"
;;
"i386"|"i686")
goarch="386"
;;
"aarch64"|"arm64")
goarch="arm64"
;;
"armv7l"|"armv6l"|"armv5l")
goarch="arm"
;;
*)
error "Unsupported architecture: $arch"
;;
esac
local download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/go-binary?arch=$goarch"
local temp_dir="/etc/patchmon/tmp"
local temp_binary="$temp_dir/patchmon-agent-new"
# Create temp directory if it doesn't exist
mkdir -p "$temp_dir"
info "Downloading Go agent binary for $goos-$goarch..."
# Download with API credentials
if curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" \
-o "$temp_binary" "$download_url"; then
# Verify binary - check if it's a valid executable
chmod +x "$temp_binary"
# Check if file exists and is executable
if [[ -f "$temp_binary" ]] && [[ -x "$temp_binary" ]]; then
success "Go agent binary downloaded successfully"
echo "$temp_binary"
else
# Try to get more info about the file
local file_output=$(file "$temp_binary" 2>/dev/null || echo "unknown")
rm -f "$temp_binary" # Clean up failed download
error "Downloaded file is not executable. File info: $file_output"
fi
else
rm -f "$temp_binary" # Clean up failed download
error "Failed to download Go agent binary"
fi
}
# Install Go agent binary and service
install_go_agent() {
local temp_binary="$1"
local install_path="/usr/local/bin/patchmon-agent"
# Check if temp_binary is provided and exists
if [[ -z "$temp_binary" ]] || [[ ! -f "$temp_binary" ]]; then
error "No valid binary provided for installation"
fi
info "Installing Go agent binary..."
# Create backup of current script if it exists
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
local backup_path="/usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)"
cp "/usr/local/bin/patchmon-agent.sh" "$backup_path"
info "Backed up legacy script to: $backup_path"
fi
# Install new binary
mv "$temp_binary" "$install_path"
success "Go agent binary installed to: $install_path"
# Clean up the temporary file (it's now moved, so this is just safety)
rm -f "$temp_binary"
# Install and start systemd service
install_systemd_service
}
# Install and start systemd service
install_systemd_service() {
info "Installing systemd service..."
# Create systemd service file
cat > /etc/systemd/system/patchmon-agent.service << EOF
[Unit]
Description=PatchMon Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/patchmon-agent serve
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable service
systemctl daemon-reload
systemctl enable patchmon-agent.service
# Start the service
info "Starting PatchMon agent service..."
if systemctl start patchmon-agent.service; then
success "PatchMon agent service started successfully"
# Wait a moment for service to start
sleep 3
# Check if service is running
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon agent service is running"
else
warning "PatchMon agent service failed to start"
fi
else
warning "Failed to start PatchMon agent service"
fi
}
# Remove cron entries
remove_cron_entries() {
@@ -274,74 +109,6 @@ remove_cron_entries() {
fi
}
# Configure Go agent
configure_go_agent() {
info "Configuring Go agent..."
# Create necessary directories
mkdir -p /etc/patchmon/logs
# Check if the Go agent binary exists
if [[ ! -f "/usr/local/bin/patchmon-agent" ]]; then
warning "Go agent binary not found at /usr/local/bin/patchmon-agent"
warning "Configuration files were created manually"
return 0
fi
# Configure credentials
if ! /usr/local/bin/patchmon-agent config set-credentials "$API_ID" "$API_KEY"; then
warning "Failed to configure credentials via CLI, but files were created manually"
fi
success "Go agent configured"
}
# Test Go agent
test_go_agent() {
info "Testing Go agent..."
# Check if the Go agent binary exists
if [[ ! -f "/usr/local/bin/patchmon-agent" ]]; then
warning "Go agent binary not found - skipping tests"
return 0
fi
# Wait a bit for service to fully start
sleep 2
# Test service status
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon agent service is running"
else
warning "PatchMon agent service is not running"
fi
# Test configuration
if /usr/local/bin/patchmon-agent config show >/dev/null 2>&1; then
success "Go agent configuration test passed"
else
warning "Go agent configuration test failed, but continuing..."
fi
# Test connectivity (ping test)
info "Testing connectivity to PatchMon server..."
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "Go agent connectivity test passed - server is reachable"
else
warning "Go agent connectivity test failed - server may be unreachable"
fi
}
# Clean up temporary directory
cleanup_temp_directory() {
info "Cleaning up temporary files..."
local temp_dir="/etc/patchmon/tmp"
if [[ -d "$temp_dir" ]]; then
rm -rf "$temp_dir"
success "Temporary directory cleaned up"
fi
}
# Clean up legacy files (only after successful verification)
cleanup_legacy_files() {
@@ -376,11 +143,10 @@ show_migration_summary() {
echo "✅ Successfully migrated from bash agent to Go agent"
echo ""
echo "What was done:"
echo " • Converted credentials to YAML format"
echo " • Created Go agent configuration"
echo " • Downloaded and installed Go agent binary"
echo " • Installed and started systemd service"
echo " • Verified service is running and connected"
echo " • Loaded legacy credentials"
echo " • Downloaded and ran PatchMon install script"
echo " • Installed Go agent binary and systemd service"
echo " • Verified service is running"
echo " • Removed legacy cron entries"
echo " • Cleaned up legacy files"
echo ""
@@ -466,50 +232,53 @@ perform_migration() {
# Load legacy credentials
load_legacy_credentials
# Convert credentials
convert_credentials_to_yaml
# Set environment variables for install script
export API_ID="$API_ID"
export API_KEY="$API_KEY"
export PATCHMON_URL="$PATCHMON_SERVER"
# Create Go agent config
create_go_agent_config
# Detect architecture
local arch=$(uname -m)
local goarch=""
case "$arch" in
"x86_64") goarch="amd64" ;;
"i386"|"i686") goarch="386" ;;
"aarch64"|"arm64") goarch="arm64" ;;
"armv7l"|"armv6l"|"armv5l") goarch="arm" ;;
*) error "Unsupported architecture: $arch" ;;
esac
# Download Go agent
local temp_binary=$(download_go_agent)
info "Downloading and running PatchMon install script..."
# Install Go agent binary and service
install_go_agent "$temp_binary"
# Configure Go agent
configure_go_agent
# Test Go agent (including service and connectivity)
test_go_agent
# Only proceed with cleanup if tests pass
if systemctl is-active --quiet patchmon-agent.service && /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "Go agent is running and connected - proceeding with cleanup"
# Download and run the install script
if curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" \
"$PATCHMON_SERVER/api/v1/hosts/install?arch=$goarch" | bash; then
# Remove cron entries
remove_cron_entries
success "PatchMon Go agent installed successfully"
# Clean up legacy files
cleanup_legacy_files
# Wait a moment for service to start
sleep 3
# Clean up temporary directory
cleanup_temp_directory
# Show summary
show_migration_summary
# Run post-migration verification
post_migration_check
success "Migration completed successfully!"
# Test if the service is running
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon agent service is running"
# Clean up legacy files
remove_cron_entries
cleanup_legacy_files
# Show summary
show_migration_summary
post_migration_check
success "Migration completed successfully!"
else
warning "PatchMon agent service failed to start"
warning "Legacy files preserved for debugging"
show_migration_summary
fi
else
warning "Go agent verification failed - skipping cleanup to preserve legacy files"
warning "You may need to manually clean up legacy files after fixing the Go agent"
# Show summary anyway
show_migration_summary
error "Failed to install PatchMon Go agent"
fi
# Exit here to prevent the legacy script from continuing

View File

@@ -1,12 +1,12 @@
#!/bin/bash
# PatchMon Docker Agent Script v1.2.9
# PatchMon Docker Agent Script v1.3.0
# This script collects Docker container and image information and sends it to PatchMon
# Configuration
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
API_VERSION="v1"
AGENT_VERSION="1.2.9"
AGENT_VERSION="1.3.0"
CONFIG_FILE="/etc/patchmon/agent.conf"
CREDENTIALS_FILE="/etc/patchmon/credentials"
LOG_FILE="/var/log/patchmon-docker-agent.log"

View File

@@ -399,7 +399,7 @@ fi
curl $CURL_FLAGS \
-H "X-API-ID: $API_ID" \
-H "X-API-KEY: $API_KEY" \
"$PATCHMON_URL/api/v1/hosts/agent/download?arch=$ARCHITECTURE" \
"$PATCHMON_URL/api/v1/hosts/agent/download?arch=$ARCHITECTURE&force=binary" \
-o /usr/local/bin/patchmon-agent
chmod +x /usr/local/bin/patchmon-agent

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Database configuration for multiple instances
* Optimizes connection pooling to prevent "too many connections" errors
* Centralized Prisma Client Singleton
* Prevents multiple Prisma clients from creating connection leaks
*/
const { PrismaClient } = require("@prisma/client");
@@ -26,22 +26,43 @@ function getOptimizedDatabaseUrl() {
return url.toString();
}
// Create optimized Prisma client
function createPrismaClient() {
const optimizedUrl = getOptimizedDatabaseUrl();
// Singleton Prisma client instance
let prismaInstance = null;
return new PrismaClient({
datasources: {
db: {
url: optimizedUrl,
function getPrismaClient() {
if (!prismaInstance) {
const optimizedUrl = getOptimizedDatabaseUrl();
prismaInstance = new PrismaClient({
datasources: {
db: {
url: optimizedUrl,
},
},
},
log:
process.env.PRISMA_LOG_QUERIES === "true"
? ["query", "info", "warn", "error"]
: ["warn", "error"],
errorFormat: "pretty",
});
log:
process.env.PRISMA_LOG_QUERIES === "true"
? ["query", "info", "warn", "error"]
: ["warn", "error"],
errorFormat: "pretty",
});
// Handle graceful shutdown
process.on("beforeExit", async () => {
await prismaInstance.$disconnect();
});
process.on("SIGINT", async () => {
await prismaInstance.$disconnect();
process.exit(0);
});
process.on("SIGTERM", async () => {
await prismaInstance.$disconnect();
process.exit(0);
});
}
return prismaInstance;
}
// Connection health check
@@ -50,7 +71,7 @@ async function checkDatabaseConnection(prisma) {
await prisma.$queryRaw`SELECT 1`;
return true;
} catch (error) {
console.error("Database connection failed:", error.message);
console.error("Database connection check failed:", error.message);
return false;
}
}
@@ -121,9 +142,8 @@ async function disconnectPrisma(prisma, maxRetries = 3) {
}
module.exports = {
createPrismaClient,
getPrismaClient,
checkDatabaseConnection,
waitForDatabase,
disconnectPrisma,
getOptimizedDatabaseUrl,
};

View File

@@ -1,12 +1,12 @@
const jwt = require("jsonwebtoken");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const {
validate_session,
update_session_activity,
is_tfa_bypassed,
} = require("../utils/session_manager");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Middleware to verify JWT token with session validation
const authenticateToken = async (req, res, next) => {

View File

@@ -1,5 +1,5 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
const { getPrismaClient } = require("../config/prisma");
const prisma = getPrismaClient();
// Permission middleware factory
const requirePermission = (permission) => {

View File

@@ -1,7 +1,7 @@
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { body, validationResult } = require("express-validator");
const { authenticateToken, _requireAdmin } = require("../middleware/auth");
const {
@@ -20,7 +20,7 @@ const {
} = require("../utils/session_manager");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
/**
* Parse user agent string to extract browser and OS info

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const crypto = require("node:crypto");
const bcrypt = require("bcryptjs");
const { body, validationResult } = require("express-validator");
@@ -8,7 +8,7 @@ const { requireManageSettings } = require("../middleware/permissions");
const { v4: uuidv4 } = require("uuid");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Generate auto-enrollment token credentials
const generate_auto_enrollment_token = () => {

View File

@@ -1,11 +1,11 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const { v4: uuidv4 } = require("uuid");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Helper function to get user permissions based on role
async function getUserPermissions(userRole) {

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const moment = require("moment");
const { authenticateToken } = require("../middleware/auth");
const {
@@ -11,7 +11,7 @@ const {
const { queueManager } = require("../services/automation");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Get dashboard statistics
router.get(
@@ -61,9 +61,15 @@ router.get(
},
}),
// Total outdated packages across all hosts
prisma.host_packages.count({
where: { needs_update: true },
// Total unique packages that need updates
prisma.packages.count({
where: {
host_packages: {
some: {
needs_update: true,
},
},
},
}),
// Errored hosts (not updated within threshold based on update interval)
@@ -76,11 +82,15 @@ router.get(
},
}),
// Security updates count
prisma.host_packages.count({
// Security updates count (unique packages)
prisma.packages.count({
where: {
needs_update: true,
is_security_update: true,
host_packages: {
some: {
needs_update: true,
is_security_update: true,
},
},
},
}),

View File

@@ -1,9 +1,9 @@
const express = require("express");
const { authenticateToken } = require("../middleware/auth");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { v4: uuidv4 } = require("uuid");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
const router = express.Router();
// Helper function to convert BigInt fields to strings for JSON serialization

View File

@@ -1,9 +1,9 @@
const express = require("express");
const { createPrismaClient } = require("../config/database");
const { getPrismaClient } = require("../config/prisma");
const bcrypt = require("bcryptjs");
const router = express.Router();
const prisma = createPrismaClient();
const prisma = getPrismaClient();
// Middleware to authenticate API key
const authenticateApiKey = async (req, res, next) => {
@@ -114,9 +114,15 @@ router.get("/stats", authenticateApiKey, async (_req, res) => {
where: { status: "active" },
});
// Get total outdated packages count
const totalOutdatedPackages = await prisma.host_packages.count({
where: { needs_update: true },
// Get total unique packages that need updates (consistent with dashboard)
const totalOutdatedPackages = await prisma.packages.count({
where: {
host_packages: {
some: {
needs_update: true,
},
},
},
});
// Get total repositories count
@@ -136,11 +142,15 @@ router.get("/stats", authenticateApiKey, async (_req, res) => {
},
});
// Get security updates count
const securityUpdates = await prisma.host_packages.count({
// Get security updates count (unique packages - consistent with dashboard)
const securityUpdates = await prisma.packages.count({
where: {
needs_update: true,
is_security_update: true,
host_packages: {
some: {
needs_update: true,
is_security_update: true,
},
},
},
});

View File

@@ -1,12 +1,12 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { randomUUID } = require("node:crypto");
const { authenticateToken } = require("../middleware/auth");
const { requireManageHosts } = require("../middleware/permissions");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Get all host groups
router.get("/", authenticateToken, async (_req, res) => {

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { body, validationResult } = require("express-validator");
const { v4: uuidv4 } = require("uuid");
const crypto = require("node:crypto");
@@ -12,7 +12,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Secure endpoint to download the agent script/binary (requires API authentication)
router.get("/agent/download", async (req, res) => {
@@ -38,10 +38,14 @@ router.get("/agent/download", async (req, res) => {
const path = require("node:path");
// Check if this is a legacy agent (bash script) requesting update
// Legacy agents will have agent_version < 1.3.0
// Legacy agents will have agent_version < 1.2.9 (excluding 1.2.9 itself)
// But allow forcing binary download for fresh installations
const forceBinary = req.query.force === "binary";
const isLegacyAgent =
!forceBinary &&
host.agent_version &&
(host.agent_version.startsWith("1.2.") ||
((host.agent_version.startsWith("1.2.") &&
host.agent_version !== "1.2.9") ||
host.agent_version.startsWith("1.1.") ||
host.agent_version.startsWith("1.0."));
@@ -118,72 +122,6 @@ router.get("/agent/download", async (req, res) => {
}
});
// Endpoint to download Go agent binary (for migration script)
router.get("/agent/go-binary", 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");
// Get architecture parameter (default to amd64)
const architecture = req.query.arch || "amd64";
// Validate architecture
const validArchitectures = ["amd64", "386", "arm64", "arm"];
if (!validArchitectures.includes(architecture)) {
return res.status(400).json({
error: "Invalid architecture. Must be one of: amd64, 386, arm64, arm",
});
}
const binaryName = `patchmon-agent-linux-${architecture}`;
const binaryPath = path.join(__dirname, "../../../agents", binaryName);
if (!fs.existsSync(binaryPath)) {
return res.status(404).json({
error: `Agent binary not found for architecture: ${architecture}`,
});
}
// Set appropriate headers for binary download
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(
"Content-Disposition",
`attachment; filename="${binaryName}"`,
);
// Stream the binary file
const fileStream = fs.createReadStream(binaryPath);
fileStream.pipe(res);
fileStream.on("error", (error) => {
console.error("Binary stream error:", error);
if (!res.headersSent) {
res.status(500).json({ error: "Failed to stream agent binary" });
}
});
} catch (error) {
console.error("Go binary download error:", error);
res.status(500).json({ error: "Failed to serve Go agent binary" });
}
});
// Version check endpoint for agents
router.get("/agent/version", async (_req, res) => {
try {

View File

@@ -1,8 +1,8 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Get all packages with their update status
router.get("/", async (req, res) => {

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const {
requireManageSettings,
@@ -7,7 +7,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Get all role permissions (allow users who can manage users to view roles)
router.get(

View File

@@ -1,6 +1,6 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const {
requireViewHosts,
@@ -8,7 +8,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Get all repositories with host count
router.get("/", authenticateToken, requireViewHosts, async (_req, res) => {

View File

@@ -1,9 +1,9 @@
const express = require("express");
const router = express.Router();
const { createPrismaClient } = require("../config/database");
const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const prisma = createPrismaClient();
const prisma = getPrismaClient();
/**
* Global search endpoint

View File

@@ -1,12 +1,12 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const { requireManageSettings } = require("../middleware/permissions");
const { getSettings, updateSettings } = require("../services/settingsService");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// WebSocket broadcaster for agent policy updates (no longer used - queue-based delivery preferred)
// const { broadcastSettingsUpdate } = require("../services/agentWs");

View File

@@ -1,12 +1,12 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const speakeasy = require("speakeasy");
const QRCode = require("qrcode");
const { authenticateToken } = require("../middleware/auth");
const { body, validationResult } = require("express-validator");
const router = express.Router();
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Generate TFA secret and QR code
router.get("/setup", authenticateToken, async (req, res) => {

View File

@@ -1,9 +1,9 @@
const express = require("express");
const { authenticateToken } = require("../middleware/auth");
const { requireManageSettings } = require("../middleware/permissions");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Default GitHub repository URL
const DEFAULT_GITHUB_REPO = "https://github.com/PatchMon/PatchMon.git";
@@ -14,13 +14,13 @@ const router = express.Router();
function getCurrentVersion() {
try {
const packageJson = require("../../package.json");
return packageJson?.version || "1.2.9";
return packageJson?.version || "1.3.0";
} catch (packageError) {
console.warn(
"Could not read version from package.json, using fallback:",
packageError.message,
);
return "1.2.9";
return "1.3.0";
}
}

View File

@@ -53,8 +53,6 @@ router.get("/status/:apiId/stream", async (req, res) => {
// Validate session (same as regular auth middleware)
const validation = await validate_session(decoded.sessionId, token);
if (!validation.valid) {
console.error("[SSE] Session validation failed:", validation.reason);
console.error("[SSE] Invalid session for api_id:", apiId);
return res.status(401).json({ error: "Invalid or expired session" });
}
@@ -62,9 +60,7 @@ router.get("/status/:apiId/stream", async (req, res) => {
await update_session_activity(decoded.sessionId);
req.user = validation.user;
} catch (err) {
console.error("[SSE] JWT verification failed:", err.message);
console.error("[SSE] Invalid token for api_id:", apiId);
} catch (_err) {
return res.status(401).json({ error: "Invalid or expired token" });
}

View File

@@ -41,10 +41,10 @@ const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const cookieParser = require("cookie-parser");
const {
createPrismaClient,
getPrismaClient,
waitForDatabase,
disconnectPrisma,
} = require("./config/database");
} = require("./config/prisma");
const winston = require("winston");
// Import routes
@@ -75,7 +75,7 @@ const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
// Initialize Prisma client with optimized connection pooling for multiple instances
const prisma = createPrismaClient();
const prisma = getPrismaClient();
// Function to check and create default role permissions on startup
async function checkAndCreateRolePermissions() {

View File

@@ -52,7 +52,7 @@ class GitHubUpdateCheck {
}
// Read version from package.json
let currentVersion = "1.2.7"; // fallback
let currentVersion = "1.3.0"; // fallback
try {
const packageJson = require("../../../package.json");
if (packageJson?.version) {

View File

@@ -99,16 +99,27 @@ class QueueManager {
* Initialize all workers
*/
async initializeWorkers() {
// Optimized worker options to reduce Redis connections
const workerOptions = {
connection: redisConnection,
concurrency: 1, // Keep concurrency low to reduce connections
// Connection optimization
maxStalledCount: 1,
stalledInterval: 30000,
// Reduce connection churn
settings: {
stalledInterval: 30000,
maxStalledCount: 1,
},
};
// GitHub Update Check Worker
this.workers[QUEUE_NAMES.GITHUB_UPDATE_CHECK] = new Worker(
QUEUE_NAMES.GITHUB_UPDATE_CHECK,
this.automations[QUEUE_NAMES.GITHUB_UPDATE_CHECK].process.bind(
this.automations[QUEUE_NAMES.GITHUB_UPDATE_CHECK],
),
{
connection: redisConnection,
concurrency: 1,
},
workerOptions,
);
// Session Cleanup Worker
@@ -117,10 +128,7 @@ class QueueManager {
this.automations[QUEUE_NAMES.SESSION_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.SESSION_CLEANUP],
),
{
connection: redisConnection,
concurrency: 1,
},
workerOptions,
);
// Orphaned Repo Cleanup Worker
@@ -129,10 +137,7 @@ class QueueManager {
this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP],
),
{
connection: redisConnection,
concurrency: 1,
},
workerOptions,
);
// Orphaned Package Cleanup Worker
@@ -141,167 +146,33 @@ class QueueManager {
this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP],
),
{
connection: redisConnection,
concurrency: 1,
},
workerOptions,
);
// Agent Commands Worker
this.workers[QUEUE_NAMES.AGENT_COMMANDS] = new Worker(
QUEUE_NAMES.AGENT_COMMANDS,
async (job) => {
const { api_id, type, update_interval } = job.data || {};
console.log("[agent-commands] processing job", job.id, api_id, type);
const { api_id, type } = job.data;
console.log(`Processing agent command: ${type} for ${api_id}`);
// Log job attempt to history - use job.id as the unique identifier
const attemptNumber = job.attemptsMade || 1;
const historyId = job.id; // Single row per job, updated with each attempt
try {
if (!api_id || !type) {
throw new Error("invalid job data");
}
// Find host by api_id
const host = await prisma.hosts.findUnique({
where: { api_id },
select: { id: true },
});
// Ensure agent is connected; if not, retry later
if (!agentWs.isConnected(api_id)) {
const error = new Error("agent not connected");
// Log failed attempt
await prisma.job_history.upsert({
where: { id: historyId },
create: {
id: historyId,
job_id: job.id,
queue_name: QUEUE_NAMES.AGENT_COMMANDS,
job_name: type,
host_id: host?.id,
api_id,
status: "failed",
attempt_number: attemptNumber,
error_message: error.message,
created_at: new Date(),
updated_at: new Date(),
},
update: {
status: "failed",
attempt_number: attemptNumber,
error_message: error.message,
updated_at: new Date(),
},
});
console.log(
"[agent-commands] agent not connected, will retry",
api_id,
);
throw error;
}
// Process the command
let result;
if (type === "settings_update") {
agentWs.pushSettingsUpdate(api_id, update_interval);
console.log(
"[agent-commands] delivered settings_update",
api_id,
update_interval,
);
result = { delivered: true, update_interval };
} else if (type === "report_now") {
agentWs.pushReportNow(api_id);
console.log("[agent-commands] delivered report_now", api_id);
result = { delivered: true };
} else {
throw new Error("unsupported agent command");
}
// Log successful completion
await prisma.job_history.upsert({
where: { id: historyId },
create: {
id: historyId,
job_id: job.id,
queue_name: QUEUE_NAMES.AGENT_COMMANDS,
job_name: type,
host_id: host?.id,
api_id,
status: "completed",
attempt_number: attemptNumber,
output: result,
created_at: new Date(),
updated_at: new Date(),
completed_at: new Date(),
},
update: {
status: "completed",
attempt_number: attemptNumber,
output: result,
error_message: null,
updated_at: new Date(),
completed_at: new Date(),
},
});
return result;
} catch (error) {
// Log error to history (if not already logged above)
if (error.message !== "agent not connected") {
const host = await prisma.hosts
.findUnique({
where: { api_id },
select: { id: true },
})
.catch(() => null);
await prisma.job_history
.upsert({
where: { id: historyId },
create: {
id: historyId,
job_id: job.id,
queue_name: QUEUE_NAMES.AGENT_COMMANDS,
job_name: type || "unknown",
host_id: host?.id,
api_id,
status: "failed",
attempt_number: attemptNumber,
error_message: error.message,
created_at: new Date(),
updated_at: new Date(),
},
update: {
status: "failed",
attempt_number: attemptNumber,
error_message: error.message,
updated_at: new Date(),
},
})
.catch((err) =>
console.error("[agent-commands] failed to log error:", err),
);
}
throw error;
// Send command via WebSocket based on type
if (type === "report_now") {
agentWs.pushReportNow(api_id);
} else if (type === "settings_update") {
// For settings update, we need additional data
const { update_interval } = job.data;
agentWs.pushSettingsUpdate(api_id, update_interval);
} else {
console.error(`Unknown agent command type: ${type}`);
}
},
{
connection: redisConnection,
concurrency: 10,
},
workerOptions,
);
// Add error handling for all workers
Object.values(this.workers).forEach((worker) => {
worker.on("error", (error) => {
console.error("Worker error:", error);
});
});
console.log("✅ All workers initialized");
console.log(
"✅ All workers initialized with optimized connection settings",
);
}
/**

View File

@@ -1,5 +1,5 @@
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../../../config/prisma");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
module.exports = { prisma };

View File

@@ -1,17 +1,56 @@
const IORedis = require("ioredis");
// Redis connection configuration
// Redis connection configuration with connection pooling
const redisConnection = {
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
username: process.env.REDIS_USER || undefined,
db: parseInt(process.env.REDIS_DB, 10) || 0,
// Connection pooling settings
lazyConnect: true,
keepAlive: 30000,
connectTimeout: 30000, // Increased from 10s to 30s
commandTimeout: 30000, // Increased from 5s to 30s
enableReadyCheck: false,
// Reduce connection churn
family: 4, // Force IPv4
// Retry settings
retryDelayOnClusterDown: 300,
retryDelayOnFailover: 100,
maxRetriesPerRequest: null, // BullMQ requires this to be null
// Connection pool settings
maxLoadingTimeout: 30000,
};
// Create Redis connection
const redis = new IORedis(redisConnection);
// Create Redis connection with singleton pattern
let redisInstance = null;
module.exports = { redis, redisConnection };
function getRedisConnection() {
if (!redisInstance) {
redisInstance = new IORedis(redisConnection);
// Handle graceful shutdown
process.on("beforeExit", async () => {
await redisInstance.quit();
});
process.on("SIGINT", async () => {
await redisInstance.quit();
process.exit(0);
});
process.on("SIGTERM", async () => {
await redisInstance.quit();
process.exit(0);
});
}
return redisInstance;
}
module.exports = {
redis: getRedisConnection(),
redisConnection,
getRedisConnection,
};

View File

@@ -33,7 +33,7 @@ async function checkPublicRepo(owner, repo) {
try {
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
let currentVersion = "1.2.7"; // fallback
let currentVersion = "1.3.0"; // fallback
try {
const packageJson = require("../../../package.json");
if (packageJson?.version) {

View File

@@ -1,7 +1,7 @@
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const { v4: uuidv4 } = require("uuid");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
// Cached settings instance
let cachedSettings = null;

View File

@@ -1,8 +1,8 @@
const jwt = require("jsonwebtoken");
const crypto = require("node:crypto");
const { PrismaClient } = require("@prisma/client");
const { getPrismaClient } = require("../config/prisma");
const prisma = new PrismaClient();
const prisma = getPrismaClient();
/**
* Session Manager - Handles secure session management with inactivity timeout

View File

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

View File

@@ -117,7 +117,7 @@ const Layout = ({ children }) => {
name: "Automation",
href: "/automation",
icon: RefreshCw,
beta: true,
new: true,
});
if (canViewReports()) {
@@ -440,6 +440,11 @@ const Layout = ({ children }) => {
Beta
</span>
)}
{subItem.new && (
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-200 px-1.5 py-0.5 rounded font-medium">
New
</span>
)}
</span>
</Link>
)}
@@ -716,6 +721,11 @@ const Layout = ({ children }) => {
Beta
</span>
)}
{subItem.new && (
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-200 px-1.5 py-0.5 rounded font-medium">
New
</span>
)}
{subItem.showUpgradeIcon && (
<UpgradeNotificationIcon className="h-3 w-3" />
)}

View File

@@ -52,6 +52,7 @@ const HostDetail = () => {
const [historyPage, setHistoryPage] = useState(0);
const [historyLimit] = useState(10);
const [notes, setNotes] = useState("");
const [notesMessage, setNotesMessage] = useState({ text: "", type: "" });
const {
data: host,
@@ -212,6 +213,17 @@ const HostDetail = () => {
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
setNotesMessage({ text: "Notes saved successfully!", type: "success" });
// Clear message after 3 seconds
setTimeout(() => setNotesMessage({ text: "", type: "" }), 3000);
},
onError: (error) => {
setNotesMessage({
text: error.response?.data?.error || "Failed to save notes",
type: "error",
});
// Clear message after 5 seconds for errors
setTimeout(() => setNotesMessage({ text: "", type: "" }), 5000);
},
});
@@ -1233,6 +1245,37 @@ const HostDetail = () => {
Host Notes
</h3>
</div>
{/* Success/Error Message */}
{notesMessage.text && (
<div
className={`rounded-md p-4 ${
notesMessage.type === "success"
? "bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700"
: "bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700"
}`}
>
<div className="flex">
{notesMessage.type === "success" ? (
<CheckCircle className="h-5 w-5 text-green-400 dark:text-green-300" />
) : (
<AlertCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
)}
<div className="ml-3">
<p
className={`text-sm font-medium ${
notesMessage.type === "success"
? "text-green-800 dark:text-green-200"
: "text-red-800 dark:text-red-200"
}`}
>
{notesMessage.text}
</p>
</div>
</div>
</div>
)}
<div className="bg-secondary-50 dark:bg-secondary-700 rounded-lg p-4">
<textarea
value={notes}

View File

@@ -153,6 +153,14 @@ const Packages = () => {
}));
}, [packagesResponse]);
// Fetch dashboard stats for card counts (consistent with homepage)
const { data: dashboardStats } = useQuery({
queryKey: ["dashboardStats"],
queryFn: () => dashboardAPI.getStats().then((res) => res.data),
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
refetchOnWindowFocus: false, // Don't refetch when window regains focus
});
// Fetch hosts data to get total packages count
const { data: hosts } = useQuery({
queryKey: ["hosts"],
@@ -446,25 +454,21 @@ const Packages = () => {
const uniquePackageHostsCount = uniquePackageHosts.size;
// Calculate total packages installed
// When filtering by host, count each package once (since it can only be installed once per host)
// When not filtering, sum up all installations across all hosts
const totalPackagesCount =
hostFilter && hostFilter !== "all"
? packages?.length || 0
: packages?.reduce(
(sum, pkg) => sum + (pkg.stats?.totalInstalls || 0),
0,
) || 0;
// Show unique package count (same as table) for consistency
const totalPackagesCount = packages?.length || 0;
// Calculate outdated packages
const outdatedPackagesCount =
packages?.filter((pkg) => (pkg.stats?.updatesNeeded || 0) > 0).length || 0;
// Calculate security updates
const securityUpdatesCount =
packages?.filter((pkg) => (pkg.stats?.securityUpdates || 0) > 0).length ||
// Calculate total installations across all hosts
const totalInstallationsCount =
packages?.reduce((sum, pkg) => sum + (pkg.stats?.totalInstalls || 0), 0) ||
0;
// Use dashboard stats for outdated packages count (consistent with homepage)
const outdatedPackagesCount =
dashboardStats?.cards?.totalOutdatedPackages || 0;
// Use dashboard stats for security updates count (consistent with homepage)
const securityUpdatesCount = dashboardStats?.cards?.securityUpdates || 0;
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
@@ -529,13 +533,13 @@ const Packages = () => {
</div>
{/* Summary Stats */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6 flex-shrink-0">
<div className="grid grid-cols-1 sm:grid-cols-5 gap-4 mb-6 flex-shrink-0">
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Package className="h-5 w-5 text-primary-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">
Total Installed
Total Packages
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{totalPackagesCount}
@@ -544,6 +548,20 @@ const Packages = () => {
</div>
</div>
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Package className="h-5 w-5 text-blue-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">
Total Installations
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{totalInstallationsCount}
</p>
</div>
</div>
</div>
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Package className="h-5 w-5 text-warning-600 mr-2" />

8
package-lock.json generated
View File

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

View File

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

View File

@@ -34,7 +34,7 @@ BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
SCRIPT_VERSION="self-hosting-install.sh v1.2.9-selfhost-2025-10-11-1"
SCRIPT_VERSION="self-hosting-install.sh v1.3.0-selfhost-2025-10-19-1"
DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git"
FQDN=""
CUSTOM_FQDN=""
@@ -665,47 +665,79 @@ configure_redis() {
print_info "Created Redis configuration backup"
fi
# Configure Redis with admin password first
print_info "Setting Redis admin password"
# Configure Redis with ACL authentication
print_info "Configuring Redis with ACL authentication"
# Add password configuration to redis.conf
if ! grep -q "^requirepass" /etc/redis/redis.conf; then
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
print_status "Added admin password configuration to Redis"
else
# Update existing password
sed -i "s/^requirepass.*/requirepass $REDIS_PASSWORD/" /etc/redis/redis.conf
print_status "Updated Redis admin password configuration"
# Ensure ACL file exists and is configured
if [ ! -f /etc/redis/users.acl ]; then
touch /etc/redis/users.acl
chown redis:redis /etc/redis/users.acl
chmod 640 /etc/redis/users.acl
print_status "Created Redis ACL file"
fi
# Restart Redis to apply admin password
print_info "Restarting Redis to apply admin password configuration..."
# Configure ACL file in redis.conf
if ! grep -q "^aclfile" /etc/redis/redis.conf; then
echo "aclfile /etc/redis/users.acl" >> /etc/redis/redis.conf
print_status "Added ACL file configuration to Redis"
fi
# Remove any requirepass configuration (incompatible with ACL)
if grep -q "^requirepass" /etc/redis/redis.conf; then
sed -i 's/^requirepass.*/# &/' /etc/redis/redis.conf
print_status "Disabled requirepass (incompatible with ACL)"
fi
# Remove any user definitions from redis.conf (should be in ACL file)
if grep -q "^user " /etc/redis/redis.conf; then
sed -i '/^user /d' /etc/redis/redis.conf
print_status "Removed user definitions from redis.conf"
fi
# Create admin user in ACL file if it doesn't exist
if ! grep -q "^user admin" /etc/redis/users.acl; then
echo "user admin on sanitize-payload >$REDIS_PASSWORD ~* &* +@all" >> /etc/redis/users.acl
print_status "Added admin user to ACL file"
fi
# Restart Redis to apply ACL configuration
print_info "Restarting Redis to apply ACL configuration..."
systemctl restart redis-server
# Wait for Redis to start
sleep 3
# Test admin connection
if ! redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
print_error "Failed to configure Redis admin password"
if ! redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
print_error "Failed to configure Redis ACL authentication"
return 1
fi
print_status "Redis admin password configuration successful"
print_status "Redis ACL authentication configuration successful"
# Create Redis user with ACL
print_info "Creating Redis ACL user: $REDIS_USER"
# Create user with password and permissions - capture output for error handling
local acl_result
acl_result=$(redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ACL SETUSER "$REDIS_USER" on ">${REDIS_USER_PASSWORD}" ~* +@all 2>&1)
acl_result=$(redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SETUSER "$REDIS_USER" on ">${REDIS_USER_PASSWORD}" ~* +@all 2>&1)
if [ "$acl_result" = "OK" ]; then
print_status "Redis user '$REDIS_USER' created successfully"
# Save ACL users to file to persist across restarts
local save_result
save_result=$(redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SAVE 2>&1)
if [ "$save_result" = "OK" ]; then
print_status "Redis ACL users saved to file"
else
print_warning "Failed to save ACL users to file: $save_result"
fi
# Verify user was actually created
local verify_result
verify_result=$(redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ACL GETUSER "$REDIS_USER" 2>&1)
verify_result=$(redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL GETUSER "$REDIS_USER" 2>&1)
if [ "$verify_result" = "(nil)" ]; then
print_error "User creation reported OK but user does not exist"
@@ -1043,7 +1075,7 @@ EOF
cat > frontend/.env << EOF
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
VITE_APP_NAME=PatchMon
VITE_APP_VERSION=1.2.9
VITE_APP_VERSION=1.3.0
EOF
print_status "Environment files created"
@@ -1415,7 +1447,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.9"
current_version="1.3.0"
print_warning "Could not determine version, using fallback: $current_version"
fi

View File

@@ -15,7 +15,7 @@ NC='\033[0m' # No Color
# Default Redis connection details
REDIS_HOST=${REDIS_HOST:-"localhost"}
REDIS_PORT=${REDIS_PORT:-6379}
REDIS_ADMIN_PASSWORD=${REDIS_ADMIN_PASSWORD:-""}
REDIS_ADMIN_PASSWORD=${REDIS_ADMIN_PASSWORD:-"YOURREDISPASSHERE"}
echo -e "${BLUE}🔧 PatchMon Redis Setup${NC}"
echo "=================================="
@@ -31,12 +31,12 @@ check_redis_connection() {
echo -e "${YELLOW}📡 Checking Redis connection...${NC}"
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
# With password - use ACL admin user
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
echo -e "${GREEN}✅ Redis connection successful${NC}"
return 0
else
echo -e "${RED}❌ Cannot connect to Redis with password${NC}"
echo -e "${RED}❌ Cannot connect to Redis with ACL admin user${NC}"
echo "Please ensure Redis is running and the admin password is correct"
return 1
fi
@@ -67,8 +67,8 @@ find_next_db() {
local redis_output
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
redis_output=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" DBSIZE 2>&1)
# With password - use ACL admin user
redis_output=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" DBSIZE 2>&1)
else
# Without password
redis_output=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$db_num" DBSIZE 2>&1)
@@ -126,8 +126,8 @@ create_redis_user() {
# Note: >password syntax is for Redis ACL, we need to properly escape it
local result
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL SETUSER "$username" on ">${password}" ~* +@all 2>&1)
# With password - use ACL admin user
result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL SETUSER "$username" on ">${password}" ~* +@all 2>&1)
else
# Without password
result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL SETUSER "$username" on ">${password}" ~* +@all 2>&1)
@@ -136,9 +136,23 @@ create_redis_user() {
if [ $? -eq 0 ] && [ "$result" = "OK" ]; then
echo -e "${GREEN}✅ Redis user '$username' created successfully for database $db_num${NC}"
# Save ACL users to file to persist across restarts
local save_result
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
save_result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL SAVE 2>&1)
else
save_result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL SAVE 2>&1)
fi
if [ "$save_result" = "OK" ]; then
echo -e "${GREEN}✅ Redis ACL users saved to file${NC}"
else
echo -e "${YELLOW}⚠️ Failed to save ACL users to file: $save_result${NC}"
fi
# Verify user was created
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
local verify=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL GETUSER "$username" 2>&1)
local verify=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL GETUSER "$username" 2>&1)
else
local verify=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL GETUSER "$username" 2>&1)
fi
@@ -180,7 +194,7 @@ mark_database_in_use() {
echo -e "${YELLOW}📝 Marking database as in-use...${NC}"
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user admin --pass "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
else
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$db_num" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
fi