mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 00:21:57 +00:00
Canonicalize Proxmox-guest discovery records to the node-name key
Discovery records for Proxmox guests (VM / system-container) are canonically keyed by node name + VMID — the form the background fingerprint loop and the Assistant prefetch already use. But the browser action path addresses guests by the linked agent UUID (discoveryTarget.agentId, the action-authorization target), and normalizeDiscoveryRequest was a no-op for guest types — so a UUID-targeted trigger/lookup stored and read records under a second, divergent key. Result: the resource drawer reported "Not discovered" for a guest the background loop had already discovered, and duplicate records accumulated (node-keyed + UUID-keyed). normalizeDiscoveryRequest now canonicalizes guest targets to the node name: resolve a linked-agent-UUID target back to its hosting node (node->linked-agent map, falling back to the agent host's hostname when it is a known node). Both creation (DiscoverResource) and lookup (GetDiscoveryByResource) funnel through this chokepoint, so every path converges on the node-name key; the caller already aliases the original target, so pre-existing UUID-keyed records are still found and consolidated onto the canonical key on the next run. The agent UUID is unchanged as the action-authorization target — this governs only the record key, so the discoveryTarget security contract is untouched. - Test: TestService_ProxmoxGuestDiscoveryCanonicalizesToNodeKey. - Live-verified on the dev instance: the Home Assistant LXC drawer readiness went missing -> fresh (discoveryId system-container:delly:101).
This commit is contained in:
parent
d29d9684db
commit
5321c8b7a2
2 changed files with 131 additions and 1 deletions
|
|
@ -2022,7 +2022,19 @@ func (s *Service) normalizeDiscoveryRequest(req DiscoveryRequest, aliasIDs *[]st
|
|||
req = normalizeDiscoveryRequestAliases(req)
|
||||
requestTargetID := req.TargetID
|
||||
|
||||
if req.ResourceType != ResourceTypeAgent {
|
||||
switch req.ResourceType {
|
||||
case ResourceTypeVM, ResourceTypeSystemContainer:
|
||||
// Proxmox guests are canonically keyed by node name + VMID. The browser
|
||||
// action path addresses them by the linked agent UUID (the
|
||||
// discoveryTarget.agentId action-authorization target); resolve that back
|
||||
// to the hosting node so the stored/looked-up record key matches the
|
||||
// background fingerprint loop and the Assistant prefetch (which already
|
||||
// use node name). See canonicalizeProxmoxGuestDiscoveryTarget.
|
||||
return s.canonicalizeProxmoxGuestDiscoveryTarget(req, requestTargetID)
|
||||
case ResourceTypeAgent:
|
||||
// Agent discovery is canonically keyed by the agent UUID; the logic below
|
||||
// redirects a node-name/hostname target to its linked agent.
|
||||
default:
|
||||
return req
|
||||
}
|
||||
snap, ok := s.getSnapshot()
|
||||
|
|
@ -2081,6 +2093,75 @@ func (s *Service) normalizeDiscoveryRequest(req DiscoveryRequest, aliasIDs *[]st
|
|||
return req
|
||||
}
|
||||
|
||||
// canonicalizeProxmoxGuestDiscoveryTarget canonicalizes a Proxmox guest (VM /
|
||||
// system-container) discovery request to the stable NODE-NAME key. Discovery
|
||||
// records for guests are canonically keyed by node name + VMID — the form the
|
||||
// background fingerprint loop and the Assistant prefetch already use. The browser
|
||||
// action path, however, addresses guests by the linked agent UUID (the
|
||||
// discoveryTarget.agentId action-authorization target). Left unnormalized, a
|
||||
// UUID-targeted request stored and looked records up under a second, divergent
|
||||
// key, so the resource drawer reported "not discovered" for a guest the
|
||||
// background loop had already discovered. Resolve a UUID-form target back to its
|
||||
// hosting node name; the caller has already registered the original (UUID-form)
|
||||
// ID as an alias, so any record previously stored under it is still found by
|
||||
// lookups and consolidated onto the canonical key on the next discovery run.
|
||||
//
|
||||
// The agent UUID is unchanged as the action target — this only governs the
|
||||
// record key. Resolution prefers the node→linked-agent map, then falls back to
|
||||
// the agent host's hostname when that hostname is itself a known node (a
|
||||
// Proxmox-node agent reports hostname == node name, which is how the command
|
||||
// executor already resolves targets).
|
||||
func (s *Service) canonicalizeProxmoxGuestDiscoveryTarget(req DiscoveryRequest, requestTargetID string) DiscoveryRequest {
|
||||
if strings.TrimSpace(requestTargetID) == "" {
|
||||
return req
|
||||
}
|
||||
snap, ok := s.getSnapshot()
|
||||
if !ok {
|
||||
return req
|
||||
}
|
||||
|
||||
nodeNames := make(map[string]struct{}, len(snap.Nodes))
|
||||
nodeByLinkedAgent := make(map[string]string, len(snap.Nodes))
|
||||
for _, node := range snap.Nodes {
|
||||
if node.Name != "" {
|
||||
nodeNames[node.Name] = struct{}{}
|
||||
}
|
||||
if node.LinkedAgentID != "" && node.Name != "" {
|
||||
nodeByLinkedAgent[node.LinkedAgentID] = node.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Already a node name — canonical, nothing to resolve.
|
||||
if _, ok := nodeNames[requestTargetID]; ok {
|
||||
return req
|
||||
}
|
||||
|
||||
// The target is the linked agent UUID: resolve it to the hosting node name.
|
||||
if nodeName := nodeByLinkedAgent[requestTargetID]; nodeName != "" {
|
||||
req.TargetID = nodeName
|
||||
if req.Hostname == "" {
|
||||
req.Hostname = nodeName
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// Fallback: match the agent host by ID and adopt its hostname when that
|
||||
// hostname is a known node.
|
||||
for _, host := range snap.Hosts {
|
||||
if host.ID == requestTargetID && host.Hostname != "" {
|
||||
if _, ok := nodeNames[host.Hostname]; ok {
|
||||
req.TargetID = host.Hostname
|
||||
if req.Hostname == "" {
|
||||
req.Hostname = host.Hostname
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func (s *Service) cleanupAliasedDiscoveries(canonicalID string, aliasIDs []string) {
|
||||
seen := make(map[string]struct{}, len(aliasIDs))
|
||||
for _, aliasID := range aliasIDs {
|
||||
|
|
|
|||
|
|
@ -1663,6 +1663,55 @@ func TestParseDockerMounts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestService_ProxmoxGuestDiscoveryCanonicalizesToNodeKey(t *testing.T) {
|
||||
store, err := NewStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore error: %v", err)
|
||||
}
|
||||
store.crypto = nil
|
||||
|
||||
// A Proxmox node "delly" whose linked agent is a UUID, hosting CT 101.
|
||||
const nodeName = "delly"
|
||||
const agentUUID = "7d465a78-80b8-4255-999c-88224ae57b4f"
|
||||
state := StateSnapshot{
|
||||
Nodes: []Node{{ID: "delly-id", Name: nodeName, LinkedAgentID: agentUUID}},
|
||||
Hosts: []Host{{ID: agentUUID, Hostname: nodeName}},
|
||||
}
|
||||
service := NewService(store, nil, DefaultConfig())
|
||||
service.SetReadState(readStateFromSnapshot(state))
|
||||
|
||||
// The background loop stored the discovery under the canonical node-name key.
|
||||
canonicalID := MakeResourceID(ResourceTypeSystemContainer, nodeName, "101")
|
||||
if err := store.Save(&ResourceDiscovery{
|
||||
ID: canonicalID,
|
||||
ResourceType: ResourceTypeSystemContainer,
|
||||
TargetID: nodeName,
|
||||
ResourceID: "101",
|
||||
ServiceName: "Home Assistant",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed discovery: %v", err)
|
||||
}
|
||||
|
||||
// The drawer/readiness path looks the record up by the linked agent UUID (the
|
||||
// discoveryTarget.agentId action target). It must canonicalize to the
|
||||
// node-name key and find the record, not report "not discovered".
|
||||
got, err := service.GetDiscoveryByResource(ResourceTypeSystemContainer, agentUUID, "101")
|
||||
if err != nil {
|
||||
t.Fatalf("GetDiscoveryByResource(agent-uuid) error: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected the node-name discovery record to be found via the agent-UUID target")
|
||||
}
|
||||
if got.ID != canonicalID {
|
||||
t.Fatalf("found discovery ID = %q, want canonical node-name key %q", got.ID, canonicalID)
|
||||
}
|
||||
|
||||
// The node-name target (background loop / Assistant) still finds it directly.
|
||||
if got2, err := service.GetDiscoveryByResource(ResourceTypeSystemContainer, nodeName, "101"); err != nil || got2 == nil {
|
||||
t.Fatalf("node-name lookup failed: got=%v err=%v", got2, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Redirection(t *testing.T) {
|
||||
// Setup store
|
||||
store, err := NewStore(t.TempDir())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue