mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 12:42:49 +00:00
8c9000ac63
Two Proxmox connections can each have a node with the same name, which is common for MSPs running one px1 per site. The node temperature lookup fell back to the first host agent whose hostname matched the node name, even when that agent was linked to the other site's node, and even when the name was ambiguous enough that the agent linker had refused to link it. Site B's px1 then showed, alerted on and recorded history for site A's sensors. The cluster sensor cache leaked the same way: an agent's SSH readings for its cluster siblings were keyed by bare node name, so a site A sibling reading fed site B's node. The poller now hands the lookup the polled node, and the lookup works on that node's slot. The state folds the views of one machine reached through two connections (a cluster added twice, or a multi-homed host added by each address) into one node held under one view's ID, and the agent link follows it. Those views used to find their agent only through the loose hostname match, so a polled view the read state does not hold also takes the one same-named node proven to be the same machine. models.NodeObservationsSameMachine and models.HostAgentBridgesNodeViews give that proof from the evidence UpdateNodesForInstance folds on, and the state's agent corroboration check now shares its rule with them. Ambiguous candidates are declined, as the state declines an ambiguous alias. An agent linked to the slot is the node's agent. The hostname fallback only takes an agent with no link when no node outside the slot and no second unlinked agent has the name. Cluster sensor cache entries record their reporting agent and serve a node only when that agent's linked node shares one of the slot's connections, or belongs to a connection whose configured view of the node has a matching TLS fingerprint. LinkedNodeID holds the Proxmox source node ID for automatic, manual and continuity-restored links, so the link match needs no normalization. The physical-disk SMART merge already matches agents by node.LinkedAgentID within one instance and is unchanged.
83 lines
3.1 KiB
Go
83 lines
3.1 KiB
Go
package models
|
|
|
|
import "strings"
|
|
|
|
// NodeObservationsSameMachine reports whether two observations of a Proxmox
|
|
// node are one machine by the evidence UpdateNodesForInstance folds node views
|
|
// into one slot on: the same logical key, or the same node name with no
|
|
// provider identity conflict and either positive cross-view proof (the same
|
|
// connection, the same cluster with matching TLS fingerprints, matching
|
|
// fingerprints, or for unclustered views the same endpoint) or a shared
|
|
// endpoint address. A cluster added through two connections is such a pair.
|
|
// Callers that see a polled node before it is merged, such as the node's host
|
|
// agent lookup, use this so they agree with the state on which views are the
|
|
// same node.
|
|
func NodeObservationsSameMachine(a, b Node) bool {
|
|
if key := nodeLogicalKey(a); key != "" && key == nodeLogicalKey(b) {
|
|
return true
|
|
}
|
|
name := normalizeNodeIdentityPart(a.Name)
|
|
if name == "" || name != normalizeNodeIdentityPart(b.Name) || nodeProviderIdentityConflicts(a, b) {
|
|
return false
|
|
}
|
|
if nodeCrossViewMergeProven(a, b) {
|
|
return true
|
|
}
|
|
// An address alias folds on the conflict check alone (resolveNodeMergeKey).
|
|
otherAliases := nodeEndpointMergeAliases(b)
|
|
for _, alias := range nodeEndpointMergeAliases(a) {
|
|
if !strings.HasPrefix(alias, "endpoint-ip:") {
|
|
continue
|
|
}
|
|
for _, other := range otherAliases {
|
|
if alias == other {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HostAgentBridgesNodeViews reports whether a host agent linked to view a proves
|
|
// view b is the same machine, the way UpdateNodesForInstance lets that agent
|
|
// fold b into a's slot: neither view names a cluster, their TLS fingerprints do
|
|
// not contradict each other (the state rejects an agent for a node whose known
|
|
// fingerprint differs from its linked node's), and the agent's own report names
|
|
// both endpoints, by exact address or full hostname. A host with several
|
|
// addresses added through two standalone connections is such a pair.
|
|
func HostAgentBridgesNodeViews(host Host, a, b Node) bool {
|
|
if normalizeNodeIdentityPart(a.ClusterName) != "" || normalizeNodeIdentityPart(b.ClusterName) != "" {
|
|
return false
|
|
}
|
|
fpA := normalizeNodeTLSFingerprint(a.TLSFingerprint)
|
|
fpB := normalizeNodeTLSFingerprint(b.TLSFingerprint)
|
|
if fpA != "" && fpB != "" && fpA != fpB {
|
|
return false
|
|
}
|
|
return hostReportsNodeEndpoint(host, a) && hostReportsNodeEndpoint(host, b)
|
|
}
|
|
|
|
// hostReportsNodeEndpoint reports whether a host agent's own report names the
|
|
// node's endpoint: the endpoint address among the addresses it reports, or the
|
|
// endpoint's full (dotted) hostname as its hostname.
|
|
func hostReportsNodeEndpoint(host Host, node Node) bool {
|
|
endpoint := extractHostEndpoint(node.Host)
|
|
if endpoint == "" {
|
|
return false
|
|
}
|
|
if ip := normalizeIPAddress(endpoint); ip != "" {
|
|
if normalizeIPAddress(host.ReportIP) == ip {
|
|
return true
|
|
}
|
|
for _, iface := range host.NetworkInterfaces {
|
|
for _, address := range iface.Addresses {
|
|
if normalizeIPAddress(address) == ip {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return strings.Contains(endpoint, ".") && endpoint == strings.TrimSpace(strings.ToLower(host.Hostname))
|
|
}
|