mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
fix(discovery): backfill availability suggestions for existing discoveries
The refreshSuggestedAvailabilityProbeFromState method existed but was never called, so existing discovery records never received availability probe suggestions. This adds backfillAvailabilitySuggestions which: - Triggers from SetReadState (goroutine) and runDiscoveryLoop (ticker) - Retries with exponential backoff when the state snapshot is empty, waiting for the monitor to populate Proxmox data before proceeding - Matches containers by VMID only (was VMID+Node, which failed in clusters where the discovery targetID differs from the container's node) Also extends SuggestAvailabilityProbe with a hostname fallback: when ServiceType is empty (deep scan not yet run), checks the discovery's Hostname against webServiceDefaults/tcpServiceDefaults. This covers containers like jellyfin, grafana, frigate, esphome, zigbee2mqtt, homeassistant, mqtt, etc. in environments where background AI is disabled. Adds zigbee2mqtt and ntfy to webServiceDefaults.
This commit is contained in:
parent
3ea61e117d
commit
fa3f57e6ba
3 changed files with 112 additions and 4 deletions
|
|
@ -59,6 +59,31 @@ func SuggestAvailabilityProbe(discovery *ResourceDiscovery, hostIP string) *Avai
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: try hostname/name against the defaults maps.
|
||||
// This covers containers where the deep scan hasn't populated ServiceType yet.
|
||||
hostnameNormalized := normalizeServiceTypeForLookup(discovery.Hostname)
|
||||
if hostnameNormalized != "" && hostnameNormalized != normalized {
|
||||
if defaults, matched, ok := lookupWebServiceDefault(hostnameNormalized); ok {
|
||||
return &AvailabilityProbeSuggestion{
|
||||
Protocol: defaults.Protocol,
|
||||
Address: hostIP,
|
||||
Port: defaults.Port,
|
||||
Path: defaults.Path,
|
||||
ServiceName: pickServiceName(discovery, matched),
|
||||
Reason: fmt.Sprintf("hostname match: %s", matched),
|
||||
}
|
||||
}
|
||||
if port, matched, ok := lookupTCPServiceDefault(hostnameNormalized); ok {
|
||||
return &AvailabilityProbeSuggestion{
|
||||
Protocol: "tcp",
|
||||
Address: hostIP,
|
||||
Port: port,
|
||||
ServiceName: pickServiceName(discovery, matched),
|
||||
Reason: fmt.Sprintf("hostname tcp match: %s", matched),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -411,8 +411,9 @@ func (s *Service) SetAIAnalyzer(analyzer AIAnalyzer) {
|
|||
// When set, getSnapshot() uses ReadState to build infrastructure snapshots.
|
||||
func (s *Service) SetReadState(rs unifiedresources.ReadState) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.readState = rs
|
||||
s.mu.Unlock()
|
||||
go s.backfillAvailabilitySuggestions(context.Background())
|
||||
}
|
||||
|
||||
// Start begins the background discovery service.
|
||||
|
|
@ -588,6 +589,7 @@ func (s *Service) runDiscoveryLoop(ctx context.Context, stopCh <-chan struct{},
|
|||
|
||||
s.collectFingerprints(ctx)
|
||||
s.runAutomaticDiscoveryRefresh(ctx)
|
||||
s.backfillAvailabilitySuggestions(ctx)
|
||||
|
||||
s.mu.RLock()
|
||||
currentInterval := s.interval
|
||||
|
|
@ -602,6 +604,7 @@ func (s *Service) runDiscoveryLoop(ctx context.Context, stopCh <-chan struct{},
|
|||
case <-ticker.C:
|
||||
s.collectFingerprints(ctx)
|
||||
s.runAutomaticDiscoveryRefresh(ctx)
|
||||
s.backfillAvailabilitySuggestions(ctx)
|
||||
case newInterval := <-s.intervalCh:
|
||||
// Interval changed - reset the ticker
|
||||
newInterval = normalizeDiscoveryInterval(newInterval)
|
||||
|
|
@ -2289,19 +2292,18 @@ func (s *Service) getResourceExternalIP(req DiscoveryRequest) string {
|
|||
switch req.ResourceType {
|
||||
case ResourceTypeSystemContainer:
|
||||
for _, c := range snap.Containers {
|
||||
if fmt.Sprintf("%d", c.VMID) == req.ResourceID && c.Node == requestTargetID {
|
||||
if fmt.Sprintf("%d", c.VMID) == req.ResourceID {
|
||||
if len(c.IPAddresses) > 0 {
|
||||
return c.IPAddresses[0]
|
||||
}
|
||||
if candidate := firstResourceHostnameCandidate(req.ResourceID, requestTargetID, c.Name, req.Hostname); candidate != "" {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
case ResourceTypeVM:
|
||||
for _, vm := range snap.VMs {
|
||||
if fmt.Sprintf("%d", vm.VMID) == req.ResourceID && vm.Node == requestTargetID {
|
||||
if fmt.Sprintf("%d", vm.VMID) == req.ResourceID {
|
||||
if len(vm.IPAddresses) > 0 {
|
||||
return vm.IPAddresses[0]
|
||||
}
|
||||
|
|
@ -3169,6 +3171,85 @@ func (s *Service) refreshSuggestedAvailabilityProbeFromState(d *ResourceDiscover
|
|||
return true
|
||||
}
|
||||
|
||||
func (s *Service) backfillAvailabilitySuggestions(ctx context.Context) {
|
||||
if ctx == nil || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if !s.hasStateAccess() || s.store == nil {
|
||||
return
|
||||
}
|
||||
|
||||
discoveries, err := s.store.List()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to list discoveries for availability suggestion backfill")
|
||||
return
|
||||
}
|
||||
if len(discoveries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
const maxEmptyRetries = 6
|
||||
initialDelay := 10 * time.Second
|
||||
|
||||
for attempt := 0; attempt < maxEmptyRetries; attempt++ {
|
||||
snap, ok := s.getSnapshot()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
hasInfra := len(snap.Containers) > 0 || len(snap.VMs) > 0 || len(snap.DockerHosts) > 0
|
||||
if hasInfra || attempt == maxEmptyRetries-1 {
|
||||
if !hasInfra {
|
||||
log.Warn().Msg("backfill: state snapshot still empty after retries; proceeding with best effort")
|
||||
}
|
||||
break
|
||||
}
|
||||
log.Info().Dur("delay", initialDelay).Int("attempt", attempt+1).Msg("backfill: state snapshot empty, waiting for monitor data")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(initialDelay):
|
||||
}
|
||||
initialDelay *= 2
|
||||
}
|
||||
|
||||
log.Info().Int("discoveries", len(discoveries)).Msg("backfill: processing discoveries")
|
||||
updated := 0
|
||||
for _, d := range discoveries {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if d.SuggestedAvailabilityProbe != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceType, targetID, resourceID, err := ParseResourceID(d.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
req := DiscoveryRequest{
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
TargetID: targetID,
|
||||
}
|
||||
|
||||
externalIP := s.getResourceExternalIP(req)
|
||||
suggestion := SuggestAvailabilityProbe(d, externalIP)
|
||||
if suggestion != nil {
|
||||
d.SuggestedAvailabilityProbe = suggestion
|
||||
if err := s.store.Save(d); err != nil {
|
||||
log.Warn().Err(err).Str("id", d.ID).Msg("Failed to save backfilled availability suggestion")
|
||||
} else {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updated > 0 {
|
||||
log.Info().Int("count", updated).Msg("Backfilled availability probe suggestions")
|
||||
}
|
||||
}
|
||||
|
||||
// lookupHostnameFromState finds the hostname/name for a resource from state
|
||||
func (s *Service) lookupHostnameFromState(resourceType ResourceType, hostID, resourceID string, snap StateSnapshot) string {
|
||||
switch resourceType {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ var webServiceDefaults = map[string]webServiceDefault{
|
|||
"domoticz": {8080, "http", ""},
|
||||
"node-red": {1880, "http", ""},
|
||||
"nodered": {1880, "http", ""},
|
||||
"zigbee2mqtt": {8080, "http", ""},
|
||||
"ntfy": {80, "http", ""},
|
||||
|
||||
// Monitoring
|
||||
"grafana": {3000, "http", ""},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue