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:
- only apply the fallback when exactly one permitted IPv4 exists
- or, prefer the non-
/32 address when several exist
- 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.
Version: 1.8.1 (2026-03-18),
bbricardo/netbox-sync:1.8.1Source: VMware / vCenter 6.7
NetBox: 4.6.0
Summary
For VMs whose guest agent reports interface IPs but no routing table,
primary_ip4is never set, even withset_primary_ip = always. The IP addressis created and correctly assigned to the VM's interface; only the VM's
primary_ip4relationship 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-agentv1.4.0, an open reimplementation of VMware Tools.
vCenter reports the VM as fully healthy:
Relevant config:
After a sync, NetBox holds:
The IP object exists, is tagged
NetBox-synced, and is assigned to the correctinterface. Only
primary_ip4is empty. This persists across many sync runs(8 observed).
Cause
In
module/sources/vmware/connection.py,vm_primary_ip4is assigned inexactly one place - inside a condition that requires a default gateway derived
from the guest routing table:
With no routes,
vm_default_gateway_ip4staysNone, sovm_primary_ip4staysNone, sop_ipv4=Nonereachesadd_device_vm_to_inventory. There: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:
grepfinds no IPv4 equivalent. This looks like an oversight rather than adeliberate 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:
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:
/32address when several existOption 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 nothingcluster_include_filter,permitted_subnets, and the NetBox prefix all matchHappy to test a patch against our environment.