feat: link host agents to PVE nodes by hostname to prevent duplication
When a host agent registers, it now searches for a PVE node with a matching hostname and links them together. Similarly, when PVE nodes are discovered, they check for existing host agents with matching hostnames. This prevents the confusion of seeing duplicate entries when users install agents on PVE cluster nodes that were already discovered via the cluster API. - Added LinkedHostAgentID field to Node struct - Added LinkedNodeID/LinkedVMID/LinkedContainerID fields to Host struct - Added findLinkedProxmoxEntity() to match by hostname (with domain stripping) - Updated UpdateNodesForInstance() to preserve and auto-set links
This commit is contained in:
parent
cc5586c5c0
commit
2bce08925f
2 changed files with 148 additions and 0 deletions
|
|
@ -87,6 +87,9 @@ type Node struct {
|
|||
ConnectionHealth string `json:"connectionHealth"`
|
||||
IsClusterMember bool `json:"isClusterMember"` // True if part of a cluster
|
||||
ClusterName string `json:"clusterName"` // Name of cluster (empty if standalone)
|
||||
|
||||
// Linking: When a host agent is running on this PVE node, link them together
|
||||
LinkedHostAgentID string `json:"linkedHostAgentId,omitempty"` // ID of the host agent running on this node
|
||||
}
|
||||
|
||||
// VM represents a virtual machine
|
||||
|
|
@ -184,6 +187,11 @@ type Host struct {
|
|||
TokenLastUsedAt *time.Time `json:"tokenLastUsedAt,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
IsLegacy bool `json:"isLegacy,omitempty"`
|
||||
|
||||
// Linking: When this host agent is running on a known PVE node/VM/container
|
||||
LinkedNodeID string `json:"linkedNodeId,omitempty"` // ID of the PVE node this agent is running on
|
||||
LinkedVMID string `json:"linkedVmId,omitempty"` // ID of the VM this agent is running inside
|
||||
LinkedContainerID string `json:"linkedContainerId,omitempty"` // ID of the container this agent is running inside
|
||||
}
|
||||
|
||||
// HostNetworkInterface describes a host network adapter summary.
|
||||
|
|
@ -1260,15 +1268,47 @@ func (s *State) UpdateNodesForInstance(instanceName string, nodes []Node) {
|
|||
defer s.mu.Unlock()
|
||||
|
||||
// Create a map of existing nodes, excluding those from this instance
|
||||
// Also preserve LinkedHostAgentID for nodes that are being updated
|
||||
existingNodeLinks := make(map[string]string) // nodeID -> linkedHostAgentID
|
||||
nodeMap := make(map[string]Node)
|
||||
for _, node := range s.Nodes {
|
||||
if node.Instance != instanceName {
|
||||
nodeMap[node.ID] = node
|
||||
} else if node.LinkedHostAgentID != "" {
|
||||
// Preserve the link for nodes from this instance
|
||||
existingNodeLinks[node.ID] = node.LinkedHostAgentID
|
||||
}
|
||||
}
|
||||
|
||||
// Build hostname-to-hostAgentID map for linking new nodes to existing host agents
|
||||
hostAgentByHostname := make(map[string]string) // lowercase hostname -> hostAgentID
|
||||
for _, host := range s.Hosts {
|
||||
if host.ID != "" {
|
||||
hostAgentByHostname[strings.ToLower(host.Hostname)] = host.ID
|
||||
// Also index by short hostname
|
||||
if idx := strings.Index(host.Hostname, "."); idx > 0 {
|
||||
hostAgentByHostname[strings.ToLower(host.Hostname[:idx])] = host.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update nodes from this instance
|
||||
for _, node := range nodes {
|
||||
// Preserve existing link if we had one
|
||||
if existingLink, ok := existingNodeLinks[node.ID]; ok {
|
||||
node.LinkedHostAgentID = existingLink
|
||||
}
|
||||
// If no existing link, try to match by hostname
|
||||
if node.LinkedHostAgentID == "" {
|
||||
nodeName := strings.ToLower(node.Name)
|
||||
if hostID, ok := hostAgentByHostname[nodeName]; ok {
|
||||
node.LinkedHostAgentID = hostID
|
||||
} else if idx := strings.Index(nodeName, "."); idx > 0 {
|
||||
if hostID, ok := hostAgentByHostname[nodeName[:idx]]; ok {
|
||||
node.LinkedHostAgentID = hostID
|
||||
}
|
||||
}
|
||||
}
|
||||
nodeMap[node.ID] = node
|
||||
}
|
||||
|
||||
|
|
@ -1930,6 +1970,22 @@ func (s *State) ClearAllHosts() int {
|
|||
return count
|
||||
}
|
||||
|
||||
// LinkNodeToHostAgent updates a PVE node to link to its host agent.
|
||||
// This is called when a host agent registers and matches a known PVE node by hostname.
|
||||
func (s *State) LinkNodeToHostAgent(nodeID, hostAgentID string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, node := range s.Nodes {
|
||||
if node.ID == nodeID {
|
||||
s.Nodes[i].LinkedHostAgentID = hostAgentID
|
||||
s.LastUpdate = time.Now()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UpsertCephCluster inserts or updates a Ceph cluster in the state.
|
||||
// Uses ID (typically the FSID) for matching.
|
||||
func (s *State) UpsertCephCluster(cluster CephCluster) {
|
||||
|
|
|
|||
|
|
@ -2147,9 +2147,42 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
host.TokenLastUsedAt = previous.TokenLastUsedAt
|
||||
}
|
||||
|
||||
// Link host agent to matching PVE node/VM/container by hostname
|
||||
// This prevents duplication when users install agents on PVE cluster nodes
|
||||
linkedNodeID, linkedVMID, linkedContainerID := m.findLinkedProxmoxEntity(hostname)
|
||||
if linkedNodeID != "" {
|
||||
host.LinkedNodeID = linkedNodeID
|
||||
log.Info().
|
||||
Str("hostId", identifier).
|
||||
Str("hostname", hostname).
|
||||
Str("linkedNodeId", linkedNodeID).
|
||||
Msg("Linked host agent to PVE node")
|
||||
}
|
||||
if linkedVMID != "" {
|
||||
host.LinkedVMID = linkedVMID
|
||||
log.Info().
|
||||
Str("hostId", identifier).
|
||||
Str("hostname", hostname).
|
||||
Str("linkedVmId", linkedVMID).
|
||||
Msg("Linked host agent to VM")
|
||||
}
|
||||
if linkedContainerID != "" {
|
||||
host.LinkedContainerID = linkedContainerID
|
||||
log.Info().
|
||||
Str("hostId", identifier).
|
||||
Str("hostname", hostname).
|
||||
Str("linkedContainerId", linkedContainerID).
|
||||
Msg("Linked host agent to container")
|
||||
}
|
||||
|
||||
m.state.UpsertHost(host)
|
||||
m.state.SetConnectionHealth(hostConnectionPrefix+host.ID, true)
|
||||
|
||||
// Update the linked PVE node to point back to this host agent
|
||||
if host.LinkedNodeID != "" {
|
||||
m.linkNodeToHostAgent(host.LinkedNodeID, host.ID)
|
||||
}
|
||||
|
||||
// If host reports Ceph data, also update the global CephClusters state
|
||||
if report.Ceph != nil {
|
||||
cephCluster := convertAgentCephToGlobalCluster(report.Ceph, hostname, identifier, timestamp)
|
||||
|
|
@ -2170,6 +2203,65 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
return host, nil
|
||||
}
|
||||
|
||||
// findLinkedProxmoxEntity searches for a PVE node, VM, or container with a matching hostname.
|
||||
// Returns the IDs of matched entities (empty string if no match).
|
||||
func (m *Monitor) findLinkedProxmoxEntity(hostname string) (nodeID, vmID, containerID string) {
|
||||
if hostname == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// Normalize hostname for comparison (lowercase, strip domain)
|
||||
normalizedHostname := strings.ToLower(hostname)
|
||||
shortHostname := normalizedHostname
|
||||
if idx := strings.Index(normalizedHostname, "."); idx > 0 {
|
||||
shortHostname = normalizedHostname[:idx]
|
||||
}
|
||||
|
||||
matchHostname := func(name string) bool {
|
||||
normalized := strings.ToLower(name)
|
||||
if normalized == normalizedHostname || normalized == shortHostname {
|
||||
return true
|
||||
}
|
||||
// Also check short version of the candidate
|
||||
if idx := strings.Index(normalized, "."); idx > 0 {
|
||||
if normalized[:idx] == shortHostname {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
state := m.GetState()
|
||||
|
||||
// Check PVE nodes first
|
||||
for _, node := range state.Nodes {
|
||||
if matchHostname(node.Name) {
|
||||
return node.ID, "", ""
|
||||
}
|
||||
}
|
||||
|
||||
// Check VMs
|
||||
for _, vm := range state.VMs {
|
||||
if matchHostname(vm.Name) {
|
||||
return "", vm.ID, ""
|
||||
}
|
||||
}
|
||||
|
||||
// Check containers
|
||||
for _, ct := range state.Containers {
|
||||
if matchHostname(ct.Name) {
|
||||
return "", "", ct.ID
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// linkNodeToHostAgent updates a PVE node to link to its host agent.
|
||||
func (m *Monitor) linkNodeToHostAgent(nodeID, hostAgentID string) {
|
||||
m.state.LinkNodeToHostAgent(nodeID, hostAgentID)
|
||||
}
|
||||
|
||||
const (
|
||||
removedDockerHostsTTL = 24 * time.Hour // Clean up removed hosts tracking after 24 hours
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue