Retry incomplete guest network metadata sooner (#1319)

This commit is contained in:
rcourtman 2026-03-27 11:41:56 +00:00
parent b05d2b0489
commit bce800e95d
2 changed files with 116 additions and 4 deletions

View file

@ -56,14 +56,19 @@ func guestMetadataCacheHasUsefulData(entry guestMetadataCacheEntry) bool {
entry.agentVersion != ""
}
func guestMetadataCacheHasCompleteNetworkData(entry guestMetadataCacheEntry) bool {
return len(entry.networkInterfaces) > 0
}
func guestMetadataCacheHasNetworkData(entry guestMetadataCacheEntry) bool {
return len(entry.ipAddresses) > 0 || len(entry.networkInterfaces) > 0
}
func guestMetadataCacheEntryTTL(entry guestMetadataCacheEntry) time.Duration {
// Treat identity-only metadata as incomplete so VMs that answered
// guest-info/version but not network-interfaces are retried soon.
if guestMetadataCacheHasNetworkData(entry) {
// Treat identity-only and IP-only metadata as incomplete so VMs that answered
// guest-info/version or partial network calls but not full interface inventory
// are retried soon instead of freezing incomplete VM Summary data for minutes.
if guestMetadataCacheHasCompleteNetworkData(entry) {
return guestMetadataCacheTTL
}
return guestMetadataEmptyTTL
@ -115,7 +120,7 @@ func (m *Monitor) scheduleGuestMetadataFetchForEntry(key string, now time.Time,
if m == nil {
return
}
if !guestMetadataCacheHasNetworkData(entry) {
if !guestMetadataCacheHasCompleteNetworkData(entry) {
m.guestMetadataLimiterMu.Lock()
m.guestMetadataLimiter[key] = now.Add(guestMetadataEmptyTTL)
m.guestMetadataLimiterMu.Unlock()
@ -591,6 +596,12 @@ func processGuestNetworkInterfaces(raw []proxmox.VMNetworkInterface) ([]string,
rxBytes := parseInterfaceStat(iface.Statistics, "rx-bytes")
txBytes := parseInterfaceStat(iface.Statistics, "tx-bytes")
if ifaceName == "" && mac == "" && len(addresses) > 0 && rxBytes == 0 && txBytes == 0 {
// Preserve discovered guest IPs, but do not treat a nameless/anonymous
// interface record as complete interface inventory.
continue
}
if len(addresses) == 0 && rxBytes == 0 && txBytes == 0 {
if len(iface.IPAddresses) > 0 {
continue

View file

@ -90,6 +90,41 @@ func (*identityThenNetworkGuestMetadataClient) GetVMAgentVersion(ctx context.Con
return "8.2.2", nil
}
type ipOnlyThenNetworkGuestMetadataClient struct {
stubPVEClient
networkCalls int
}
func (c *ipOnlyThenNetworkGuestMetadataClient) GetVMNetworkInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.VMNetworkInterface, error) {
c.networkCalls++
if c.networkCalls == 1 {
return []proxmox.VMNetworkInterface{
{
IPAddresses: []proxmox.VMIpAddress{
{Address: "192.168.1.60", Prefix: 24},
},
},
}, nil
}
return []proxmox.VMNetworkInterface{
{
Name: "Ethernet0",
HardwareAddr: "00:11:22:33:44:66",
IPAddresses: []proxmox.VMIpAddress{
{Address: "192.168.1.60", Prefix: 24},
},
},
}, nil
}
func (*ipOnlyThenNetworkGuestMetadataClient) GetVMAgentInfo(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
return map[string]interface{}{}, nil
}
func (*ipOnlyThenNetworkGuestMetadataClient) GetVMAgentVersion(ctx context.Context, node string, vmid int) (string, error) {
return "", nil
}
func TestGuestMetadataCacheKey(t *testing.T) {
t.Parallel()
@ -969,9 +1004,19 @@ func TestGuestMetadataCacheEntryTTL(t *testing.T) {
name: "network metadata uses full ttl",
entry: guestMetadataCacheEntry{
ipAddresses: []string{"192.168.1.10"},
networkInterfaces: []models.GuestNetworkInterface{
{Name: "eth0", Addresses: []string{"192.168.1.10"}},
},
},
want: guestMetadataCacheTTL,
},
{
name: "ip-only metadata retries quickly",
entry: guestMetadataCacheEntry{
ipAddresses: []string{"192.168.1.10"},
},
want: guestMetadataEmptyTTL,
},
{
name: "identity-only metadata retries quickly",
entry: guestMetadataCacheEntry{
@ -1047,6 +1092,62 @@ func TestFetchGuestAgentMetadataRetriesIdentityOnlyCacheSooner(t *testing.T) {
}
}
func TestFetchGuestAgentMetadataRetriesIPOnlyCacheSooner(t *testing.T) {
t.Parallel()
client := &ipOnlyThenNetworkGuestMetadataClient{}
monitor := &Monitor{
guestMetadataCache: make(map[string]guestMetadataCacheEntry),
guestMetadataLimiter: make(map[string]time.Time),
}
status := &proxmox.VMStatus{Agent: proxmox.VMAgentField{Value: 1}}
firstIPs, firstIfaces, _, _, _ := monitor.fetchGuestAgentMetadata(
context.Background(),
client,
"pve",
"node1",
"vm100",
100,
status,
)
if len(firstIPs) != 1 || firstIPs[0] != "192.168.1.60" {
t.Fatalf("expected first fetch to preserve discovered IP, got %#v", firstIPs)
}
if len(firstIfaces) != 0 {
t.Fatalf("expected first fetch to remain interface-incomplete, got %#v", firstIfaces)
}
key := guestMetadataCacheKey("pve", "node1", 100)
entry := monitor.guestMetadataCache[key]
if got := guestMetadataCacheEntryTTL(entry); got != guestMetadataEmptyTTL {
t.Fatalf("guestMetadataCacheEntryTTL(ip-only) = %s, want %s", got, guestMetadataEmptyTTL)
}
entry.fetchedAt = time.Now().Add(-guestMetadataEmptyTTL - time.Second)
monitor.guestMetadataCache[key] = entry
monitor.guestMetadataLimiter[key] = time.Now().Add(-time.Second)
secondIPs, secondIfaces, _, _, _ := monitor.fetchGuestAgentMetadata(
context.Background(),
client,
"pve",
"node1",
"vm100",
100,
status,
)
if len(secondIPs) != 1 || secondIPs[0] != "192.168.1.60" {
t.Fatalf("expected second fetch to preserve IP, got %#v", secondIPs)
}
if len(secondIfaces) != 1 || secondIfaces[0].Name != "Ethernet0" {
t.Fatalf("expected second fetch to populate interfaces, got %#v", secondIfaces)
}
if client.networkCalls != 2 {
t.Fatalf("expected network metadata to be fetched twice, got %d calls", client.networkCalls)
}
}
func TestFetchGuestAgentMetadataRetriesEmptyCacheSooner(t *testing.T) {
t.Parallel()