Skip to content

set_primary_ip = always never sets primary_ip4 for guests that report no routing table #515

Description

@Adam-Conolly

Version: 1.8.1 (2026-03-18), bbricardo/netbox-sync:1.8.1
Source: VMware / vCenter 6.7
NetBox: 4.6.0

Summary

For VMs whose guest agent reports interface IPs but no routing table,
primary_ip4 is never set, even with set_primary_ip = always. The IP address
is created and correctly assigned to the VM's interface; only the VM's
primary_ip4 relationship is missing. Nothing is logged - no error, no warning,
no unresolved-dependency message.

The equivalent IPv6 path has a single-address fallback. IPv4 does not.

Reproduction

Any VM whose guest tools publish IPs but not routes. In our case, Talos Linux
using siderolabs/vmtoolsd-guest-agent
v1.4.0, an open reimplementation of VMware Tools.

vCenter reports the VM as fully healthy:

toolsStatus         : toolsOk
toolsRunningStatus  : guestToolsRunning
toolsVersionStatus  : guestToolsUnmanaged
hostName            : unifydemo-stg-talos-cp1
guest.ipAddress     : 10.90.3.130
guest.net[0]        : mac=00:50:56:ae:cf:5d  ips=['10.90.3.130', 'fe80::250:56ff:feae:cf5d']

guest.ipStack entries                   : 1
guest.ipStack[0].ipRouteConfig.ipRoute  : 0        <-- no routes

Relevant config:

[source/vcenter-west-staging]
type = vmware
set_primary_ip = always
permitted_subnets = 10.90.0.0/16

After a sync, NetBox holds:

vm id       : 86
primary_ip4 : NONE
assigned IP : 10.90.3.130/24 -> interface 'vNIC 1 (st-infapp VLAN 3003)'

The IP object exists, is tagged NetBox-synced, and is assigned to the correct
interface. Only primary_ip4 is empty. This persists across many sync runs
(8 observed).

Cause

In module/sources/vmware/connection.py, vm_primary_ip4 is assigned in
exactly one place - inside a condition that requires a default gateway derived
from the guest routing table:

# ~2293 - the only source of vm_default_gateway_ip4
for route in grab(obj, "guest.ipStack.0.ipRouteConfig.ipRoute", fallback=list()):
    if grab(route, "prefixLength") == 0:
        ...

# ~2479 - the only assignment of vm_primary_ip4
if vm_default_gateway_ip4 is not None and \
        vm_default_gateway_ip4 in ip_interface(int_ip_address).network and \
        vm_primary_ip4 is None:
    vm_primary_ip4 = int_ip_address

With no routes, vm_default_gateway_ip4 stays None, so vm_primary_ip4 stays
None, so p_ipv4=None reaches add_device_vm_to_inventory. There:

# ~1165
primary_ipv4_object = None
if p_ipv4 is not None:
    primary_ipv4_object = ip_interface(p_ipv4)

# ~1198 - every IP is skipped
if ip_interface_object not in [primary_ipv4_object, primary_ipv6_object]:
    continue

so the set_primary_ip == "always" block below is never reached.

The asymmetry

The same function already contains a single-address fallback for IPv6, applied
unconditionally:

# if VM has only one IPv6 on all interfaces, use it as primary IPv6 address
if vm_primary_ip6 is None or True:
    ...
    if len(potential_primary_ipv6_list) == 1:
        log.debug(f"Found one IPv6 ... using it as primary IPv6.")
        vm_primary_ip6 = potential_primary_ipv6_list[0]

grep finds no IPv4 equivalent. This looks like an oversight rather than a
deliberate difference.

Suggested fix

Mirror the IPv6 fallback for IPv4: if no default route is available and exactly
one permitted IPv4 exists across the VM's interfaces, use it as primary_ip4.

Important edge case

The fallback must not guess when several IPv4 addresses are present. Kubernetes
control planes commonly carry a floating VIP alongside the node address:

10.90.3.132/24    node address
10.90.3.138/32    control-plane VIP, moves between nodes

Picking arbitrarily would assign a shared VIP as one node's primary IP and
thrash it on every failover. Reasonable options, in order of preference:

  1. only apply the fallback when exactly one permitted IPv4 exists
  2. or, prefer the non-/32 address when several exist
  3. or, expose the behaviour as a config option, so operators opt in

Option 1 alone fixes the common case and is what the IPv6 path already does.

Things ruled out

Documented in case they narrow the search:

  • use_caching = false (a real write run) changed nothing
  • cluster_include_filter, permitted_subnets, and the NetBox prefix all match
  • vCenter permissions are fine; the sync writes other VMs in the same run
  • the NetBox VM object is stable (created once, updated repeatedly)

Happy to test a patch against our environment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions