mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Disambiguate same-name Proxmox agent links
Use unique provider-observed node interface addresses to restore automatic host-agent linking when standalone sites share a short hostname but use distinct FQDN endpoints. Keep reused addresses ambiguous and fail closed. Refs #1753 Contract-Neutral: narrows internal agent-to-node identity reconciliation without changing API or extension shapes
This commit is contained in:
@@ -3924,6 +3924,38 @@ func collectReportedHostIPs(
|
||||
return ips
|
||||
}
|
||||
|
||||
func collectNodeNetworkIPs(network []unifiedresources.NetworkInterface) map[string]struct{} {
|
||||
ips := make(map[string]struct{})
|
||||
for _, nic := range network {
|
||||
for _, address := range nic.Addresses {
|
||||
if normalized := unifiedresources.NormalizeIP(address); normalized != "" {
|
||||
ips[normalized] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// uniqueNodeNetworkMatchesReportedHints accepts only provider-observed node
|
||||
// addresses that belong to one current Proxmox node. This disambiguates the
|
||||
// common FQDN-endpoint/short-hostname case without guessing when separate
|
||||
// sites reuse an RFC1918 address or every node exposes the same bridge IP.
|
||||
func uniqueNodeNetworkMatchesReportedHints(
|
||||
network []unifiedresources.NetworkInterface,
|
||||
reportedIPs map[string]struct{},
|
||||
ownerCounts map[string]int,
|
||||
) bool {
|
||||
for ip := range collectNodeNetworkIPs(network) {
|
||||
if ownerCounts[ip] != 1 {
|
||||
continue
|
||||
}
|
||||
if _, reported := reportedIPs[ip]; reported {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointHostMatchesReportedHints(
|
||||
endpointHost string,
|
||||
reportedHostname string,
|
||||
@@ -3977,6 +4009,7 @@ func (m *Monitor) findLinkedProxmoxEntityWithHints(
|
||||
if readState == nil {
|
||||
return "", "", ""
|
||||
}
|
||||
nodes := readState.Nodes()
|
||||
|
||||
type linkedEntityMatch struct {
|
||||
id string
|
||||
@@ -3984,13 +4017,21 @@ func (m *Monitor) findLinkedProxmoxEntityWithHints(
|
||||
}
|
||||
|
||||
reportedIPs := collectReportedHostIPs(reportIP, network)
|
||||
nodeIPOwnerCounts := make(map[string]int)
|
||||
for _, node := range nodes {
|
||||
for ip := range collectNodeNetworkIPs(node.NetworkInterfaces()) {
|
||||
nodeIPOwnerCounts[ip]++
|
||||
}
|
||||
}
|
||||
|
||||
// First, try to match the configured PVE node endpoint against the host report.
|
||||
// This is stronger than node-name matching and can disambiguate clustered nodes
|
||||
// that share the same short hostname but have different management addresses.
|
||||
// First, try to match the configured PVE node endpoint or a unique address
|
||||
// from the provider-observed node network inventory against the host report.
|
||||
// Both are stronger than node-name matching and can disambiguate nodes that
|
||||
// share the same short hostname but have different management addresses.
|
||||
var endpointMatchedNodes []linkedEntityMatch
|
||||
for _, node := range readState.Nodes() {
|
||||
if endpointHostMatchesReportedHints(extractHostname(node.HostURL()), hostname, reportedIPs) {
|
||||
for _, node := range nodes {
|
||||
if endpointHostMatchesReportedHints(extractHostname(node.HostURL()), hostname, reportedIPs) ||
|
||||
uniqueNodeNetworkMatchesReportedHints(node.NetworkInterfaces(), reportedIPs, nodeIPOwnerCounts) {
|
||||
endpointMatchedNodes = append(endpointMatchedNodes, linkedEntityMatch{
|
||||
id: node.SourceID(),
|
||||
instance: node.Instance(),
|
||||
@@ -4011,7 +4052,7 @@ func (m *Monitor) findLinkedProxmoxEntityWithHints(
|
||||
|
||||
// Check PVE nodes first - but detect ambiguity when multiple nodes match
|
||||
var matchingNodes []linkedEntityMatch
|
||||
for _, node := range readState.Nodes() {
|
||||
for _, node := range nodes {
|
||||
if matchHostname(node.Name()) {
|
||||
matchingNodes = append(matchingNodes, linkedEntityMatch{
|
||||
id: node.SourceID(),
|
||||
|
||||
@@ -586,6 +586,89 @@ func TestFindLinkedProxmoxEntityWithHints_UsesExactEndpointHostnameBeforeNameFal
|
||||
}
|
||||
}
|
||||
|
||||
// The v6.4.0-rc.12 state reconciliation fix keeps standalone providers with
|
||||
// the same native node name separate, but the later agent report must still be
|
||||
// able to attach to the right node. In the common case the configured endpoint
|
||||
// is an FQDN while Proxmox and the agent both report the short hostname. The
|
||||
// provider-observed node interface is the remaining machine-local evidence.
|
||||
// Refs #1753.
|
||||
func TestFindLinkedProxmoxEntityWithHints_UsesUniqueNodeNetworkToDisambiguateFQDNEndpoints(t *testing.T) {
|
||||
monitor := &Monitor{
|
||||
state: models.NewState(),
|
||||
}
|
||||
|
||||
monitor.state.UpdateNodes([]models.Node{
|
||||
{
|
||||
ID: "staging-pve01",
|
||||
Name: "pve01",
|
||||
Instance: "staging",
|
||||
Host: "https://pve01.staging.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"192.0.2.11/24"}},
|
||||
{Name: "docker0", Addresses: []string{"172.17.0.1/16"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "production-pve01",
|
||||
Name: "pve01",
|
||||
Instance: "production",
|
||||
Host: "https://pve01.production.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"198.51.100.21/24"}},
|
||||
{Name: "docker0", Addresses: []string{"172.17.0.1/16"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
nodeID, vmID, ctID := monitor.findLinkedProxmoxEntityWithHints(
|
||||
"pve01",
|
||||
"",
|
||||
[]agentshost.NetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"198.51.100.21/24"}},
|
||||
{Name: "docker0", Addresses: []string{"172.17.0.1/16"}},
|
||||
},
|
||||
)
|
||||
if nodeID != "production-pve01" || vmID != "" || ctID != "" {
|
||||
t.Fatalf("expected unique provider network evidence to select production node, got node=%q vm=%q ct=%q", nodeID, vmID, ctID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindLinkedProxmoxEntityWithHints_SharedNodeNetworkRemainsAmbiguous(t *testing.T) {
|
||||
monitor := &Monitor{
|
||||
state: models.NewState(),
|
||||
}
|
||||
|
||||
monitor.state.UpdateNodes([]models.Node{
|
||||
{
|
||||
ID: "staging-pve01",
|
||||
Name: "pve01",
|
||||
Instance: "staging",
|
||||
Host: "https://pve01.staging.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"192.168.1.11/24"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "production-pve01",
|
||||
Name: "pve01",
|
||||
Instance: "production",
|
||||
Host: "https://pve01.production.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"192.168.1.11/24"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
nodeID, vmID, ctID := monitor.findLinkedProxmoxEntityWithHints(
|
||||
"pve01",
|
||||
"",
|
||||
[]agentshost.NetworkInterface{{Name: "vmbr0", Addresses: []string{"192.168.1.11/24"}}},
|
||||
)
|
||||
if nodeID != "" || vmID != "" || ctID != "" {
|
||||
t.Fatalf("shared cross-site address must remain ambiguous, got node=%q vm=%q ct=%q", nodeID, vmID, ctID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostSensorsFromReadStateViewPreservesTypedSensorData(t *testing.T) {
|
||||
customValue := 12.5
|
||||
observedAt := time.Date(2026, 7, 30, 19, 0, 0, 0, time.UTC)
|
||||
|
||||
Reference in New Issue
Block a user