Fix reload-driven PVE host linking consistency (#1269)

This commit is contained in:
rcourtman 2026-03-26 09:01:23 +00:00
parent 0a7b93a842
commit 42a84fc5ca
2 changed files with 125 additions and 0 deletions

View file

@ -1534,9 +1534,69 @@ func (s *State) UpdateNodesForInstance(instanceName string, nodes []Node) {
})
s.Nodes = newNodes
s.reconcileHostNodeLinksLocked()
s.LastUpdate = time.Now()
}
// reconcileHostNodeLinksLocked backfills host-side LinkedNodeID values from the
// authoritative node-side LinkedHostAgentID links after node refreshes. This is
// especially important after reloads/auto-registration, where nodes are rebuilt
// from config and can recover their linked host agent by hostname before the host
// agent has sent another report.
func (s *State) reconcileHostNodeLinksLocked() {
if len(s.Hosts) == 0 || len(s.Nodes) == 0 {
return
}
validNodeIDs := make(map[string]struct{}, len(s.Nodes))
uniqueNodeForHost := make(map[string]string)
ambiguousHosts := make(map[string]struct{})
for _, node := range s.Nodes {
nodeID := strings.TrimSpace(node.ID)
if nodeID == "" {
continue
}
validNodeIDs[nodeID] = struct{}{}
hostID := strings.TrimSpace(node.LinkedHostAgentID)
if hostID == "" {
continue
}
if _, ambiguous := ambiguousHosts[hostID]; ambiguous {
continue
}
if existingNodeID, ok := uniqueNodeForHost[hostID]; ok && existingNodeID != nodeID {
delete(uniqueNodeForHost, hostID)
ambiguousHosts[hostID] = struct{}{}
continue
}
uniqueNodeForHost[hostID] = nodeID
}
for i := range s.Hosts {
currentNodeID := strings.TrimSpace(s.Hosts[i].LinkedNodeID)
candidateNodeID, hasCandidate := uniqueNodeForHost[s.Hosts[i].ID]
switch {
case currentNodeID != "":
if _, ok := validNodeIDs[currentNodeID]; !ok {
if hasCandidate {
s.Hosts[i].LinkedNodeID = candidateNodeID
} else {
s.Hosts[i].LinkedNodeID = ""
}
}
case hasCandidate:
s.Hosts[i].LinkedNodeID = candidateNodeID
}
if s.Hosts[i].LinkedNodeID != "" {
s.Hosts[i].LinkedVMID = ""
s.Hosts[i].LinkedContainerID = ""
}
}
}
// UpdateVMs updates the VMs in the state
func (s *State) UpdateVMs(vms []VM) {
s.mu.Lock()

View file

@ -186,6 +186,71 @@ func TestStateLinkHostAgentToNode(t *testing.T) {
}
}
func TestStateUpdateNodesForInstanceBackfillsHostLinkedNodeID(t *testing.T) {
state := &State{
Hosts: []Host{
{ID: "host-1", Hostname: "pve01.local"},
},
}
state.UpdateNodesForInstance("cluster-a", []Node{
{ID: "node-1", Instance: "cluster-a", Name: "pve01"},
})
if len(state.Nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(state.Nodes))
}
if state.Nodes[0].LinkedHostAgentID != "host-1" {
t.Fatalf("LinkedHostAgentID = %q, want host-1", state.Nodes[0].LinkedHostAgentID)
}
if state.Hosts[0].LinkedNodeID != "node-1" {
t.Fatalf("LinkedNodeID = %q, want node-1", state.Hosts[0].LinkedNodeID)
}
}
func TestStateUpdateNodesForInstanceRepairsStaleHostLinkedNodeID(t *testing.T) {
state := &State{
Hosts: []Host{
{ID: "host-1", Hostname: "pve01.local", LinkedNodeID: "node-old"},
},
Nodes: []Node{
{ID: "node-old", Instance: "cluster-a", Name: "pve01", LinkedHostAgentID: "host-1"},
},
}
state.UpdateNodesForInstance("cluster-a", []Node{
{ID: "node-new", Instance: "cluster-a", Name: "pve01"},
})
if len(state.Nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(state.Nodes))
}
if state.Nodes[0].LinkedHostAgentID != "host-1" {
t.Fatalf("LinkedHostAgentID = %q, want host-1", state.Nodes[0].LinkedHostAgentID)
}
if state.Hosts[0].LinkedNodeID != "node-new" {
t.Fatalf("LinkedNodeID = %q, want node-new", state.Hosts[0].LinkedNodeID)
}
}
func TestStateUpdateNodesForInstanceDoesNotBackfillAmbiguousHostLink(t *testing.T) {
state := &State{
Hosts: []Host{
{ID: "host-1", Hostname: "pve01.local"},
},
Nodes: []Node{
{ID: "node-1", Instance: "cluster-a", Name: "pve01", LinkedHostAgentID: "host-1"},
{ID: "node-2", Instance: "cluster-b", Name: "pve01", LinkedHostAgentID: "host-1"},
},
}
state.UpdateNodesForInstance("cluster-c", nil)
if state.Hosts[0].LinkedNodeID != "" {
t.Fatalf("LinkedNodeID = %q, want empty", state.Hosts[0].LinkedNodeID)
}
}
func TestStateSnapshotPreservesEmptyTemplateInventoryReadiness(t *testing.T) {
state := &State{}