feat: enhance runtime configuration and system settings management
Improves configuration handling and system settings APIs to support v4.24.0 features including runtime logging controls, adaptive polling configuration, and enhanced config export/persistence. Changes: - Add config override system for discovery service - Enhance system settings API with runtime logging controls - Improve config persistence and export functionality - Update security setup handling - Refine monitoring and discovery service integration These changes provide the backend support for the configuration features documented in the v4.24.0 release.
This commit is contained in:
parent
c91b7874ac
commit
5ebb32ce10
10 changed files with 677 additions and 101 deletions
|
|
@ -28,13 +28,16 @@ import (
|
|||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||
discoveryinternal "github.com/rcourtman/pulse-go-rewrite/internal/discovery"
|
||||
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const minProxyReadyVersion = "4.24.0"
|
||||
|
||||
// SetupCode represents a one-time setup code for secure node registration
|
||||
type SetupCode struct {
|
||||
ExpiresAt time.Time
|
||||
|
|
@ -2455,29 +2458,28 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
|||
persistedSettings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to load persisted system settings")
|
||||
persistedSettings = &config.SystemSettings{}
|
||||
persistedSettings = config.DefaultSystemSettings()
|
||||
}
|
||||
if persistedSettings == nil {
|
||||
persistedSettings = config.DefaultSystemSettings()
|
||||
}
|
||||
|
||||
// Get current values from running config
|
||||
settings := config.SystemSettings{
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
PBSPollingInterval: int(h.config.PBSPollingInterval.Seconds()),
|
||||
BackupPollingInterval: int(h.config.BackupPollingInterval.Seconds()),
|
||||
BackendPort: h.config.BackendPort,
|
||||
FrontendPort: h.config.FrontendPort,
|
||||
AllowedOrigins: h.config.AllowedOrigins,
|
||||
ConnectionTimeout: int(h.config.ConnectionTimeout.Seconds()),
|
||||
UpdateChannel: h.config.UpdateChannel,
|
||||
AutoUpdateEnabled: h.config.AutoUpdateEnabled,
|
||||
AutoUpdateCheckInterval: int(h.config.AutoUpdateCheckInterval.Hours()),
|
||||
AutoUpdateTime: h.config.AutoUpdateTime,
|
||||
LogLevel: h.config.LogLevel, // Include log level
|
||||
Theme: persistedSettings.Theme, // Include theme from persisted settings
|
||||
AllowEmbedding: persistedSettings.AllowEmbedding, // Include allowEmbedding from persisted settings
|
||||
AllowedEmbedOrigins: persistedSettings.AllowedEmbedOrigins, // Include allowedEmbedOrigins from persisted settings
|
||||
DiscoveryEnabled: persistedSettings.DiscoveryEnabled, // Include discoveryEnabled from persisted settings
|
||||
DiscoverySubnet: persistedSettings.DiscoverySubnet, // Include discoverySubnet from persisted settings
|
||||
}
|
||||
settings := *persistedSettings
|
||||
settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds())
|
||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||
settings.BackendPort = h.config.BackendPort
|
||||
settings.FrontendPort = h.config.FrontendPort
|
||||
settings.AllowedOrigins = h.config.AllowedOrigins
|
||||
settings.ConnectionTimeout = int(h.config.ConnectionTimeout.Seconds())
|
||||
settings.UpdateChannel = h.config.UpdateChannel
|
||||
settings.AutoUpdateEnabled = h.config.AutoUpdateEnabled
|
||||
settings.AutoUpdateCheckInterval = int(h.config.AutoUpdateCheckInterval.Hours())
|
||||
settings.AutoUpdateTime = h.config.AutoUpdateTime
|
||||
settings.LogLevel = h.config.LogLevel
|
||||
settings.DiscoveryEnabled = h.config.DiscoveryEnabled
|
||||
settings.DiscoverySubnet = h.config.DiscoverySubnet
|
||||
settings.DiscoveryConfig = config.CloneDiscoveryConfig(h.config.Discovery)
|
||||
backupEnabled := h.config.EnableBackupPolling
|
||||
settings.BackupPollingEnabled = &backupEnabled
|
||||
|
||||
|
|
@ -2927,7 +2929,11 @@ func (h *ConfigHandlers) HandleDiscoverServers(w http.ResponseWriter, r *http.Re
|
|||
|
||||
log.Info().Str("subnet", subnet).Msg("Starting manual discovery scan")
|
||||
|
||||
scanner := discovery.NewScanner()
|
||||
scanner, buildErr := discoveryinternal.BuildScanner(h.config.Discovery)
|
||||
if buildErr != nil {
|
||||
log.Warn().Err(buildErr).Msg("Falling back to default scanner for manual discovery")
|
||||
scanner = pkgdiscovery.NewScanner()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -3594,6 +3600,40 @@ SSH_SENSORS_PUBLIC_KEY="%s"
|
|||
SSH_PROXY_KEY_ENTRY="restrict,permitopen=\"*:22\" $SSH_PROXY_PUBLIC_KEY # pulse-proxyjump"
|
||||
SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_SENSORS_PUBLIC_KEY # pulse-sensors"
|
||||
TEMPERATURE_ENABLED=false
|
||||
TEMP_MONITORING_AVAILABLE=true
|
||||
MIN_PROXY_VERSION="%s"
|
||||
PULSE_VERSION_ENDPOINT="%s/api/version"
|
||||
|
||||
version_ge() {
|
||||
if command -v dpkg >/dev/null 2>&1; then
|
||||
dpkg --compare-versions "$1" ge "$2"
|
||||
return $?
|
||||
fi
|
||||
if command -v sort >/dev/null 2>&1; then
|
||||
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
|
||||
return $?
|
||||
fi
|
||||
[ "$1" = "$2" ]
|
||||
}
|
||||
|
||||
PULSE_VERSION=$(curl -s -f "$PULSE_VERSION_ENDPOINT" 2>/dev/null | awk -F'"' '/"version":/{print $4}' | head -n1)
|
||||
if [ -z "$PULSE_VERSION" ]; then
|
||||
echo ""
|
||||
echo "⚠️ Could not determine Pulse version from $PULSE_VERSION_ENDPOINT"
|
||||
echo " Temperature proxy requires Pulse $MIN_PROXY_VERSION or later."
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
elif ! version_ge "$PULSE_VERSION" "$MIN_PROXY_VERSION"; then
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "⚠️ Pulse upgrade required for temperature proxy"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Detected Pulse version: $PULSE_VERSION"
|
||||
echo "Minimum required version: $MIN_PROXY_VERSION"
|
||||
echo ""
|
||||
echo "Please upgrade the Pulse container before rerunning this setup script."
|
||||
echo ""
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
fi
|
||||
|
||||
# Check if temperature proxy is available and override SSH key if it is
|
||||
PROXY_KEY_URL="%s/api/system/proxy-public-key"
|
||||
|
|
@ -3640,11 +3680,10 @@ if command -v pct >/dev/null 2>&1; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Track whether temperature monitoring can work
|
||||
TEMP_MONITORING_AVAILABLE=true
|
||||
# Track whether temperature monitoring can work (may be disabled by checks above)
|
||||
|
||||
# If Pulse is containerized, try to install proxy automatically
|
||||
if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||
if [ "$TEMP_MONITORING_AVAILABLE" = true ] && [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||
# Try automatic installation - proxy keeps SSH credentials on the host for security
|
||||
if true; then
|
||||
# Download installer script from Pulse server
|
||||
|
|
@ -3677,42 +3716,91 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
|||
echo "✓ Secure proxy architecture enabled"
|
||||
echo " SSH keys are managed on the host for enhanced security"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ pulse-sensor-proxy service is not active. Check logs with:"
|
||||
echo " journalctl -u pulse-sensor-proxy -n 40"
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
fi
|
||||
|
||||
# Configure socket bind mount and restart container automatically
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Finalizing Setup"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Configuring socket bind mount for container $PULSE_CTID..."
|
||||
if [ "$TEMP_MONITORING_AVAILABLE" = true ] && [ ! -S /run/pulse-sensor-proxy/pulse-sensor-proxy.sock ]; then
|
||||
echo " ✗ Proxy socket not found at /run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
|
||||
echo " Check logs with: journalctl -u pulse-sensor-proxy -n 40"
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
fi
|
||||
|
||||
# Configure bind mount for proxy socket
|
||||
pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy
|
||||
|
||||
# Check if container is currently running
|
||||
if pct status "$PULSE_CTID" 2>/dev/null | grep -q "running"; then
|
||||
if [ "$TEMP_MONITORING_AVAILABLE" = true ]; then
|
||||
# Configure socket bind mount and restart container automatically
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Finalizing Setup"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "⚠️ Container $PULSE_CTID is currently running."
|
||||
echo " The proxy socket will be available after a restart."
|
||||
echo ""
|
||||
echo " Restart manually when ready:"
|
||||
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
||||
echo ""
|
||||
echo " Or the socket may be hot-plugged automatically (LXC 4.0+)"
|
||||
echo ""
|
||||
else
|
||||
echo "Container is stopped, starting it now..."
|
||||
echo "Configuring socket bind mount for container $PULSE_CTID..."
|
||||
|
||||
# Set up trap to restart container even if script is interrupted
|
||||
trap "echo 'Starting container before exit...'; pct start $PULSE_CTID 2>/dev/null || true" EXIT INT TERM
|
||||
# Configure bind mount for proxy socket
|
||||
pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy
|
||||
|
||||
pct start "$PULSE_CTID"
|
||||
CONTAINER_RESTARTED=false
|
||||
# Check if container is currently running
|
||||
if pct status "$PULSE_CTID" 2>/dev/null | grep -q "running"; then
|
||||
echo ""
|
||||
echo "⚠️ Container $PULSE_CTID must be restarted to expose the proxy socket."
|
||||
echo "Restart now? [Y/n]"
|
||||
printf "> "
|
||||
if [ -t 0 ]; then
|
||||
read -n 1 -r RESTART_REPLY
|
||||
echo ""
|
||||
else
|
||||
if read -n 1 -r RESTART_REPLY </dev/tty 2>/dev/null; then
|
||||
echo ""
|
||||
else
|
||||
RESTART_REPLY="n"
|
||||
echo "(No terminal available - skipping automatic restart)"
|
||||
fi
|
||||
fi
|
||||
RESTART_REPLY=${RESTART_REPLY:-Y}
|
||||
if [[ "$RESTART_REPLY" =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
echo "Restarting container $PULSE_CTID..."
|
||||
if pct stop "$PULSE_CTID" && sleep 2 && pct start "$PULSE_CTID"; then
|
||||
echo " ✓ Container restarted"
|
||||
CONTAINER_RESTARTED=true
|
||||
else
|
||||
echo " ✗ Failed to restart container automatically."
|
||||
echo " Please run: pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "Please restart manually when ready:"
|
||||
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
||||
fi
|
||||
else
|
||||
echo "Container is stopped, starting it now..."
|
||||
|
||||
# Clear the trap after successful start
|
||||
trap - EXIT INT TERM
|
||||
# Set up trap to restart container even if script is interrupted
|
||||
trap "echo 'Starting container before exit...'; pct start $PULSE_CTID 2>/dev/null || true" EXIT INT TERM
|
||||
|
||||
echo " ✓ Container started successfully"
|
||||
echo ""
|
||||
if pct start "$PULSE_CTID"; then
|
||||
CONTAINER_RESTARTED=true
|
||||
echo " ✓ Container started successfully"
|
||||
echo ""
|
||||
else
|
||||
echo " ✗ Failed to start container $PULSE_CTID automatically."
|
||||
fi
|
||||
|
||||
# Clear the trap after successful start
|
||||
trap - EXIT INT TERM
|
||||
fi
|
||||
|
||||
if [ "$CONTAINER_RESTARTED" = true ]; then
|
||||
if ! pct exec "$PULSE_CTID" -- test -S /mnt/pulse-proxy/pulse-sensor-proxy.sock 2>/dev/null; then
|
||||
echo " ✗ Proxy socket not visible inside the container (/mnt/pulse-proxy/pulse-sensor-proxy.sock)"
|
||||
echo " Verify the bind mount and container restart completed successfully."
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
fi
|
||||
else
|
||||
TEMP_MONITORING_AVAILABLE=false
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
|
|
@ -4326,7 +4414,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then
|
|||
fi
|
||||
`, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP,
|
||||
tokenName, tokenName, tokenName, tokenName, tokenName, tokenName,
|
||||
authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshKeys.ProxyPublicKey, sshKeys.SensorsPublicKey, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken, pulseURL, tokenName)
|
||||
authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshKeys.ProxyPublicKey, sshKeys.SensorsPublicKey, minProxyReadyVersion, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken, pulseURL, tokenName)
|
||||
|
||||
} else { // PBS
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
|
|
|
|||
|
|
@ -156,15 +156,14 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc {
|
|||
}
|
||||
log.Info().Msg("Runtime config updated with new security settings - active immediately")
|
||||
|
||||
// Save system settings to system.json
|
||||
systemSettings := config.SystemSettings{
|
||||
ConnectionTimeout: 10, // Default
|
||||
AutoUpdateEnabled: false, // Default
|
||||
}
|
||||
if err := r.persistence.SaveSystemSettings(systemSettings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
// Continue anyway - not critical for auth setup
|
||||
}
|
||||
// Save system settings to system.json
|
||||
systemSettings := config.DefaultSystemSettings()
|
||||
systemSettings.ConnectionTimeout = 10 // Default seconds
|
||||
systemSettings.AutoUpdateEnabled = false // Default disabled
|
||||
if err := r.persistence.SaveSystemSettings(*systemSettings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
// Continue anyway - not critical for auth setup
|
||||
}
|
||||
|
||||
// Detect environment
|
||||
isSystemd := os.Getenv("INVOCATION_ID") != ""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -152,6 +153,107 @@ func validateSystemSettings(settings *config.SystemSettings, rawRequest map[stri
|
|||
}
|
||||
}
|
||||
|
||||
if val, ok := rawRequest["discoveryConfig"]; ok {
|
||||
cfgMap, ok := val.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig must be an object")
|
||||
}
|
||||
|
||||
if envVal, exists := cfgMap["environmentOverride"]; exists {
|
||||
envStr, ok := envVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.environmentOverride must be a string")
|
||||
}
|
||||
if !config.IsValidDiscoveryEnvironment(envStr) {
|
||||
return fmt.Errorf("invalid discovery environment override: %s", envStr)
|
||||
}
|
||||
}
|
||||
|
||||
if allowVal, exists := cfgMap["subnetAllowlist"]; exists {
|
||||
items, ok := allowVal.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.subnetAllowlist must be an array of CIDR strings")
|
||||
}
|
||||
for _, item := range items {
|
||||
cidr, ok := item.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.subnetAllowlist entries must be strings")
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(cidr); err != nil {
|
||||
return fmt.Errorf("invalid CIDR in discoveryConfig.subnetAllowlist: %s", cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if blockVal, exists := cfgMap["subnetBlocklist"]; exists {
|
||||
items, ok := blockVal.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.subnetBlocklist must be an array of CIDR strings")
|
||||
}
|
||||
for _, item := range items {
|
||||
cidr, ok := item.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.subnetBlocklist entries must be strings")
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(cidr); err != nil {
|
||||
return fmt.Errorf("invalid CIDR in discoveryConfig.subnetBlocklist: %s", cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hostsVal, exists := cfgMap["maxHostsPerScan"]; exists {
|
||||
value, ok := hostsVal.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.maxHostsPerScan must be a number")
|
||||
}
|
||||
if value <= 0 {
|
||||
return fmt.Errorf("discoveryConfig.maxHostsPerScan must be greater than zero")
|
||||
}
|
||||
}
|
||||
|
||||
if concurrentVal, exists := cfgMap["maxConcurrent"]; exists {
|
||||
value, ok := concurrentVal.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.maxConcurrent must be a number")
|
||||
}
|
||||
if value <= 0 || value > 1000 {
|
||||
return fmt.Errorf("discoveryConfig.maxConcurrent must be between 1 and 1000")
|
||||
}
|
||||
}
|
||||
|
||||
if val, exists := cfgMap["enableReverseDns"]; exists {
|
||||
if _, ok := val.(bool); !ok {
|
||||
return fmt.Errorf("discoveryConfig.enableReverseDns must be a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
if val, exists := cfgMap["scanGateways"]; exists {
|
||||
if _, ok := val.(bool); !ok {
|
||||
return fmt.Errorf("discoveryConfig.scanGateways must be a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
if val, exists := cfgMap["dialTimeoutMs"]; exists {
|
||||
timeout, ok := val.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.dialTimeoutMs must be a number")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return fmt.Errorf("discoveryConfig.dialTimeoutMs must be greater than zero")
|
||||
}
|
||||
}
|
||||
|
||||
if val, exists := cfgMap["httpTimeoutMs"]; exists {
|
||||
timeout, ok := val.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("discoveryConfig.httpTimeoutMs must be a number")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return fmt.Errorf("discoveryConfig.httpTimeoutMs must be greater than zero")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate connection timeout (min 1 second, max 5 minutes)
|
||||
if val, ok := rawRequest["connectionTimeout"]; ok {
|
||||
if timeout, ok := val.(float64); ok {
|
||||
|
|
@ -204,7 +306,10 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
|||
settings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load system settings")
|
||||
settings = &config.SystemSettings{}
|
||||
settings = config.DefaultSystemSettings()
|
||||
}
|
||||
if settings == nil {
|
||||
settings = config.DefaultSystemSettings()
|
||||
}
|
||||
|
||||
// Log loaded settings for debugging
|
||||
|
|
@ -217,6 +322,7 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
|||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||
enabled := h.config.EnableBackupPolling
|
||||
settings.BackupPollingEnabled = &enabled
|
||||
settings.DiscoveryConfig = config.CloneDiscoveryConfig(h.config.Discovery)
|
||||
}
|
||||
|
||||
// Include env override information
|
||||
|
|
@ -268,10 +374,10 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
existingSettings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load existing settings")
|
||||
existingSettings = &config.SystemSettings{}
|
||||
existingSettings = config.DefaultSystemSettings()
|
||||
}
|
||||
if existingSettings == nil {
|
||||
existingSettings = &config.SystemSettings{}
|
||||
existingSettings = config.DefaultSystemSettings()
|
||||
}
|
||||
|
||||
// Read the request body into a map to check which fields were provided
|
||||
|
|
@ -303,6 +409,7 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
|
||||
// Start with existing settings
|
||||
settings := *existingSettings
|
||||
discoveryConfigUpdated := false
|
||||
|
||||
// Only update fields that were provided in the request
|
||||
// Note: PVE polling is hardcoded to 10s, legacy polling fields are ignored
|
||||
|
|
@ -336,6 +443,10 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if updates.DiscoverySubnet != "" {
|
||||
settings.DiscoverySubnet = updates.DiscoverySubnet
|
||||
}
|
||||
if _, ok := rawRequest["discoveryConfig"]; ok {
|
||||
settings.DiscoveryConfig = config.CloneDiscoveryConfig(updates.DiscoveryConfig)
|
||||
discoveryConfigUpdated = true
|
||||
}
|
||||
// Allow clearing of AllowedEmbedOrigins by setting to empty string
|
||||
if _, ok := rawRequest["allowedEmbedOrigins"]; ok {
|
||||
settings.AllowedEmbedOrigins = updates.AllowedEmbedOrigins
|
||||
|
|
@ -401,6 +512,7 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if settings.DiscoverySubnet != "" {
|
||||
h.config.DiscoverySubnet = settings.DiscoverySubnet
|
||||
}
|
||||
h.config.Discovery = config.CloneDiscoveryConfig(settings.DiscoveryConfig)
|
||||
|
||||
// Start or stop discovery service based on setting change
|
||||
if h.monitor != nil {
|
||||
|
|
@ -422,6 +534,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
svc.SetSubnet(settings.DiscoverySubnet)
|
||||
}
|
||||
}
|
||||
if discoveryConfigUpdated && settings.DiscoveryEnabled {
|
||||
if svc := h.monitor.GetDiscoveryService(); svc != nil {
|
||||
log.Info().Msg("Discovery configuration changed; triggering refresh")
|
||||
svc.ForceRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save to persistence
|
||||
|
|
|
|||
|
|
@ -130,8 +130,9 @@ type Config struct {
|
|||
AutoUpdateTime string `envconfig:"AUTO_UPDATE_TIME" default:"03:00"`
|
||||
|
||||
// Discovery settings
|
||||
DiscoveryEnabled bool `envconfig:"DISCOVERY_ENABLED" default:"true"`
|
||||
DiscoverySubnet string `envconfig:"DISCOVERY_SUBNET" default:"auto"`
|
||||
DiscoveryEnabled bool `envconfig:"DISCOVERY_ENABLED" default:"true"`
|
||||
DiscoverySubnet string `envconfig:"DISCOVERY_SUBNET" default:"auto"`
|
||||
Discovery DiscoveryConfig `json:"discoveryConfig"`
|
||||
|
||||
// Deprecated - for backward compatibility
|
||||
Port int `envconfig:"PORT"` // Maps to BackendPort
|
||||
|
|
@ -141,6 +142,71 @@ type Config struct {
|
|||
EnvOverrides map[string]bool `json:"-"`
|
||||
}
|
||||
|
||||
// DiscoveryConfig captures overrides for network discovery behaviour.
|
||||
type DiscoveryConfig struct {
|
||||
EnvironmentOverride string `json:"environmentOverride,omitempty"`
|
||||
SubnetAllowlist []string `json:"subnetAllowlist,omitempty"`
|
||||
SubnetBlocklist []string `json:"subnetBlocklist,omitempty"`
|
||||
MaxHostsPerScan int `json:"maxHostsPerScan,omitempty"`
|
||||
MaxConcurrent int `json:"maxConcurrent,omitempty"`
|
||||
EnableReverseDNS bool `json:"enableReverseDns"`
|
||||
ScanGateways bool `json:"scanGateways"`
|
||||
DialTimeout int `json:"dialTimeoutMs,omitempty"`
|
||||
HTTPTimeout int `json:"httpTimeoutMs,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultDiscoveryConfig returns opinionated defaults for discovery behaviour.
|
||||
func DefaultDiscoveryConfig() DiscoveryConfig {
|
||||
return DiscoveryConfig{
|
||||
EnvironmentOverride: "auto",
|
||||
SubnetAllowlist: []string{},
|
||||
SubnetBlocklist: []string{"169.254.0.0/16"},
|
||||
MaxHostsPerScan: 1024,
|
||||
MaxConcurrent: 50,
|
||||
EnableReverseDNS: true,
|
||||
ScanGateways: true,
|
||||
DialTimeout: 1000,
|
||||
HTTPTimeout: 2000,
|
||||
}
|
||||
}
|
||||
|
||||
// CloneDiscoveryConfig returns a deep copy of the provided discovery config.
|
||||
func CloneDiscoveryConfig(cfg DiscoveryConfig) DiscoveryConfig {
|
||||
clone := cfg
|
||||
if cfg.SubnetAllowlist != nil {
|
||||
clone.SubnetAllowlist = append([]string(nil), cfg.SubnetAllowlist...)
|
||||
}
|
||||
if cfg.SubnetBlocklist != nil {
|
||||
clone.SubnetBlocklist = append([]string(nil), cfg.SubnetBlocklist...)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// IsValidDiscoveryEnvironment reports whether the supplied override is recognised.
|
||||
func IsValidDiscoveryEnvironment(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "auto", "native", "docker_host", "docker_bridge", "lxc_privileged", "lxc_unprivileged":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func splitAndTrim(value string) []string {
|
||||
if value == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PVEInstance represents a Proxmox VE connection
|
||||
type PVEInstance struct {
|
||||
Name string
|
||||
|
|
@ -284,9 +350,11 @@ func Load() (*Config, error) {
|
|||
DiscoveryEnabled: true,
|
||||
DiscoverySubnet: "auto",
|
||||
EnvOverrides: make(map[string]bool),
|
||||
OIDC: NewOIDCConfig(),
|
||||
OIDC: NewOIDCConfig(),
|
||||
}
|
||||
|
||||
cfg.Discovery = DefaultDiscoveryConfig()
|
||||
|
||||
// Initialize persistence
|
||||
persistence := NewConfigPersistence(dataDir)
|
||||
if persistence != nil {
|
||||
|
|
@ -361,6 +429,7 @@ func Load() (*Config, error) {
|
|||
if systemSettings.DiscoverySubnet != "" {
|
||||
cfg.DiscoverySubnet = systemSettings.DiscoverySubnet
|
||||
}
|
||||
cfg.Discovery = CloneDiscoveryConfig(systemSettings.DiscoveryConfig)
|
||||
// APIToken no longer loaded from system.json - only from .env
|
||||
log.Info().
|
||||
Str("updateChannel", cfg.UpdateChannel).
|
||||
|
|
@ -369,13 +438,11 @@ func Load() (*Config, error) {
|
|||
} else {
|
||||
// No system.json exists - create default one
|
||||
log.Info().Msg("No system.json found, creating default")
|
||||
defaultSettings := SystemSettings{
|
||||
ConnectionTimeout: int(cfg.ConnectionTimeout.Seconds()),
|
||||
AutoUpdateEnabled: false,
|
||||
}
|
||||
if err := persistence.SaveSystemSettings(defaultSettings); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to create default system.json")
|
||||
}
|
||||
defaultSettings := DefaultSystemSettings()
|
||||
defaultSettings.ConnectionTimeout = int(cfg.ConnectionTimeout.Seconds())
|
||||
if err := persistence.SaveSystemSettings(*defaultSettings); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to create default system.json")
|
||||
}
|
||||
}
|
||||
|
||||
if oidcSettings, err := persistence.LoadOIDCConfig(); err == nil && oidcSettings != nil {
|
||||
|
|
@ -741,6 +808,83 @@ func Load() (*Config, error) {
|
|||
cfg.EnvOverrides["discoverySubnet"] = true
|
||||
log.Info().Str("subnet", discoverySubnet).Msg("Discovery subnet overridden by DISCOVERY_SUBNET env var")
|
||||
}
|
||||
if envOverride := strings.TrimSpace(os.Getenv("DISCOVERY_ENVIRONMENT_OVERRIDE")); envOverride != "" {
|
||||
if IsValidDiscoveryEnvironment(envOverride) {
|
||||
cfg.Discovery.EnvironmentOverride = strings.ToLower(envOverride)
|
||||
cfg.EnvOverrides["discoveryEnvironmentOverride"] = true
|
||||
log.Info().Str("environment", cfg.Discovery.EnvironmentOverride).Msg("Discovery environment override set by DISCOVERY_ENVIRONMENT_OVERRIDE")
|
||||
} else {
|
||||
log.Warn().Str("value", envOverride).Msg("Ignoring invalid DISCOVERY_ENVIRONMENT_OVERRIDE value")
|
||||
}
|
||||
}
|
||||
if allowlistEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SUBNET_ALLOWLIST")); allowlistEnv != "" {
|
||||
parts := splitAndTrim(allowlistEnv)
|
||||
cfg.Discovery.SubnetAllowlist = parts
|
||||
cfg.EnvOverrides["discoverySubnetAllowlist"] = true
|
||||
log.Info().Int("allowlistCount", len(parts)).Msg("Discovery subnet allowlist overridden by DISCOVERY_SUBNET_ALLOWLIST")
|
||||
}
|
||||
if blocklistEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SUBNET_BLOCKLIST")); blocklistEnv != "" {
|
||||
parts := splitAndTrim(blocklistEnv)
|
||||
cfg.Discovery.SubnetBlocklist = parts
|
||||
cfg.EnvOverrides["discoverySubnetBlocklist"] = true
|
||||
log.Info().Int("blocklistCount", len(parts)).Msg("Discovery subnet blocklist overridden by DISCOVERY_SUBNET_BLOCKLIST")
|
||||
}
|
||||
if maxHostsEnv := strings.TrimSpace(os.Getenv("DISCOVERY_MAX_HOSTS_PER_SCAN")); maxHostsEnv != "" {
|
||||
if v, err := strconv.Atoi(maxHostsEnv); err == nil && v > 0 {
|
||||
cfg.Discovery.MaxHostsPerScan = v
|
||||
cfg.EnvOverrides["discoveryMaxHostsPerScan"] = true
|
||||
log.Info().Int("maxHostsPerScan", v).Msg("Discovery max hosts per scan overridden by DISCOVERY_MAX_HOSTS_PER_SCAN")
|
||||
} else {
|
||||
log.Warn().Str("value", maxHostsEnv).Msg("Ignoring invalid DISCOVERY_MAX_HOSTS_PER_SCAN value")
|
||||
}
|
||||
}
|
||||
if maxConcurrentEnv := strings.TrimSpace(os.Getenv("DISCOVERY_MAX_CONCURRENT")); maxConcurrentEnv != "" {
|
||||
if v, err := strconv.Atoi(maxConcurrentEnv); err == nil && v > 0 {
|
||||
cfg.Discovery.MaxConcurrent = v
|
||||
cfg.EnvOverrides["discoveryMaxConcurrent"] = true
|
||||
log.Info().Int("maxConcurrent", v).Msg("Discovery concurrency overridden by DISCOVERY_MAX_CONCURRENT")
|
||||
} else {
|
||||
log.Warn().Str("value", maxConcurrentEnv).Msg("Ignoring invalid DISCOVERY_MAX_CONCURRENT value")
|
||||
}
|
||||
}
|
||||
if reverseDNSEnv := strings.TrimSpace(os.Getenv("DISCOVERY_ENABLE_REVERSE_DNS")); reverseDNSEnv != "" {
|
||||
switch strings.ToLower(reverseDNSEnv) {
|
||||
case "0", "false", "no", "off":
|
||||
cfg.Discovery.EnableReverseDNS = false
|
||||
default:
|
||||
cfg.Discovery.EnableReverseDNS = true
|
||||
}
|
||||
cfg.EnvOverrides["discoveryEnableReverseDns"] = true
|
||||
log.Info().Bool("enableReverseDNS", cfg.Discovery.EnableReverseDNS).Msg("Discovery reverse DNS setting overridden by DISCOVERY_ENABLE_REVERSE_DNS")
|
||||
}
|
||||
if scanGatewaysEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SCAN_GATEWAYS")); scanGatewaysEnv != "" {
|
||||
switch strings.ToLower(scanGatewaysEnv) {
|
||||
case "0", "false", "no", "off":
|
||||
cfg.Discovery.ScanGateways = false
|
||||
default:
|
||||
cfg.Discovery.ScanGateways = true
|
||||
}
|
||||
cfg.EnvOverrides["discoveryScanGateways"] = true
|
||||
log.Info().Bool("scanGateways", cfg.Discovery.ScanGateways).Msg("Discovery gateway scanning overridden by DISCOVERY_SCAN_GATEWAYS")
|
||||
}
|
||||
if dialTimeoutEnv := strings.TrimSpace(os.Getenv("DISCOVERY_DIAL_TIMEOUT_MS")); dialTimeoutEnv != "" {
|
||||
if v, err := strconv.Atoi(dialTimeoutEnv); err == nil && v > 0 {
|
||||
cfg.Discovery.DialTimeout = v
|
||||
cfg.EnvOverrides["discoveryDialTimeoutMs"] = true
|
||||
log.Info().Int("dialTimeoutMs", v).Msg("Discovery dial timeout overridden by DISCOVERY_DIAL_TIMEOUT_MS")
|
||||
} else {
|
||||
log.Warn().Str("value", dialTimeoutEnv).Msg("Ignoring invalid DISCOVERY_DIAL_TIMEOUT_MS value")
|
||||
}
|
||||
}
|
||||
if httpTimeoutEnv := strings.TrimSpace(os.Getenv("DISCOVERY_HTTP_TIMEOUT_MS")); httpTimeoutEnv != "" {
|
||||
if v, err := strconv.Atoi(httpTimeoutEnv); err == nil && v > 0 {
|
||||
cfg.Discovery.HTTPTimeout = v
|
||||
cfg.EnvOverrides["discoveryHttpTimeoutMs"] = true
|
||||
log.Info().Int("httpTimeoutMs", v).Msg("Discovery HTTP timeout overridden by DISCOVERY_HTTP_TIMEOUT_MS")
|
||||
} else {
|
||||
log.Warn().Str("value", httpTimeoutEnv).Msg("Ignoring invalid DISCOVERY_HTTP_TIMEOUT_MS value")
|
||||
}
|
||||
}
|
||||
if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" {
|
||||
cfg.LogLevel = logLevel
|
||||
cfg.EnvOverrides["logLevel"] = true
|
||||
|
|
@ -820,6 +964,7 @@ func SaveConfig(cfg *Config) error {
|
|||
LogLevel: cfg.LogLevel,
|
||||
DiscoveryEnabled: cfg.DiscoveryEnabled,
|
||||
DiscoverySubnet: cfg.DiscoverySubnet,
|
||||
DiscoveryConfig: CloneDiscoveryConfig(cfg.Discovery),
|
||||
AdaptivePollingEnabled: &adaptiveEnabled,
|
||||
AdaptivePollingBaseInterval: int(cfg.AdaptivePollingBaseInterval / time.Second),
|
||||
AdaptivePollingMinInterval: int(cfg.AdaptivePollingMinInterval / time.Second),
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func (c *ConfigPersistence) ExportConfig(passphrase string) (string, error) {
|
|||
return "", fmt.Errorf("failed to load system settings: %w", err)
|
||||
}
|
||||
if systemSettings == nil {
|
||||
systemSettings = &SystemSettings{}
|
||||
systemSettings = DefaultSystemSettings()
|
||||
}
|
||||
|
||||
oidcConfig, err := c.LoadOIDCConfig()
|
||||
|
|
|
|||
|
|
@ -682,12 +682,27 @@ type SystemSettings struct {
|
|||
LogLevel string `json:"logLevel,omitempty"`
|
||||
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
||||
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
||||
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
||||
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
||||
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
||||
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
||||
// APIToken removed - now handled via .env file only
|
||||
}
|
||||
|
||||
// DefaultSystemSettings returns a SystemSettings struct populated with sane defaults.
|
||||
func DefaultSystemSettings() *SystemSettings {
|
||||
defaultDiscovery := DefaultDiscoveryConfig()
|
||||
return &SystemSettings{
|
||||
PBSPollingInterval: 60,
|
||||
PMGPollingInterval: 60,
|
||||
AutoUpdateEnabled: false,
|
||||
DiscoveryEnabled: true,
|
||||
DiscoverySubnet: "auto",
|
||||
DiscoveryConfig: defaultDiscovery,
|
||||
AllowEmbedding: false,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveNodesConfig saves nodes configuration to file (encrypted)
|
||||
func (c *ConfigPersistence) SaveNodesConfig(pveInstances []PVEInstance, pbsInstances []PBSInstance, pmgInstances []PMGInstance) error {
|
||||
return c.saveNodesConfig(pveInstances, pbsInstances, pmgInstances, false)
|
||||
|
|
@ -1154,13 +1169,16 @@ func (c *ConfigPersistence) LoadSystemSettings() (*SystemSettings, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var settings SystemSettings
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
settings := DefaultSystemSettings()
|
||||
if settings == nil {
|
||||
settings = &SystemSettings{}
|
||||
}
|
||||
if err := json.Unmarshal(data, settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Str("file", c.systemFile).Msg("System settings loaded")
|
||||
return &settings, nil
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// updateEnvFile updates the .env file with new system settings
|
||||
|
|
|
|||
180
internal/discovery/config_override.go
Normal file
180
internal/discovery/config_override.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery/envdetect"
|
||||
)
|
||||
|
||||
// BuildScanner creates a discovery scanner configured using the supplied discovery config.
|
||||
func BuildScanner(cfg config.DiscoveryConfig) (*pkgdiscovery.Scanner, error) {
|
||||
profile, err := envdetect.DetectEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ApplyConfigToProfile(profile, cfg)
|
||||
return pkgdiscovery.NewScannerWithProfile(profile), nil
|
||||
}
|
||||
|
||||
// ApplyConfigToProfile mutates the supplied environment profile according to the discovery config.
|
||||
func ApplyConfigToProfile(profile *envdetect.EnvironmentProfile, cfg config.DiscoveryConfig) {
|
||||
if profile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Environment override
|
||||
if env, ok := environmentFromOverride(cfg.EnvironmentOverride); ok {
|
||||
profile.Type = env
|
||||
filterPhasesForEnvironment(profile, env)
|
||||
} else if cfg.EnvironmentOverride != "" && strings.ToLower(cfg.EnvironmentOverride) != "auto" {
|
||||
profile.Warnings = append(profile.Warnings, fmt.Sprintf("Unknown environment override: %s", cfg.EnvironmentOverride))
|
||||
}
|
||||
|
||||
// Apply subnet blocklist
|
||||
blocked := parseCIDRMap(cfg.SubnetBlocklist, &profile.Warnings)
|
||||
if len(blocked) > 0 {
|
||||
var filtered []envdetect.SubnetPhase
|
||||
for _, phase := range profile.Phases {
|
||||
var kept []net.IPNet
|
||||
for _, subnet := range phase.Subnets {
|
||||
if _, blocked := blocked[subnet.String()]; blocked {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, subnet)
|
||||
}
|
||||
if len(kept) > 0 {
|
||||
phase.Subnets = kept
|
||||
filtered = append(filtered, phase)
|
||||
}
|
||||
}
|
||||
profile.Phases = filtered
|
||||
}
|
||||
|
||||
// Apply subnet allowlist as highest priority phase
|
||||
if len(cfg.SubnetAllowlist) > 0 {
|
||||
allowlist := parseCIDRs(cfg.SubnetAllowlist, &profile.Warnings)
|
||||
if len(allowlist) > 0 {
|
||||
if len(blocked) > 0 {
|
||||
filtered := allowlist[:0]
|
||||
for _, subnet := range allowlist {
|
||||
if _, skip := blocked[subnet.String()]; skip {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, subnet)
|
||||
}
|
||||
allowlist = filtered
|
||||
}
|
||||
}
|
||||
if len(allowlist) > 0 {
|
||||
allowPhase := envdetect.SubnetPhase{
|
||||
Name: "config_allowlist",
|
||||
Subnets: allowlist,
|
||||
Confidence: 1.0,
|
||||
Priority: 0,
|
||||
}
|
||||
profile.Phases = append([]envdetect.SubnetPhase{allowPhase}, profile.Phases...)
|
||||
}
|
||||
}
|
||||
|
||||
// Override scan policy
|
||||
if cfg.MaxHostsPerScan > 0 {
|
||||
profile.Policy.MaxHostsPerScan = cfg.MaxHostsPerScan
|
||||
}
|
||||
if cfg.MaxConcurrent > 0 {
|
||||
profile.Policy.MaxConcurrent = cfg.MaxConcurrent
|
||||
}
|
||||
profile.Policy.EnableReverseDNS = cfg.EnableReverseDNS
|
||||
profile.Policy.ScanGateways = cfg.ScanGateways
|
||||
|
||||
if cfg.DialTimeout > 0 {
|
||||
profile.Policy.DialTimeout = time.Duration(cfg.DialTimeout) * time.Millisecond
|
||||
}
|
||||
if cfg.HTTPTimeout > 0 {
|
||||
profile.Policy.HTTPTimeout = time.Duration(cfg.HTTPTimeout) * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
func parseCIDRs(values []string, warnings *[]string) []net.IPNet {
|
||||
var subnets []net.IPNet
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
_, ipNet, err := net.ParseCIDR(value)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
*warnings = append(*warnings, fmt.Sprintf("Invalid CIDR '%s' ignored", value))
|
||||
}
|
||||
continue
|
||||
}
|
||||
subnets = append(subnets, *ipNet)
|
||||
}
|
||||
return subnets
|
||||
}
|
||||
|
||||
func parseCIDRMap(values []string, warnings *[]string) map[string]struct{} {
|
||||
cidrs := parseCIDRs(values, warnings)
|
||||
result := make(map[string]struct{}, len(cidrs))
|
||||
for _, cidr := range cidrs {
|
||||
result[cidr.String()] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func environmentFromOverride(value string) (envdetect.Environment, bool) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
switch normalized {
|
||||
case "", "auto":
|
||||
return envdetect.Unknown, false
|
||||
case "native":
|
||||
return envdetect.Native, true
|
||||
case "docker_host":
|
||||
return envdetect.DockerHost, true
|
||||
case "docker_bridge":
|
||||
return envdetect.DockerBridge, true
|
||||
case "lxc_privileged":
|
||||
return envdetect.LXCPrivileged, true
|
||||
case "lxc_unprivileged":
|
||||
return envdetect.LXCUnprivileged, true
|
||||
default:
|
||||
return envdetect.Unknown, false
|
||||
}
|
||||
}
|
||||
|
||||
func filterPhasesForEnvironment(profile *envdetect.EnvironmentProfile, env envdetect.Environment) {
|
||||
if len(profile.Phases) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var keep []envdetect.SubnetPhase
|
||||
for _, phase := range profile.Phases {
|
||||
name := strings.ToLower(phase.Name)
|
||||
switch env {
|
||||
case envdetect.Native, envdetect.DockerHost, envdetect.LXCPrivileged:
|
||||
if strings.Contains(name, "local") || strings.Contains(name, "host") {
|
||||
keep = append(keep, phase)
|
||||
}
|
||||
case envdetect.DockerBridge:
|
||||
if strings.Contains(name, "container") || strings.Contains(name, "inferred") || strings.Contains(name, "host") {
|
||||
keep = append(keep, phase)
|
||||
}
|
||||
case envdetect.LXCUnprivileged:
|
||||
if strings.Contains(name, "lxc") || strings.Contains(name, "container") || strings.Contains(name, "parent") {
|
||||
keep = append(keep, phase)
|
||||
}
|
||||
default:
|
||||
keep = append(keep, phase)
|
||||
}
|
||||
}
|
||||
|
||||
if len(keep) > 0 {
|
||||
profile.Phases = keep
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,15 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Service handles background network discovery
|
||||
type Service struct {
|
||||
scanner *discovery.Scanner
|
||||
scanner *pkgdiscovery.Scanner
|
||||
wsHub *websocket.Hub
|
||||
cache *DiscoveryCache
|
||||
interval time.Duration
|
||||
|
|
@ -23,17 +24,18 @@ type Service struct {
|
|||
isScanning bool
|
||||
stopChan chan struct{}
|
||||
ctx context.Context
|
||||
cfgProvider func() config.DiscoveryConfig
|
||||
}
|
||||
|
||||
// DiscoveryCache stores the latest discovery results
|
||||
type DiscoveryCache struct {
|
||||
mu sync.RWMutex
|
||||
result *discovery.DiscoveryResult
|
||||
result *pkgdiscovery.DiscoveryResult
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
// NewService creates a new discovery service
|
||||
func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string) *Service {
|
||||
func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string, cfgProvider func() config.DiscoveryConfig) *Service {
|
||||
if interval == 0 {
|
||||
interval = 5 * time.Minute // Default to 5 minutes
|
||||
}
|
||||
|
|
@ -41,13 +43,18 @@ func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string) *Se
|
|||
subnet = "auto"
|
||||
}
|
||||
|
||||
if cfgProvider == nil {
|
||||
cfgProvider = func() config.DiscoveryConfig { return config.DefaultDiscoveryConfig() }
|
||||
}
|
||||
|
||||
return &Service{
|
||||
scanner: discovery.NewScanner(),
|
||||
scanner: pkgdiscovery.NewScanner(),
|
||||
wsHub: wsHub,
|
||||
cache: &DiscoveryCache{},
|
||||
interval: interval,
|
||||
subnet: subnet,
|
||||
stopChan: make(chan struct{}),
|
||||
cfgProvider: cfgProvider,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +108,7 @@ func (s *Service) performScan() {
|
|||
s.isScanning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
var result *discovery.DiscoveryResult
|
||||
var result *pkgdiscovery.DiscoveryResult
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
|
|
@ -144,14 +151,22 @@ func (s *Service) performScan() {
|
|||
scanCtx, cancel := context.WithTimeout(s.ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Refresh scanner to ensure environment detection stays current
|
||||
newScanner := discovery.NewScanner()
|
||||
cfg := config.DefaultDiscoveryConfig()
|
||||
if s.cfgProvider != nil {
|
||||
cfg = config.CloneDiscoveryConfig(s.cfgProvider())
|
||||
}
|
||||
|
||||
newScanner, err := BuildScanner(cfg)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Environment detection failed during discovery; falling back to default scanner configuration")
|
||||
newScanner = pkgdiscovery.NewScanner()
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.scanner = newScanner
|
||||
s.mu.Unlock()
|
||||
|
||||
// Perform the scan with real-time callback
|
||||
result, err = newScanner.DiscoverServersWithCallback(scanCtx, s.subnet, func(server discovery.DiscoveredServer, phase string) {
|
||||
result, err = newScanner.DiscoverServersWithCallback(scanCtx, s.subnet, func(server pkgdiscovery.DiscoveredServer, phase string) {
|
||||
// Send immediate update for each discovered server
|
||||
if s.wsHub != nil {
|
||||
s.wsHub.Broadcast(websocket.Message{
|
||||
|
|
@ -221,15 +236,15 @@ func (s *Service) performScan() {
|
|||
}
|
||||
|
||||
// GetCachedResult returns the cached discovery result
|
||||
func (s *Service) GetCachedResult() (*discovery.DiscoveryResult, time.Time) {
|
||||
func (s *Service) GetCachedResult() (*pkgdiscovery.DiscoveryResult, time.Time) {
|
||||
s.cache.mu.RLock()
|
||||
defer s.cache.mu.RUnlock()
|
||||
|
||||
if s.cache.result == nil {
|
||||
return &discovery.DiscoveryResult{
|
||||
Servers: []discovery.DiscoveredServer{},
|
||||
Errors: []string{},
|
||||
}, time.Time{}
|
||||
if s.cache.result == nil {
|
||||
return &pkgdiscovery.DiscoveryResult{
|
||||
Servers: []pkgdiscovery.DiscoveredServer{},
|
||||
Errors: []string{},
|
||||
}, time.Time{}
|
||||
}
|
||||
|
||||
return s.cache.result, s.cache.updated
|
||||
|
|
|
|||
|
|
@ -1915,7 +1915,15 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
|||
if discoverySubnet == "" {
|
||||
discoverySubnet = "auto"
|
||||
}
|
||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, discoverySubnet)
|
||||
cfgProvider := func() config.DiscoveryConfig {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.config == nil {
|
||||
return config.DefaultDiscoveryConfig()
|
||||
}
|
||||
return config.CloneDiscoveryConfig(m.config.Discovery)
|
||||
}
|
||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, discoverySubnet, cfgProvider)
|
||||
if m.discoveryService != nil {
|
||||
m.discoveryService.Start(ctx)
|
||||
log.Info().Msg("Discovery service initialized and started")
|
||||
|
|
@ -5444,7 +5452,16 @@ func (m *Monitor) StartDiscoveryService(ctx context.Context, wsHub *websocket.Hu
|
|||
subnet = "auto"
|
||||
}
|
||||
|
||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, subnet)
|
||||
cfgProvider := func() config.DiscoveryConfig {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.config == nil {
|
||||
return config.DefaultDiscoveryConfig()
|
||||
}
|
||||
return config.CloneDiscoveryConfig(m.config.Discovery)
|
||||
}
|
||||
|
||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, subnet, cfgProvider)
|
||||
if m.discoveryService != nil {
|
||||
m.discoveryService.Start(ctx)
|
||||
log.Info().Str("subnet", subnet).Msg("Discovery service started")
|
||||
|
|
|
|||
|
|
@ -423,10 +423,6 @@ func buildEnvironmentInfo(profile *envdetect.EnvironmentProfile) *EnvironmentInf
|
|||
func ensurePolicyDefaults(policy envdetect.ScanPolicy) envdetect.ScanPolicy {
|
||||
defaults := envdetect.DefaultScanPolicy()
|
||||
|
||||
if policy == (envdetect.ScanPolicy{}) {
|
||||
return defaults
|
||||
}
|
||||
|
||||
if policy.MaxConcurrent <= 0 {
|
||||
policy.MaxConcurrent = defaults.MaxConcurrent
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue