fix(agent): use /etc/machine-id in LXC containers to avoid ID collisions
LXC containers share the host's /sys/class/dmi/id/product_uuid, which causes gopsutil to return identical HostIDs for all LXC containers on the same physical host. This results in agent ID collisions where multiple LXC containers appear as a single host in Pulse. The fix detects LXC containers and prefers /etc/machine-id (which is unique per container) over gopsutil's HostID. Related to #773
This commit is contained in:
parent
9e7240042f
commit
02c4131853
2 changed files with 125 additions and 1 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -118,7 +119,7 @@ func New(cfg Config) (*Agent, error) {
|
|||
|
||||
displayName := hostname
|
||||
|
||||
machineID := strings.TrimSpace(info.HostID)
|
||||
machineID := getReliableMachineID(info.HostID, logger)
|
||||
|
||||
agentID := strings.TrimSpace(cfg.AgentID)
|
||||
if agentID == "" {
|
||||
|
|
@ -643,3 +644,62 @@ func (a *Agent) runProxmoxSetup(ctx context.Context) {
|
|||
Msg("Proxmox token created but registration failed (node may need manual configuration)")
|
||||
}
|
||||
}
|
||||
|
||||
// isLXCContainer detects if we're running inside an LXC container.
|
||||
// LXC containers share the host's /sys/class/dmi/id/product_uuid, which causes
|
||||
// gopsutil to return identical HostIDs for all LXC containers on the same host.
|
||||
func isLXCContainer() bool {
|
||||
// Check systemd-detect-virt if available
|
||||
if data, err := os.ReadFile("/run/systemd/container"); err == nil {
|
||||
container := strings.TrimSpace(string(data))
|
||||
if strings.Contains(container, "lxc") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check /proc/1/environ for container=lxc
|
||||
if data, err := os.ReadFile("/proc/1/environ"); err == nil {
|
||||
if strings.Contains(string(data), "container=lxc") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check /proc/1/cgroup for lxc markers
|
||||
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
|
||||
text := string(data)
|
||||
if strings.Contains(text, "/lxc/") || strings.Contains(text, "lxc.payload") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getReliableMachineID returns a machine ID that's unique per container/host.
|
||||
// In LXC containers, gopsutil's HostID may use /sys/class/dmi/id/product_uuid
|
||||
// which is shared with the host, causing ID collisions. This function detects
|
||||
// LXC and prefers /etc/machine-id which is unique per container.
|
||||
func getReliableMachineID(gopsutilHostID string, logger zerolog.Logger) string {
|
||||
gopsutilID := strings.TrimSpace(gopsutilHostID)
|
||||
|
||||
// For LXC containers, prefer /etc/machine-id to avoid ID collisions
|
||||
if isLXCContainer() {
|
||||
if data, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
machineID := strings.TrimSpace(string(data))
|
||||
if len(machineID) >= 32 {
|
||||
// Format as UUID if it's a 32-char hex string (like machine-id typically is)
|
||||
if len(machineID) == 32 {
|
||||
machineID = fmt.Sprintf("%s-%s-%s-%s-%s",
|
||||
machineID[0:8], machineID[8:12], machineID[12:16],
|
||||
machineID[16:20], machineID[20:32])
|
||||
}
|
||||
logger.Debug().
|
||||
Str("machineID", machineID).
|
||||
Msg("LXC container detected, using /etc/machine-id for unique identification")
|
||||
return machineID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gopsutilID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package hostagent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func TestNormalisePlatform(t *testing.T) {
|
||||
|
|
@ -71,3 +74,64 @@ func TestNormalisePlatform(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReliableMachineID(t *testing.T) {
|
||||
logger := zerolog.Nop()
|
||||
|
||||
// Check if we're running in an LXC container
|
||||
inLXC := isLXCContainer()
|
||||
|
||||
t.Run("returns non-empty ID", func(t *testing.T) {
|
||||
result := getReliableMachineID("test-gopsutil-id", logger)
|
||||
if result == "" {
|
||||
t.Error("getReliableMachineID returned empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("trims whitespace", func(t *testing.T) {
|
||||
result := getReliableMachineID(" test-id ", logger)
|
||||
if result == " test-id " {
|
||||
t.Error("getReliableMachineID did not trim whitespace")
|
||||
}
|
||||
})
|
||||
|
||||
if inLXC {
|
||||
t.Run("LXC uses machine-id", func(t *testing.T) {
|
||||
// In LXC, we should get a machine-id regardless of gopsutil input
|
||||
result := getReliableMachineID("gopsutil-product-uuid", logger)
|
||||
if result == "gopsutil-product-uuid" {
|
||||
t.Error("In LXC, getReliableMachineID should use /etc/machine-id, not gopsutil ID")
|
||||
}
|
||||
// Verify it looks like a formatted UUID
|
||||
if len(result) < 32 {
|
||||
t.Errorf("Expected UUID-like result, got %q", result)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
t.Run("non-LXC uses gopsutil ID", func(t *testing.T) {
|
||||
result := getReliableMachineID("12345678-1234-1234-1234-123456789abc", logger)
|
||||
if result != "12345678-1234-1234-1234-123456789abc" {
|
||||
t.Errorf("Expected gopsutil ID, got %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLXCContainer(t *testing.T) {
|
||||
// This test documents the detection behavior.
|
||||
// On non-LXC systems, isLXCContainer should return false.
|
||||
// We can't easily test the true case without mocking filesystem.
|
||||
result := isLXCContainer()
|
||||
|
||||
// Check if we're actually in an LXC container
|
||||
isActuallyLXC := false
|
||||
if data, err := os.ReadFile("/run/systemd/container"); err == nil {
|
||||
if string(data) == "lxc" || string(data) == "lxc\n" {
|
||||
isActuallyLXC = true
|
||||
}
|
||||
}
|
||||
|
||||
if result != isActuallyLXC {
|
||||
t.Logf("isLXCContainer() = %v (expected %v based on environment)", result, isActuallyLXC)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue