python3 -m pip install -r requirements.txt- Discovered MACs
- Object Types: IPAM > IP Addresses
- Name: discovered_mac
- Label: Discovered MAC
- Description: "Matching MACs when the device is not in netbox"
- Type: Multiple objects
- Related object type: DCIM > MAC Address
Copy .env.example to .env and fill in your values.
cp .env.example .env- Since netbox 4.2 MACs are managed objects (compared with strings before). This now enforces uniqueness as well as other constraints, one of which is the MAC can only be assigned to one interface at once. This creates a bit of an issue for all of the virtual interface types which take their MAC from the physical interfaces they depend on (A lag or bridge's MAC is one of the slave devices, vlans take their parent device's MAC address).
- As such for now MAC addresses for all virtual interfaces will remain un-set
Two independent conventions the sync recognises for connecting devices to each other in NetBox - one wireless, one physical. Both only ever add data (fill in whatever's currently empty); neither overwrites a value someone already set by hand.
- A radio is linked as point-to-point (
WirelessLink) if it currently has exactly one linked peer, or as the AP side of a point-to-multipoint network (WirelessLAN) if it has more than one. Nothing to do if it has zero. - A peer is matched to a NetBox interface by looking up its MAC address - read-only, never creates a bare MAC record just because a peer was seen. If the MAC doesn't resolve to any interface, that's logged as a visible warning (not an error) rather than silently dropped, since it usually means a customer CPE isn't tracked in NetBox yet - a real inventory gap worth surfacing. The local side's own radio still gets its fields set regardless of whether the peer resolved.
- Always updated to match the device's live report (device is
authoritative, same as interfaces/IPs elsewhere in this tool): the
interface's
type(set toother-wirelessif it isn't already one of NetBox's wireless PHY types - required beforerf_role/rf_channel_*can be set at all),rf_role,rf_channel_frequency,rf_channel_width, and theWirelessLink'sstatus(connected, since reaching this code path at all means a live peer was just observed). - Only filled in if currently empty:
ssid,auth_type,auth_pskon theWirelessLink/WirelessLAN.auth_typeis derived from the device's raw security string with a simple substring match (PSK/WPA->wpa-personal,WEP->wep, empty/open/disabled->open, anything else left unset rather than guessed). - Some dual-radio hardware (e.g. Wave Pro/LR's 60 GHz "main" + 5 GHz "backup" radios) reports the identical MAC on both of a device's wireless interfaces, so a MAC-only match can resolve to the wrong radio on the peer's end. This is corrected by preferring whichever sibling wireless interface on the peer's device already has a synced frequency closest to the one being linked - there's nothing to compare against on that peer's very first sync, so a brand new pair of devices may get mismatched for one run, then self-correct on the next.
- An interface
descriptionthat starts with"<Device name> [<Port name>]"(e.g."FIB-IE1 [sfp-sfpplus1]") is read as a manual note that the far end of this cable is that device's named port - matched as a prefix, so any text after the closing bracket is ignored (some descriptions carry extra notes there already, e.g."DAN-SW0031 [sfp-sfpplus2] / Was Roylances UXG"). - If the named device or port doesn't actually exist in NetBox, the description is silently ignored - never an error. Same if either end already has a cable connected (never replaces an existing one).
- Unlike everything else in this tool, this isn't driven by any live
device - it only reads descriptions already entered by hand into
NetBox - so it runs once per
sync.pyinvocation against every interface in NetBox, regardless of platform, rather than per-device.
Two more conventions, both opt-in via their own .env flag (off by
default) since they infer a cable's existence purely from IP layout
rather than reading any explicit "this connects to that" data. Both
skip an interface that already has a cable, same as everything else
above, and both only ever consider a physical port - a real,
single, cable-terminable interface (not virtual/bridge/lag, and
not a wireless radio). NetBox's type alone can't fully tell that apart
from a software construct - a tunnel interface (e.g. IPIP) has no
dedicated type of its own and just shows up as the generic other,
indistinguishable by type from a genuinely unclassified physical port -
so a real MAC address (something the device itself reported, which a
tunnel never has) is required too. If the IP is on a bridge or a
VLAN/virtual sub-interface instead of directly on a physical port, that
resolves one level up (the bridge's sole physical member, or the VLAN
interface's own parent) - but only when that resolves to exactly one
candidate. A router's bridge aggregating several physical uplink ports
at once (a switch fabric, confirmed live: one device's bridge had three
plausible physical members) is left alone rather than guessed, since
there's no LLDP or other data here to say which one a given cable is
actually in.
ASSUME_SUBNET_LINKS_30_31- a /30 or /31 has exactly 2 usable host addresses, the standard point-to-point routed-link convention. Two physical ports (on two different devices) whose IPs land in the same one of these small subnets are assumed to be the two ends of one cable. This is blind subnet matching with no other corroborating evidence at all, so it's opt-in.ASSUME_SUBNET_LINKS_29- for backhaul point-to-point wireless pairs specifically (i.e. only devices already linked to each other via an existingWirelessLink, from the wireless sync above): this fleet's convention is to lay a backhaul link's own /29 out with both radios' bridge IPs and both sides' router IPs in the one block, with each side's router IP and radio IP numerically closer to each other than to the far side's. Sorting both the two already-known radio IPs and the two remaining ("router") IPs in the block and pairing them up in the same order recovers "same side" without hardcoding which octets belong to which end, then connects each backhaul's own physical ethernet uplink port to the router port on its own side. Much better-evidenced than the /30-/31 rule above (it only ever acts on a pair already confirmed live over the air), but still an assumption about which physical port that implies, so it has its own separate gate rather than sharingASSUME_SUBNET_LINKS_30_31.
A third opt-in cabling rule, currently only implemented for RouterOS
(drivers/routeros.py's get_ospf_neighbours(), reading
routing/ospf/neighbor) - real routing-protocol evidence of a direct
link, rather than an inference from IP layout like the two rules above.
- Only ever acts on a local interface with exactly one distinct
neighbour router ID currently in the
Fulladjacency state. A shared/ broadcast segment (e.g. a "trunk" bridge aggregating several physical uplinks) can carryFulladjacencies to more than one neighbour at once on the same local interface - a singleCablecan't represent that, so it's left alone entirely rather than guessed at. - This fleet's convention is for each router's OSPF router ID to be its own loopback address. The neighbour's reported link address (its IP on this specific link, used to resolve which NetBox interface/device it is) and its router ID are cross-checked against each other - confirmed live that these can actually disagree (an IP address recorded in NetBox against the wrong device entirely). A mismatch is logged as a warning (a real data-quality issue worth surfacing) and skipped rather than trusting either value blindly.
- A router ID or address that falls in a known shared/non-unique
convenience network (see
utils.networks_to_ignore- e.g.172.16.255.255/32, confirmed live to sit identically on aloopback1interface across multiple independent routers) is skipped silently rather than reaching the cross-check above, since it can never uniquely identify a single neighbour device. - OSPF runs happily over a VLAN/bridge interface, but NetBox
Cables can only terminate on a real physical port - both ends are resolved up to their physical port the same way as the/29rule above (giving up rather than guessing on an ambiguous multi-member bridge).
Unlike everything above, which only ever links or fills in fields on Devices that already exist, this one creates brand-new Device/Interface records - so it's off by default even more deliberately than the two subnet-inference rules.
For the AP side of a point-to-multipoint WirelessLAN only (never a
point-to-point WirelessLink - that peer is expected to already be a
real, separately-managed device): a peer whose MAC doesn't resolve to
any existing NetBox interface is, on a platform with an entry in
sync.py's _CLIENT_DEVICE_CONVENTIONS (currently Tachyon, and all
three Ubiquiti-family platforms - AirOS v8/AirFiber/UISP),
auto-provisioned a placeholder client Device instead of just being
logged as an unresolved peer:
- Named
"<prefix><peer's reported name>"-"TACH-<name>"for Tachyon (the name being e.g.wireless.peers[].system_nameon a Tachyon radio),"UBNT-<name>"for Ubiquiti - either way it's exactly what the peer itself reports, not independently verified. Deliberately its own prefix, distinct from this fleet's pre-existing manualCUST-<name>customer-CPE convention (e.g.CUST-Botany Honey Company) - a real, manually-managedCUST-device was briefly modified by mistake by a cleanup script that scoped by name prefix instead of byplatform is None+role is CPE - Dish, which is what actually, reliably identifies an auto-created placeholder. A distinct prefix means that's no longer the only way to tell. - Device type is fuzzy-matched from the peer's own reported hardware
model against existing NetBox device types for that platform's
manufacturer, in either direction - a peer can report a more
specific model than any registered type (Tachyon:
"TNA-303L-65"matches the registered"TNA-303", since no more specific"TNA-303L"type exists), or a less specific one (Ubiquiti: peers only ever report a base model like"Rocket Prism 5AC", itself a prefix of the registered"Rocket Prism 5AC Gen2"). Only trusted when it resolves to exactly one candidate - confirmed live that the less-specific direction is often genuinely ambiguous (a peer reporting"PowerBeam 5AC"or"NanoBeam 5AC"matches several different registered variants -5AC 300/5AC 500/5AC Gen2, or5AC 19/5AC Gen2- at once). No match, or more than one, and the device isn't created - a warning is logged instead, same as an unresolved peer normally gets. - Role
CPE - Dish, site matching the AP's own site, statusactive. platformis deliberately left unset (matching every existing manually-created customer placeholder in this fleet already) - so this tool's own per-device sync loop never tries to poll it with credentials that aren't ours to use.- Gets one
wlan0interface holding the peer's MAC and IP addresses (recorded as host routes -/32//128- since a peer only ever reports its own bare address, never the subnet it's part of; the same link-local/loopback ranges ignored everywhere else in this tool are skipped here too) - then treated exactly like a normally-resolved peer from there on (associated with theWirelessLAN, etc).