mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2026-01-05 12:39:35 -06:00
Merge pull request #183 from PatchMon/feature/go-agent
Improved detection logic and upgrade mechanism using intermeditary sc…
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const { getPrismaClient } = require("../../../config/prisma");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const prisma = getPrismaClient();
|
||||
|
||||
module.exports = { prisma };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.9",
|
||||
"version": "1.3.0",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
72
setup.sh
72
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.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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user