mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 19:57:09 +00:00
Add runtime mock toggles and auth-safe dev assets
This commit is contained in:
@@ -2271,6 +2271,35 @@ func (m *Manager) CleanupAlertsForNodes(existingNodes map[string]bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ClearActiveAlerts removes all active and pending alerts, resetting the manager state.
|
||||
func (m *Manager) ClearActiveAlerts() {
|
||||
m.mu.Lock()
|
||||
if len(m.activeAlerts) == 0 && len(m.pendingAlerts) == 0 {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.activeAlerts = make(map[string]*Alert)
|
||||
m.pendingAlerts = make(map[string]time.Time)
|
||||
m.recentAlerts = make(map[string]*Alert)
|
||||
m.suppressedUntil = make(map[string]time.Time)
|
||||
m.alertRateLimit = make(map[string][]time.Time)
|
||||
m.nodeOfflineCount = make(map[string]int)
|
||||
m.offlineConfirmations = make(map[string]int)
|
||||
m.mu.Unlock()
|
||||
|
||||
m.resolvedMutex.Lock()
|
||||
m.recentlyResolved = make(map[string]*ResolvedAlert)
|
||||
m.resolvedMutex.Unlock()
|
||||
|
||||
log.Info().Msg("Cleared all active and pending alerts")
|
||||
|
||||
go func() {
|
||||
if err := m.SaveActiveAlerts(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to persist cleared alerts")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// periodicSaveAlerts saves active alerts to disk periodically
|
||||
func (m *Manager) periodicSaveAlerts() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
internalauth "github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
@@ -620,7 +621,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
|
||||
// HandleGetNodes returns all configured nodes
|
||||
func (h *ConfigHandlers) HandleGetNodes(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if mock mode is enabled
|
||||
if os.Getenv("PULSE_MOCK_MODE") == "true" {
|
||||
if mock.IsMockEnabled() {
|
||||
// Return mock nodes for settings page
|
||||
mockNodes := []NodeResponse{}
|
||||
|
||||
@@ -788,7 +789,7 @@ func extractHostAndPort(hostStr string) (string, string, error) {
|
||||
// HandleAddNode adds a new node
|
||||
func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent node modifications in mock mode
|
||||
if os.Getenv("PULSE_MOCK_MODE") == "true" {
|
||||
if mock.IsMockEnabled() {
|
||||
http.Error(w, "Cannot modify nodes in mock mode. Please disable mock mode first: /opt/pulse/scripts/toggle-mock.sh off", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -1270,7 +1271,7 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
|
||||
// HandleUpdateNode updates an existing node
|
||||
func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent node modifications in mock mode
|
||||
if os.Getenv("PULSE_MOCK_MODE") == "true" {
|
||||
if mock.IsMockEnabled() {
|
||||
http.Error(w, "Cannot modify nodes in mock mode", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -1511,7 +1512,7 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
|
||||
// HandleDeleteNode deletes a node
|
||||
func (h *ConfigHandlers) HandleDeleteNode(w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent node modifications in mock mode
|
||||
if os.Getenv("PULSE_MOCK_MODE") == "true" {
|
||||
if mock.IsMockEnabled() {
|
||||
http.Error(w, "Cannot modify nodes in mock mode", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -3182,6 +3183,103 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleGetMockMode returns the current mock mode state and configuration.
|
||||
func (h *ConfigHandlers) HandleGetMockMode(w http.ResponseWriter, r *http.Request) {
|
||||
status := struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Config mock.MockConfig `json:"config"`
|
||||
}{
|
||||
Enabled: mock.IsMockEnabled(),
|
||||
Config: mock.GetConfig(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode mock mode status")
|
||||
}
|
||||
}
|
||||
|
||||
type mockModeRequest struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Config struct {
|
||||
NodeCount *int `json:"nodeCount"`
|
||||
VMsPerNode *int `json:"vmsPerNode"`
|
||||
LXCsPerNode *int `json:"lxcsPerNode"`
|
||||
RandomMetrics *bool `json:"randomMetrics"`
|
||||
HighLoadNodes []string `json:"highLoadNodes"`
|
||||
StoppedPercent *float64 `json:"stoppedPercent"`
|
||||
} `json:"config"`
|
||||
}
|
||||
|
||||
// HandleUpdateMockMode updates mock mode and optionally its configuration.
|
||||
func (h *ConfigHandlers) HandleUpdateMockMode(w http.ResponseWriter, r *http.Request) {
|
||||
var req mockModeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decode mock mode request")
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Update configuration first if provided.
|
||||
currentCfg := mock.GetConfig()
|
||||
if req.Config.NodeCount != nil {
|
||||
if *req.Config.NodeCount <= 0 {
|
||||
http.Error(w, "nodeCount must be greater than zero", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
currentCfg.NodeCount = *req.Config.NodeCount
|
||||
}
|
||||
if req.Config.VMsPerNode != nil {
|
||||
if *req.Config.VMsPerNode < 0 {
|
||||
http.Error(w, "vmsPerNode cannot be negative", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
currentCfg.VMsPerNode = *req.Config.VMsPerNode
|
||||
}
|
||||
if req.Config.LXCsPerNode != nil {
|
||||
if *req.Config.LXCsPerNode < 0 {
|
||||
http.Error(w, "lxcsPerNode cannot be negative", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
currentCfg.LXCsPerNode = *req.Config.LXCsPerNode
|
||||
}
|
||||
if req.Config.RandomMetrics != nil {
|
||||
currentCfg.RandomMetrics = *req.Config.RandomMetrics
|
||||
}
|
||||
if req.Config.HighLoadNodes != nil {
|
||||
currentCfg.HighLoadNodes = req.Config.HighLoadNodes
|
||||
}
|
||||
if req.Config.StoppedPercent != nil {
|
||||
if *req.Config.StoppedPercent < 0 || *req.Config.StoppedPercent > 1 {
|
||||
http.Error(w, "stoppedPercent must be between 0 and 1", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
currentCfg.StoppedPercent = *req.Config.StoppedPercent
|
||||
}
|
||||
|
||||
mock.SetMockConfig(currentCfg)
|
||||
|
||||
if req.Enabled != nil {
|
||||
if h.monitor != nil {
|
||||
h.monitor.SetMockMode(*req.Enabled)
|
||||
} else {
|
||||
mock.SetEnabled(*req.Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
status := struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Config mock.MockConfig `json:"config"`
|
||||
}{
|
||||
Enabled: mock.IsMockEnabled(),
|
||||
Config: mock.GetConfig(),
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode mock mode response")
|
||||
}
|
||||
}
|
||||
|
||||
// AutoRegisterRequest represents a request from the setup script to auto-register a node
|
||||
type AutoRegisterRequest struct {
|
||||
Type string `json:"type"` // "pve" or "pbs"
|
||||
|
||||
+21
-1
@@ -184,6 +184,18 @@ func (r *Router) setupRoutes() {
|
||||
}
|
||||
})
|
||||
|
||||
// Mock mode toggle routes
|
||||
r.mux.HandleFunc("/api/system/mock-mode", func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
configHandlers.HandleGetMockMode(w, req)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
RequireAdmin(configHandlers.config, configHandlers.HandleUpdateMockMode)(w, req)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
// Registration token routes removed - feature deprecated
|
||||
|
||||
// Security routes
|
||||
@@ -884,13 +896,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Also allow static assets without auth (JS, CSS, etc)
|
||||
// These MUST be accessible for the login page to work
|
||||
isStaticAsset := strings.HasPrefix(req.URL.Path, "/assets/") ||
|
||||
strings.HasPrefix(req.URL.Path, "/@vite/") ||
|
||||
strings.HasPrefix(req.URL.Path, "/@solid-refresh") ||
|
||||
strings.HasPrefix(req.URL.Path, "/src/") ||
|
||||
strings.HasPrefix(req.URL.Path, "/node_modules/") ||
|
||||
req.URL.Path == "/" ||
|
||||
req.URL.Path == "/index.html" ||
|
||||
req.URL.Path == "/favicon.ico" ||
|
||||
req.URL.Path == "/logo.svg" ||
|
||||
strings.HasSuffix(req.URL.Path, ".js") ||
|
||||
strings.HasSuffix(req.URL.Path, ".css") ||
|
||||
strings.HasSuffix(req.URL.Path, ".map")
|
||||
strings.HasSuffix(req.URL.Path, ".map") ||
|
||||
strings.HasSuffix(req.URL.Path, ".ts") ||
|
||||
strings.HasSuffix(req.URL.Path, ".tsx") ||
|
||||
strings.HasSuffix(req.URL.Path, ".mjs") ||
|
||||
strings.HasSuffix(req.URL.Path, ".jsx")
|
||||
|
||||
isPublic := isStaticAsset
|
||||
for _, path := range publicPaths {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -446,7 +447,7 @@ type SystemSettings struct {
|
||||
func (c *ConfigPersistence) SaveNodesConfig(pveInstances []PVEInstance, pbsInstances []PBSInstance) error {
|
||||
// CRITICAL: Prevent saving empty nodes when in mock mode
|
||||
// Mock mode should NEVER modify real node configuration
|
||||
if os.Getenv("PULSE_MOCK_MODE") == "true" {
|
||||
if mock.IsMockEnabled() {
|
||||
log.Warn().Msg("Skipping nodes save - mock mode is enabled")
|
||||
return nil // Silently succeed to prevent errors but don't save
|
||||
}
|
||||
|
||||
+211
-98
@@ -3,6 +3,8 @@ package mock
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
@@ -10,63 +12,175 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
mockData models.StateSnapshot
|
||||
// Removed mockAlerts - using real alert manager instead
|
||||
mockAlertHistory []models.Alert
|
||||
mockEnabled bool
|
||||
lastUpdate time.Time
|
||||
updateInterval = 2 * time.Second
|
||||
dataMu sync.RWMutex
|
||||
mockData models.StateSnapshot
|
||||
mockAlerts []models.Alert
|
||||
mockConfig = DefaultConfig
|
||||
enabled atomic.Bool
|
||||
updateTicker *time.Ticker
|
||||
stopUpdatesCh chan struct{}
|
||||
)
|
||||
|
||||
const updateInterval = 2 * time.Second
|
||||
|
||||
func init() {
|
||||
// Check if mock mode is enabled
|
||||
mockEnabled = os.Getenv("PULSE_MOCK_MODE") == "true"
|
||||
initialEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
|
||||
if initialEnabled {
|
||||
log.Info().Msg("Mock mode enabled at startup")
|
||||
}
|
||||
setEnabled(initialEnabled, true)
|
||||
}
|
||||
|
||||
if mockEnabled {
|
||||
log.Info().Msg("Mock mode enabled - using simulated data")
|
||||
// IsMockEnabled returns whether mock mode is enabled.
|
||||
func IsMockEnabled() bool {
|
||||
return enabled.Load()
|
||||
}
|
||||
|
||||
// Load configuration from env vars or use defaults
|
||||
config := LoadMockConfig()
|
||||
// SetEnabled enables or disables mock mode.
|
||||
func SetEnabled(enable bool) {
|
||||
setEnabled(enable, false)
|
||||
}
|
||||
|
||||
// Generate initial mock data
|
||||
mockData = GenerateMockData(config)
|
||||
// Removed fake alert generation - real alert manager will handle this
|
||||
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
lastUpdate = time.Now()
|
||||
// ToggleMockMode enables or disables mock mode at runtime (backwards-compatible helper).
|
||||
func ToggleMockMode(enable bool) {
|
||||
SetEnabled(enable)
|
||||
}
|
||||
|
||||
// Start update ticker
|
||||
go func() {
|
||||
ticker := time.NewTicker(updateInterval)
|
||||
defer ticker.Stop()
|
||||
func setEnabled(enable bool, fromInit bool) {
|
||||
current := enabled.Load()
|
||||
if current == enable {
|
||||
// Still update env so other processes see the latest value when not invoked from init.
|
||||
if !fromInit {
|
||||
setEnvFlag(enable)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
if mockEnabled {
|
||||
UpdateMetrics(&mockData, config)
|
||||
// Removed fake alert regeneration
|
||||
}
|
||||
}
|
||||
}()
|
||||
if enable {
|
||||
enableMockMode(fromInit)
|
||||
} else {
|
||||
disableMockMode()
|
||||
}
|
||||
|
||||
if !fromInit {
|
||||
setEnvFlag(enable)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMockConfig loads mock configuration from environment variables
|
||||
func setEnvFlag(enable bool) {
|
||||
if enable {
|
||||
_ = os.Setenv("PULSE_MOCK_MODE", "true")
|
||||
} else {
|
||||
_ = os.Setenv("PULSE_MOCK_MODE", "false")
|
||||
}
|
||||
}
|
||||
|
||||
func enableMockMode(fromInit bool) {
|
||||
config := LoadMockConfig()
|
||||
|
||||
dataMu.Lock()
|
||||
mockConfig = config
|
||||
mockData = GenerateMockData(config)
|
||||
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
mockData.LastUpdate = time.Now()
|
||||
enabled.Store(true)
|
||||
startUpdateLoopLocked()
|
||||
dataMu.Unlock()
|
||||
|
||||
log.Info().
|
||||
Int("nodes", config.NodeCount).
|
||||
Int("vms_per_node", config.VMsPerNode).
|
||||
Int("lxcs_per_node", config.LXCsPerNode).
|
||||
Bool("random_metrics", config.RandomMetrics).
|
||||
Float64("stopped_percent", config.StoppedPercent).
|
||||
Msg("Mock mode enabled")
|
||||
|
||||
if !fromInit {
|
||||
log.Info().Msg("Mock data generator started")
|
||||
}
|
||||
}
|
||||
|
||||
func disableMockMode() {
|
||||
dataMu.Lock()
|
||||
if !enabled.Load() {
|
||||
dataMu.Unlock()
|
||||
return
|
||||
}
|
||||
enabled.Store(false)
|
||||
stopUpdateLoopLocked()
|
||||
mockData = models.StateSnapshot{}
|
||||
mockAlerts = nil
|
||||
dataMu.Unlock()
|
||||
|
||||
log.Info().Msg("Mock mode disabled")
|
||||
}
|
||||
|
||||
func startUpdateLoopLocked() {
|
||||
stopUpdateLoopLocked()
|
||||
stopUpdatesCh = make(chan struct{})
|
||||
updateTicker = time.NewTicker(updateInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-updateTicker.C:
|
||||
cfg := GetConfig()
|
||||
updateMetrics(cfg)
|
||||
case <-stopUpdatesCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func stopUpdateLoopLocked() {
|
||||
if updateTicker != nil {
|
||||
updateTicker.Stop()
|
||||
updateTicker = nil
|
||||
}
|
||||
if stopUpdatesCh != nil {
|
||||
close(stopUpdatesCh)
|
||||
stopUpdatesCh = nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateMetrics(cfg MockConfig) {
|
||||
if !IsMockEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
dataMu.Lock()
|
||||
defer dataMu.Unlock()
|
||||
|
||||
UpdateMetrics(&mockData, cfg)
|
||||
mockData.LastUpdate = time.Now()
|
||||
}
|
||||
|
||||
// GetConfig returns the current mock configuration.
|
||||
func GetConfig() MockConfig {
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
return mockConfig
|
||||
}
|
||||
|
||||
// LoadMockConfig loads mock configuration from environment variables.
|
||||
func LoadMockConfig() MockConfig {
|
||||
config := DefaultConfig
|
||||
|
||||
if val := os.Getenv("PULSE_MOCK_NODES"); val != "" {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
||||
config.NodeCount = n
|
||||
}
|
||||
}
|
||||
|
||||
if val := os.Getenv("PULSE_MOCK_VMS_PER_NODE"); val != "" {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
if n, err := strconv.Atoi(val); err == nil && n >= 0 {
|
||||
config.VMsPerNode = n
|
||||
}
|
||||
}
|
||||
|
||||
if val := os.Getenv("PULSE_MOCK_LXCS_PER_NODE"); val != "" {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
if n, err := strconv.Atoi(val); err == nil && n >= 0 {
|
||||
config.LXCsPerNode = n
|
||||
}
|
||||
}
|
||||
@@ -81,84 +195,83 @@ func LoadMockConfig() MockConfig {
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("nodes", config.NodeCount).
|
||||
Int("vms_per_node", config.VMsPerNode).
|
||||
Int("lxcs_per_node", config.LXCsPerNode).
|
||||
Bool("random_metrics", config.RandomMetrics).
|
||||
Float64("stopped_percent", config.StoppedPercent).
|
||||
Msg("Mock configuration loaded")
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// IsMockEnabled returns whether mock mode is enabled
|
||||
func IsMockEnabled() bool {
|
||||
return mockEnabled
|
||||
}
|
||||
|
||||
// GetMockState returns the current mock state snapshot
|
||||
func GetMockState() models.StateSnapshot {
|
||||
if !mockEnabled {
|
||||
return models.StateSnapshot{}
|
||||
// SetMockConfig updates the mock configuration dynamically and regenerates data when enabled.
|
||||
func SetMockConfig(cfg MockConfig) {
|
||||
dataMu.Lock()
|
||||
mockConfig = cfg
|
||||
if enabled.Load() {
|
||||
mockData = GenerateMockData(cfg)
|
||||
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
mockData.LastUpdate = time.Now()
|
||||
}
|
||||
|
||||
// Return the current mock data
|
||||
// Don't override alerts - let the real alert manager handle them
|
||||
// mockData.ActiveAlerts = mockAlerts
|
||||
return mockData
|
||||
}
|
||||
|
||||
// ToggleMockMode enables or disables mock mode at runtime
|
||||
func ToggleMockMode(enable bool) {
|
||||
if enable && !mockEnabled {
|
||||
mockEnabled = true
|
||||
config := LoadMockConfig()
|
||||
mockData = GenerateMockData(config)
|
||||
// Removed fake alert generation
|
||||
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
log.Info().
|
||||
Int("history_count", len(mockAlertHistory)).
|
||||
Msg("Mock mode enabled dynamically with alert history")
|
||||
} else if !enable && mockEnabled {
|
||||
mockEnabled = false
|
||||
log.Info().Msg("Mock mode disabled dynamically")
|
||||
}
|
||||
}
|
||||
|
||||
// SetMockConfig updates the mock configuration dynamically
|
||||
func SetMockConfig(nodeCount, vmsPerNode, lxcsPerNode int) {
|
||||
if !mockEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
config := MockConfig{
|
||||
NodeCount: nodeCount,
|
||||
VMsPerNode: vmsPerNode,
|
||||
LXCsPerNode: lxcsPerNode,
|
||||
RandomMetrics: true,
|
||||
StoppedPercent: 0.2,
|
||||
}
|
||||
|
||||
mockData = GenerateMockData(config)
|
||||
// Removed fake alert generation
|
||||
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
dataMu.Unlock()
|
||||
|
||||
log.Info().
|
||||
Int("nodes", nodeCount).
|
||||
Int("vms", vmsPerNode).
|
||||
Int("lxcs", lxcsPerNode).
|
||||
Int("nodes", cfg.NodeCount).
|
||||
Int("vms_per_node", cfg.VMsPerNode).
|
||||
Int("lxcs_per_node", cfg.LXCsPerNode).
|
||||
Bool("random_metrics", cfg.RandomMetrics).
|
||||
Float64("stopped_percent", cfg.StoppedPercent).
|
||||
Msg("Mock configuration updated")
|
||||
}
|
||||
|
||||
// GetMockAlertHistory returns mock alert history
|
||||
// GetMockState returns the current mock state snapshot.
|
||||
func GetMockState() models.StateSnapshot {
|
||||
if !IsMockEnabled() {
|
||||
return models.StateSnapshot{}
|
||||
}
|
||||
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
return cloneState(mockData)
|
||||
}
|
||||
|
||||
// GetMockAlertHistory returns mock alert history.
|
||||
func GetMockAlertHistory(limit int) []models.Alert {
|
||||
if !mockEnabled {
|
||||
if !IsMockEnabled() {
|
||||
return []models.Alert{}
|
||||
}
|
||||
|
||||
if limit > 0 && limit < len(mockAlertHistory) {
|
||||
return mockAlertHistory[:limit]
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
if limit > 0 && limit < len(mockAlerts) {
|
||||
return append([]models.Alert(nil), mockAlerts[:limit]...)
|
||||
}
|
||||
return mockAlertHistory
|
||||
return append([]models.Alert(nil), mockAlerts...)
|
||||
}
|
||||
|
||||
func cloneState(state models.StateSnapshot) models.StateSnapshot {
|
||||
copyState := models.StateSnapshot{
|
||||
Nodes: append([]models.Node(nil), state.Nodes...),
|
||||
VMs: append([]models.VM(nil), state.VMs...),
|
||||
Containers: append([]models.Container(nil), state.Containers...),
|
||||
Storage: append([]models.Storage(nil), state.Storage...),
|
||||
PhysicalDisks: append([]models.PhysicalDisk(nil), state.PhysicalDisks...),
|
||||
PBSInstances: append([]models.PBSInstance(nil), state.PBSInstances...),
|
||||
PBSBackups: append([]models.PBSBackup(nil), state.PBSBackups...),
|
||||
Metrics: append([]models.Metric(nil), state.Metrics...),
|
||||
Performance: state.Performance,
|
||||
Stats: state.Stats,
|
||||
ActiveAlerts: append([]models.Alert(nil), state.ActiveAlerts...),
|
||||
RecentlyResolved: append([]models.ResolvedAlert(nil), state.RecentlyResolved...),
|
||||
LastUpdate: state.LastUpdate,
|
||||
ConnectionHealth: make(map[string]bool, len(state.ConnectionHealth)),
|
||||
}
|
||||
|
||||
copyState.PVEBackups = models.PVEBackups{
|
||||
BackupTasks: append([]models.BackupTask(nil), state.PVEBackups.BackupTasks...),
|
||||
StorageBackups: append([]models.StorageBackup(nil), state.PVEBackups.StorageBackups...),
|
||||
GuestSnapshots: append([]models.GuestSnapshot(nil), state.PVEBackups.GuestSnapshots...),
|
||||
}
|
||||
|
||||
for k, v := range state.ConnectionHealth {
|
||||
copyState.ConnectionHealth[k] = v
|
||||
}
|
||||
|
||||
return copyState
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -68,6 +67,8 @@ type Monitor struct {
|
||||
lastAuthAttempt map[string]time.Time // Track last auth attempt time
|
||||
lastClusterCheck map[string]time.Time // Track last cluster check for standalone nodes
|
||||
persistence *config.ConfigPersistence // Add persistence for saving updated configs
|
||||
runtimeCtx context.Context // Context used while monitor is running
|
||||
wsHub *websocket.Hub // Hub used for broadcasting state
|
||||
}
|
||||
|
||||
// safePercentage calculates percentage safely, returning 0 if divisor is 0
|
||||
@@ -110,6 +111,26 @@ func sortContent(content string) string {
|
||||
|
||||
// GetConnectionStatuses returns the current connection status for all nodes
|
||||
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||
if mock.IsMockEnabled() {
|
||||
statuses := make(map[string]bool)
|
||||
state := mock.GetMockState()
|
||||
for _, node := range state.Nodes {
|
||||
key := "pve-" + node.Name
|
||||
statuses[key] = strings.ToLower(node.Status) == "online"
|
||||
if node.Host != "" {
|
||||
statuses[node.Host] = strings.ToLower(node.Status) == "online"
|
||||
}
|
||||
}
|
||||
for _, pbsInst := range state.PBSInstances {
|
||||
key := "pbs-" + pbsInst.Name
|
||||
statuses[key] = strings.ToLower(pbsInst.Status) != "offline"
|
||||
if pbsInst.Host != "" {
|
||||
statuses[pbsInst.Host] = strings.ToLower(pbsInst.Status) != "offline"
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -212,7 +233,7 @@ func New(cfg *config.Config) (*Monitor, error) {
|
||||
}
|
||||
|
||||
// Check if mock mode is enabled before initializing clients
|
||||
mockEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
|
||||
mockEnabled := mock.IsMockEnabled()
|
||||
|
||||
if mockEnabled {
|
||||
log.Info().Msg("Mock mode enabled - skipping PVE/PBS client initialization")
|
||||
@@ -373,6 +394,11 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||
Dur("pollingInterval", 10*time.Second).
|
||||
Msg("Starting monitoring loop")
|
||||
|
||||
m.mu.Lock()
|
||||
m.runtimeCtx = ctx
|
||||
m.wsHub = wsHub
|
||||
m.mu.Unlock()
|
||||
|
||||
// Initialize and start discovery service if enabled
|
||||
if m.config.DiscoveryEnabled {
|
||||
discoverySubnet := m.config.DiscoverySubnet
|
||||
@@ -455,25 +481,23 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||
broadcastTicker := time.NewTicker(pollingInterval)
|
||||
defer broadcastTicker.Stop()
|
||||
|
||||
// Check if mock mode is enabled
|
||||
mockEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
|
||||
|
||||
// Do an immediate poll on start (only if not in mock mode)
|
||||
if !mockEnabled {
|
||||
go m.poll(ctx, wsHub)
|
||||
} else {
|
||||
if mock.IsMockEnabled() {
|
||||
log.Info().Msg("Mock mode enabled - skipping real node polling")
|
||||
go m.checkMockAlerts()
|
||||
} else {
|
||||
go m.poll(ctx, wsHub)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pollTicker.C:
|
||||
// Start polling in a goroutine so it doesn't block the ticker (only if not in mock mode)
|
||||
if !mockEnabled {
|
||||
go m.poll(ctx, wsHub)
|
||||
} else {
|
||||
// In mock mode, still check alerts for mock data
|
||||
if mock.IsMockEnabled() {
|
||||
// In mock mode, keep synthetic alerts fresh
|
||||
go m.checkMockAlerts()
|
||||
} else {
|
||||
// Poll real infrastructure
|
||||
go m.poll(ctx, wsHub)
|
||||
}
|
||||
|
||||
case <-broadcastTicker.C:
|
||||
@@ -2759,6 +2783,53 @@ func (m *Monitor) GetState() models.StateSnapshot {
|
||||
return m.state.GetSnapshot()
|
||||
}
|
||||
|
||||
// SetMockMode switches between mock data and real infrastructure data at runtime.
|
||||
func (m *Monitor) SetMockMode(enable bool) {
|
||||
current := mock.IsMockEnabled()
|
||||
if current == enable {
|
||||
log.Info().Bool("mockMode", enable).Msg("Mock mode already in desired state")
|
||||
return
|
||||
}
|
||||
|
||||
if enable {
|
||||
mock.SetEnabled(true)
|
||||
m.alertManager.ClearActiveAlerts()
|
||||
m.mu.Lock()
|
||||
m.resetStateLocked()
|
||||
m.mu.Unlock()
|
||||
log.Info().Msg("Switched monitor to mock mode")
|
||||
} else {
|
||||
mock.SetEnabled(false)
|
||||
m.alertManager.ClearActiveAlerts()
|
||||
m.mu.Lock()
|
||||
m.resetStateLocked()
|
||||
m.mu.Unlock()
|
||||
log.Info().Msg("Switched monitor to real data mode")
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
ctx := m.runtimeCtx
|
||||
hub := m.wsHub
|
||||
m.mu.RUnlock()
|
||||
|
||||
if hub != nil {
|
||||
hub.BroadcastState(m.GetState())
|
||||
}
|
||||
|
||||
if !enable && ctx != nil && hub != nil {
|
||||
// Kick off an immediate poll to repopulate state with live data
|
||||
go m.poll(ctx, hub)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) resetStateLocked() {
|
||||
m.state = models.NewState()
|
||||
m.state.Stats = models.Stats{
|
||||
StartTime: m.startTime,
|
||||
Version: "2.0.0-go",
|
||||
}
|
||||
}
|
||||
|
||||
// GetStartTime returns the monitor start time
|
||||
func (m *Monitor) GetStartTime() time.Time {
|
||||
return m.startTime
|
||||
@@ -3362,21 +3433,23 @@ func (m *Monitor) checkMockAlerts() {
|
||||
Msg("Checking alerts for mock data")
|
||||
|
||||
// Clean up alerts for nodes that no longer exist
|
||||
// Use the mock state nodes since we haven't updated global state yet
|
||||
existingNodes := make(map[string]bool)
|
||||
for _, node := range state.Nodes {
|
||||
existingNodes[node.Name] = true
|
||||
if node.Host != "" {
|
||||
existingNodes[node.Host] = true
|
||||
}
|
||||
}
|
||||
// Also add any real nodes from the global state
|
||||
allState := m.state.GetSnapshot()
|
||||
for _, node := range allState.Nodes {
|
||||
existingNodes[node.Name] = true
|
||||
for _, pbsInst := range state.PBSInstances {
|
||||
existingNodes[pbsInst.Name] = true
|
||||
existingNodes["pbs-"+pbsInst.Name] = true
|
||||
if pbsInst.Host != "" {
|
||||
existingNodes[pbsInst.Host] = true
|
||||
}
|
||||
}
|
||||
log.Info().
|
||||
Int("mockNodes", len(state.Nodes)).
|
||||
Int("stateNodes", len(allState.Nodes)).
|
||||
Int("totalNodes", len(existingNodes)).
|
||||
Msg("Collecting nodes for alert cleanup")
|
||||
Int("trackedNodes", len(existingNodes)).
|
||||
Msg("Collecting resources for alert cleanup in mock mode")
|
||||
m.alertManager.CleanupAlertsForNodes(existingNodes)
|
||||
|
||||
// Limit how many guests we check per cycle to prevent blocking with large datasets
|
||||
|
||||
Reference in New Issue
Block a user