netboxtool

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

README

NetBox client (internal/netboxtool)

In-tree NetBox GraphQL/REST client used by internal/netbox, device-sync, drivers, and the web GUI. This used to be the standalone github.com/abundo/netboxtool module; change the client here rather than depending on a tagged library.

The standalone netboxtool CLI is not shipped. Diagnostic fetch-strategy notes below still apply to GetDevices/GetVMs.

Layout

  • netboxtool.goNetboxClient, HTTP plumbing (GraphQL POST + REST PATCH/POST/DELETE), pagination loop, and the Get*/Update*/*Create/ *Delete API methods.
  • netboxtool_graphql.go — the raw GraphQL query bodies, both the nested ones (deviceListGraphQLbody, virtualMachineListGraphQLbody, deviceTypeGraphQLbody, manufacturerListGraphQLbody, ...) and the flat ones GetDevices/GetVMs use instead (flatDeviceListGraphQLbody, interfaceListGraphQLbody, flatVirtualMachineListGraphQLbody, vmInterfaceListGraphQLbody, ipAddressListGraphQLbody) — see "GraphQL query architecture" below for why there are two shapes.
  • netboxtool_flat.go — the flat-query fetch functions and the stitchDevices/stitchVMs functions that reassemble flat devices/interfaces/addresses back into the nested []JSONDevice shape parseDevices expects, plus getDevicesFlat/getVMsFlat (what GetDevices/GetVMs actually call).
  • models.goNBDevice/NBInterface/NBAddress/NBTag, the shapes returned to callers (also carry gorm tags for consumers that persist them).
  • cache.go — optional in-memory wrapper around device/device-type lookups.

GraphQL query architecture

Netbox exposes both a GraphQL endpoint (read-only) and a REST API (read/write). Reads (Get*) go through GraphQL; writes (Update*, *Create, *Delete) go through REST, since Netbox's GraphQL API doesn't support mutations. Interface type choices (not a NetBox object) are read with REST OPTIONS /api/dcim/interfaces/ (GetInterfaceTypeChoices).

There are two different query shapes in use, for two different call patterns:

  • GetDevice(name, id)/GetVM(name, id) (via GetDevices_/GetVM_) fetch one device/VM by name or id, filtered server-side (filters: { name/id: { exact: ... } } on device_list/ virtual_machine_list, see NetboxAPICall). These still use the nested query bodies (deviceListGraphQLbody/virtualMachineListGraphQLbody), which resolve device_type/role/site/platform/tags and interfaces (+ each interface's tags/ip_addresses) all nested inline in one query. Nesting is fine, even preferable, for one row — the cost problem below only shows up at table scale.
  • GetDevices()/GetVMs() (the full-table fetch, no name/id filter) instead use getDevicesFlat/getVMsFlat (netboxtool_flat.go): three separate flat top-level queries — device_list/virtual_machine_list (no nested interfaces), interface_list/vm_interface_list (no nested ip_addresses, plus a device/virtual_machine back-reference the nested version got for free from nesting), and a single shared ip_address_list (resolving its owning interface via assigned_object, a polymorphic union - see "Flattening GetDevices/GetVMs" below) — run concurrently, then stitched back into the exact same []JSONDevice{Interfaces: []JSONInterface{IPAddresses: [...]}} shape the nested query used to produce directly. parseDevices (the actual device/interface/address field-mapping logic — status, lat/long inheritance, custom fields, ...) is unchanged either way; only how the raw data is fetched differs.
A previous, narrower flattening attempt — and why this one is different

An earlier version of this package flattened device_type_list/ site_list/role_list/platform_list (small to-one lookup tables) into separate queries joined via Go-side lookup maps, because a benchmark suggested it was faster even for those to-one relations. That was reverted back to nested queries at the user's request once a server-side uwsgi-worker-count fix (see "Performance notes" below) made the nested version fast enough on its own — the added stitching complexity wasn't worth it for cheap single-row FK resolutions.

That finding is still valid and still applies to those four lookup tables — don't re-flatten device_type/role/site/platform based on this section. The flattening described above (GetDevices/GetVMs) is a different, independently-benchmarked case: it targets the nested interfaces{ip_addresses{...}} one-to-many collection, not a to-one lookup, and — per the measurements below — has a severe, worse-than-linear cost as interface count grows, an effect the earlier small-table experiment would never have surfaced. Don't use this section to justify flattening anything else without measuring it the same way first (see cmd/benchmark.go).

Pagination

Netbox's GraphQL API caps a single query's result at graphqlPageSize (1000 rows) regardless of the requested limit. fetchAllPages/ fetchAllPagesRaw (netboxtool.go) loop until a page comes back short.

Pagination is cursor-based, not offset-based: pagination: { start: <id>, limit: N }, where start is the Netbox object id to resume from (id >= start), not a row offset. Netbox returns rows ordered by id ascending, so each loop iteration sets start to lastID + 1. This matters because offset-based pagination (pagination: { offset: <n>, limit: N }) gets more expensive per page as offset grows — Postgres has to scan and discard offset rows — while cursor pagination is a constant-cost index seek regardless of position.

Netbox's standard REST DRF pagination (?limit=/next, used by GetCables and by the now-abandoned flat-REST variant benchmarked below) is a different mechanism entirely, not covered by the cursor-vs-offset comparison above — see "Flattening GetDevices/GetVMs" for what was found benchmarking REST specifically for interfaces/addresses. GetCables itself wasn't part of that benchmark and hasn't been measured the same way.

Performance notes / things already tried

Netbox's REST/GraphQL API was originally much slower for a full fetch than an equivalent script run in Netbox's Django nbshell (in-process ORM, no HTTP). Root causes found and fixed:

  1. HTTP connection reuseNetboxClient holds one shared http.Client (built once in NewNetboxClient), instead of a fresh http.Transport/http.Client per call. Any new HTTP call added to the client should reuse nb.httpClient, not construct its own.
  2. Server-side uwsgi worker count — a single-worker (processes = 1) uwsgi config on the Netbox host bottlenecked every client regardless of how the Go side was optimized. If fetch performance regresses, check the Netbox host's uwsgi processes/threads config before assuming it's a client-side problem.
  3. Cursor vs offset pagination — see above.

Tried and reverted, do not reintroduce without asking:

  • Flat fetch-and-stitch for the small to-one lookup tables (device_type_list/site_list/role_list/platform_list) — no measured benefit once (2) above was fixed. This is a different scope than "Flattening GetDevices/GetVMs" below — don't cite that section as justification for re-flattening these small tables too.
  • Goroutine/sync.WaitGroup-based concurrent fetching of those same small lookup tables — likewise no measured benefit once (2) was fixed. (Also a different scope than the concurrent fetching getDevicesFlat/ getVMsFlat do now — see below.)
Flattening GetDevices/GetVMs (2026-08)

GetDevices()/GetVMs() (full-table, no filter) were measured as the actual bottleneck in a real full-table sync — not because of N+1 REST calls (there weren't any: the old nested query fetched everything in one page-cursored GraphQL query per page), but because Netbox's GraphQL resolver does real per-row work server-side that a flatter query shape avoids. At the time this was written that looked like an inherent cost of resolving a nested one-to-many field; several concrete, since-fixed Netbox bugs turned up afterward that better explain it — see "Upstream Netbox bugs" below before assuming nested-vs-flat is the whole story. Investigation and benchmark tooling lived in the old standalone netboxtool CLI (cmd/benchmark.go: seven fetch-strategy variants, labeled A–G, with per-phase timing and page counts; cmd/introspect.go's introspect-ip-address subcommand: read-only GraphQL schema introspection, used once to find assigned_object's real union member types instead of guessing them — see below).

Measurements, two real Netbox instances:

  • Small: 2 CPUs, 6 uwsgi workers, 279 devices / 7,965 interfaces / 1,302 addresses. Old nested query: 10.15s (fetch only). Flat queries, interfaces+addresses fetched concurrently: 2.3–2.8s.
  • Large: Netbox 4.5.9 in Docker, 2 CPUs, 6 granian workers, 1,375 devices / 37,021 interfaces / 3,794 addresses. Old nested query: 2m47.7s (fetch only). Flat queries, concurrent: 20.2s~8x faster. A real end-to-end sync (fetch + local DB writes + cable/site/ tenant sync) against the small instance afterwards: 33.7s wall time, correct output (0 new, 325 updated — 279 devices + ~46 VMs, matching what a re-sync of already-known devices should show).

What didn't work, and why it's not just "REST vs GraphQL":

  • A fully flat REST variant (/api/dcim/devices/, /api/dcim/ interfaces/, /api/ipam/ip-addresses/, paginated) was worse than the original nested GraphQL query — 34–37s on the small instance, over 3 minutes on the large one. The REST interfaces endpoint specifically did the same number of page round trips as the equivalent flat GraphQL query (verified via page counts) but took roughly 10x longer per page, meaning it's not a pagination/round-trip problem — something in Netbox's REST interface serializer itself is expensive per row (a likely Django-side N+1, e.g. a missing prefetch_related), independent of payload size or request count.
  • On the large instance, the REST interfaces call appeared to leave the server measurably degraded for the next request too: a GraphQL interface_list fetch that normally took ~9s took over a minute immediately after the REST interfaces call, then returned to ~9s once more time had passed. Avoid REST bulk-list endpoints in any latency-sensitive path on a shared/production Netbox instance — the cost isn't necessarily contained to the one slow request.
  • ip_address_list's assigned_object field is a polymorphic union (IPAddressAssignmentType on Netbox 4.5.9, with InterfaceType/ VMInterfaceType/FHRPGroupType as concrete members) — resolved via inline fragments (... on InterfaceType { id }). The member type names were found via introspect-ip-address, not guessed — a first guess at the VM interfaces query field name (vminterface_list instead of the real vm_interface_list) shipped to production once and crashed on first real use; verify against the actual schema before trusting a Netbox GraphQL field/type name pulled from memory or convention.

Known accepted tradeoff: getVMsFlat re-fetches ip_address_list independently of getDevicesFlat, so a full sync (which calls GetDevices() then GetVMs()) pays for that fetch twice. Given VMs are typically a small fraction of these inventories, this was the simpler first cut over adding cross-call caching to NetboxClient — which cache.go's own doc comment says deliberately never caches anything itself. Revisit only if this shows up as real cost in production timing.

Upstream Netbox bugs behind much of this (fixed in v4.6.7/v4.6.8)

Both instances above were benchmarked against Netbox 4.5.9. After benchmarking, four independent, previously-unknown-to-us N+1/caching bugs in Netbox's GraphQL layer turned up, all found and fixed within the same short window, all released by v4.6.8 — meaning the "why was the nested query so slow" answer is broader than "resolving a nested one-to-many field is inherently expensive" (this doc's working theory at the time):

  • #22787IPAddressType.assigned_object (the exact field ipAddressListGraphQLbody selects) issued ~8 SQL queries per row instead of a handful total. Fixed in v4.6.8. The underlying cause (a GenericForeignKey field with no only=[...]/GenericPrefetch optimizer hint) is called out by a maintainer as likely affecting every GFK-backed GraphQL field in Netbox, not just this one - watch for similar fixes to L2VPNTerminationType.assigned_object, MACAddressType.assigned_object, etc. in later releases.
  • #22813 — requesting custom_fields on any GraphQL list endpoint cost one extra single-row query per object returned (a deferred-column reload, unrelated to GFKs). This hits nearly every query in this package — device_list/interface_list/virtual_machine_list/vm_interface_list/ tenant_list all request custom_fields. On the large instance's 37,021-row interface_list fetch alone, that's ~37,000 extra queries under the old behavior. Fixed in v4.6.7.
  • #22837 — a to-one relation (site/role/platform/device_type - exactly what deviceListGraphQLbody/flatDeviceListGraphQLbody nest) doesn't cost one row for the related object; it costs one row for every other object that shares that same related object, independent of page size or how many rows you actually requested. 1,000 devices at one site costs 1,000 rows fetched just for resolving site. This directly affects both device_list query variants in this package. Fixed in v4.6.8.
  • #22877 — smaller in scope: CustomFieldManager.get_for_model()'s cache check treated a cached "this model has no custom fields" result as falsy, bypassing the cache and re-querying every time. Compounds with #22813. Fixed in v4.6.8.

Confirmed: the small instance was upgraded 4.5.9 → 4.6.7 → 4.6.8 and re-benchmarked at each step (same 279 devices / 7,965 interfaces / 1,302 addresses throughout):

A (nested) G (flat, concurrent) C addresses phase (REST) F/G addresses phase (ip_address_list)
4.5.9 ~10.15s ~2.3–2.8s ~1.9s* ~2.7s*
4.6.7 (has #22813 only) 8.517s 3.04s 2.136s 2.672s
4.6.8 (has all four) 7.559s 1.702s 2.295s 367ms

*4.5.9 figures are single representative runs from the original A–G benchmark, not a rerun of every variant at each version like 4.6.7/4.6.8 were.

The ip_address_list fetch — the exact query #22787 (assigned_object) targeted — dropped 2.672s → 367ms (~7.3x) going 4.6.7→4.6.8, precisely the version that issue shipped in: about as clean a confirmation as a real production instance can give that this was the actual mechanism, not a coincidence. The nested query (A) also improved with each upgrade (10.15s→8.517s→7.559s, ~25% total) — consistent with these bugs affecting both query shapes, as expected. REST (C's addresses/interfaces phases) did not improve at all across any version (interfaces phase: 35s→ 37.054s→37.556s) — confirming that slowness is a separate, still-open issue, unrelated to this bug cluster, and unaffected by upgrading.

Combined effect of this session's client-side rework and the Netbox upgrade, old-nested-on-4.5.9 vs. flat-concurrent-on-4.6.8: 10.15s → 1.702s, ~6x. Recommendation: upgrade any Netbox instance this package talks to, to ≥v4.6.8 — confirmed, not just theorized, to help both GetDevice/GetVM (still on the nested query) and GetDevices/GetVMs (the flat path), independent of and in addition to any further client-side work.

The large instance confirms this at real scale: upgraded 4.5.10 → 4.6.8 (1,375 devices / 37,024 interfaces / 3,794 addresses throughout):

A (nested) G (flat, concurrent) C interfaces phase (REST)
4.5.x (pre-upgrade, from the original A–G run) 2m47.7s 20.2s 2m39.9s
4.6.8 45.38s 6.261s 2m14.1s

The Netbox upgrade alone cut the nested query by ~3.7x (2m47.7s → 45.38s) even with no client-side change at all — consistent with the small instance, confirming these bugs scale with row count as expected (bigger inventory, bigger fixed win). On top of the upgraded server, the flat+concurrent approach is still worth ~7.25x (45.38s → 6.261s) — so this wasn't just working around soon-to-be-fixed Netbox bugs, there's a real, independent client-side win underneath. Combined, old-nested-on-pre-upgrade vs. flat-concurrent-on-4.6.8: 2m47.7s → 6.261s, ~26.8x. REST stayed just as broken (2m39.9s → 2m14.1s, no meaningful change) — the same conclusion as the small instance: REST's interfaces-endpoint slowness is unrelated to this bug cluster and unaffected by the upgrade.

Testing

netboxtool_test.go covers GetTenant's REST filtered lookup; netboxtool_flat_test.go covers GetDevices/GetVMs' flat-fetch-and- stitch path end-to-end (a fake server that routes each GraphQL request by inspecting its query text) plus stitchDevices/stitchVMs edge cases. Both use httptest.NewServer — no real Netbox instance needed to run go test ./....

Anything not covered by those (a real query rejected by an actual Netbox schema, real-world timing) still means talking to a real (or lab) NetBox instance — for example factum2-netbox sync against dev/.

Documentation

Overview

Package netboxtool is Factum's NetBox GraphQL/REST client (devices, VMs, interfaces, addresses, cables, VRFs, L2VPNs). Reads go through GraphQL; writes go through REST. See README.md in this directory for query architecture and pagination notes.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache wraps a CacheSource with an in-memory, per-instance cache of devices (keyed by name) and device-type interface templates (keyed by "manufacturer/model"). It is not safe for concurrent use.

func NewCache

func NewCache(src CacheSource) *Cache

func (*Cache) GetDevice

func (c *Cache) GetDevice(name string, refresh bool) (*NBDevice, error)

GetDevice returns one device, from cache unless refresh is set. Returns nil, nil (not an error) for an unknown name, matching the underlying source's GetDevice.

func (*Cache) GetDevices

func (c *Cache) GetDevices() ([]*NBDevice, error)

GetDevices fetches every device and (re)primes the cache.

func (*Cache) RefreshDevice

func (c *Cache) RefreshDevice(device *NBDevice) (*NBDevice, error)

RefreshDevice reloads device and all its related data from the source - callers use this after a mutation so later cached lookups see the new state.

func (*Cache) TemplateInterfaceTypes

func (c *Cache) TemplateInterfaceTypes(manufacturer, model string) (map[string]string, error)

TemplateInterfaceTypes returns the Netbox interface type (e.g. "1000base-t") of every interface defined by a device type's template (manufacturer+model), keyed by interface name. Returns nil, nil (not an error) when manufacturer or model is empty, since there's no template to look up. Results are cached per manufacturer/model for the life of the Cache.

type CacheSource

type CacheSource interface {
	GetDevices() ([]*NBDevice, error)
	GetDevice(name string, id int) (*NBDevice, error)
	GetDeviceType(manufacturer, model string) (*NetboxDeviceTypeDetail, error)
}

CacheSource is the subset of NetboxClient that Cache needs.

type ConfigNetbox

type ConfigNetbox struct {
	URL      string `boa:"configonly" yaml:"url"`
	Token    string `boa:"configonly" yaml:"token"`
	Insecure bool   `boa:"configonly" yaml:"insecure"`
}

Configuration file YAML structure

type ConfigRoot

type ConfigRoot struct {
	Netbox ConfigNetbox `yaml:"netbox"`
}
var Config ConfigRoot

type InterfaceCustomFields

type InterfaceCustomFields struct {
	InterfaceRole string         `json:"interface_role"`
	All           map[string]any `json:"-"`
}

InterfaceCustomFields is dcim.Interface.custom_fields. interface_role is the one field this package copies onto NBInterface.CfRole; everything else is available via All / NBInterface.CustomFields.

func (*InterfaceCustomFields) UnmarshalJSON

func (c *InterfaceCustomFields) UnmarshalJSON(data []byte) error

type InterfaceTypeChoice

type InterfaceTypeChoice struct {
	Value string
	Label string
}

InterfaceTypeChoice is one dcim.Interface.type value from NetBox's OPTIONS metadata (not a first-class API object).

type JSONDevice

type JSONDevice struct {
	ID          uint               `json:"id,string"`
	Name        string             `json:"name"`
	Comments    string             `json:"comments"`
	Status      string             `json:"status"`
	DeviceType  *NetboxDeviceType  `json:"device_type"`
	Platform    *NetboxPlatform    `json:"platform"`
	Role        *NetboxRole        `json:"role"`
	Site        *NetboxSite        `json:"site"`
	Latitude    *NBDecimal         `json:"latitude"`
	Longitude   *NBDecimal         `json:"longitude"`
	PrimaryIPv4 *NetboxAddress     `json:"primary_ip4"`
	PrimaryIPv6 *NetboxAddress     `json:"primary_ip6"`
	Tags        []NetboxTag        `json:"tags"`
	CF          NetboxCustomFields `json:"custom_fields"`
	Interfaces  []JSONInterface    `json:"interfaces"`
}

Response from Graphql device_list / virtual_machine_list query. device_type/role/site/platform and interfaces (with their ip addresses) are nested inline in the query (see deviceListGraphQLbody/ ids.

type JSONDevices

type JSONDevices struct {
	Data struct {
		// virtualMachineListGraphQLbody), so this shape carries names, not just
		Devices []JSONDevice `json:"device_list"`
	} `json:"data"`
}

type JSONInterface

type JSONInterface struct {
	ID          uint   `json:"id,string"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Enabled     bool   `json:"enabled"`
	Type        string `json:"type"`
	// Mode is dcim.Interface.mode ("access", "tagged", "tagged-all" or
	// "q-in-q"), "" if the interface isn't a switchport at all.
	Mode   string `json:"mode"`
	Label  string `json:"label"`
	Parent *struct {
		ID uint `json:"id,string"`
	} `json:"parent"`
	VRF   *NetboxVRFRef `json:"vrf"`
	Cable *struct {
		ID uint `json:"id,string"`
	} `json:"cable"`
	UntaggedVLAN *NetboxVlanRef  `json:"untagged_vlan"`
	TaggedVLANs  []NetboxVlanRef `json:"tagged_vlans"`
	// QinQSVlan is dcim.Interface.qinq_svlan - the S-VLAN Netbox uses in
	// place of untagged_vlan when mode is "q-in-q", mutually exclusive with
	// UntaggedVLAN.
	QinQSVlan    *NetboxVlanRef        `json:"qinq_svlan"`
	Tags         []NetboxTag           `json:"tags"`
	IPAddresses  []NetboxAddress       `json:"ip_addresses"`
	CustomFields InterfaceCustomFields `json:"custom_fields"`
}

type JSONSites

type JSONSites struct {
	Data struct {
		Sites []NetboxSite `json:"site_list"`
	} `json:"data"`
}

JSONSites is a page of the top-level site_list GraphQL query (see siteListGraphQLbody), reusing NetboxSite since its fields (id/name/ latitude/longitude) already match what device_list's nested "site" selection returns.

type JSONTenant

type JSONTenant struct {
	ID   uint               `json:"id,string"`
	Name string             `json:"name"`
	Slug string             `json:"slug"`
	CF   TenantCustomFields `json:"custom_fields"`
}

JSONTenant is a tenancy.Tenant row, fetched via GraphQL (see tenantListGraphQLbody).

type JSONTenants

type JSONTenants struct {
	Data struct {
		Tenants []JSONTenant `json:"tenant_list"`
	} `json:"data"`
}

type JSONVMs

type JSONVMs struct {
	Data struct {
		Devices []JSONDevice `json:"virtual_machine_list"`
	} `json:"data"`
}

type NBAddress

type NBAddress struct {
	NBModel
	NBAddressID   uint   `json:"address_id"`
	NBInterfaceID uint   `json:"interface_id"`
	NetboxID      uint   `json:"netbox_id"`
	Address       string `gorm:"type:varchar(80)"`
	// Role is ipam.IPAddress.role (e.g. "anycast"), "" if unset.
	Role string `gorm:"type:varchar(80)"`
	// VRF is the name of the VRF this address belongs to, "" for the
	// global/default VRF.
	VRF string `gorm:"type:varchar(255)"`
}

type NBCable

type NBCable struct {
	NetboxID   uint
	AInterface uint // interface ID on the A side
	BInterface uint // interface ID on the B side
	Label      string
}

NBCable is a dcim.Cable connecting exactly two interfaces (the only shape this package creates and returns - Netbox cables can in principle terminate on more than two endpoints, e.g. distribution cables, which this type doesn't attempt to represent).

type NBCustomField

type NBCustomField struct {
	NetboxID    uint
	Name        string
	Type        string // "text", "integer", "boolean", "select", ...
	Label       string
	Description string
	Required    bool
	GroupName   string
	ObjectTypes []string
	ChoiceSetID uint
}

NBCustomField is extras.CustomField. ObjectTypes are Netbox object-type strings such as "dcim.device" or "dcim.interface".

func (*NBCustomField) AssignedTo

func (f *NBCustomField) AssignedTo(objectType string) bool

AssignedTo reports whether this custom field is assigned to objectType (e.g. "dcim.device").

type NBDecimal

type NBDecimal float64

NBDecimal unmarshals a Netbox GraphQL Decimal scalar (used for latitude/longitude), which may be serialized as either a bare JSON number or a JSON string, into a float64.

func (*NBDecimal) UnmarshalJSON

func (d *NBDecimal) UnmarshalJSON(b []byte) error

type NBDevice

type NBDevice struct {
	NBModel
	VM             bool
	NetboxID       uint   `json:"netbox_id"`
	Name           string `json:"name" gorm:"type:varchar(255)"`
	Comments       string `json:"comments" gorm:"type:varchar(255)"`
	Enabled        bool   `json:"enabled"`
	Manufacturer   string `json:"manufacturer" gorm:"type:varchar(255)"`
	ManufacturerID uint   `json:"manufacturer_id"`
	ModelName      string `json:"model_name" gorm:"type:varchar(255)"`
	ModelID        uint   `json:"model_id"`
	Platform       string `json:"platform" gorm:"type:varchar(255)"`
	PlatformID     uint   `json:"platform_id"`
	PrimaryIPv4    string `json:"primary_ipv4" gorm:"type:varchar(255)"`
	PrimaryIPv4ID  uint   `json:"primary_ipv4_id"`
	PrimaryIPv6    string `json:"primary_ipv6" gorm:"type:varchar(255)"`
	PrimaryIPv6ID  uint   `json:"primary_ipv6_id"`
	Role           string `json:"role" gorm:"type:varchar(255)"`
	RoleID         uint   `json:"role_id"`
	Site           string `json:"site" gorm:"type:varchar(255)"`
	SiteID         uint   `json:"site_id"`
	Status         string `json:"status" gorm:"type:varchar(255)"`
	// Latitude/Longitude are the device's own GPS coordinates if set in
	// Netbox, else inherited from its site (resolved in parseDevices) -
	// nil if neither the device nor its site has coordinates.
	Latitude           *float64 `json:"latitude"`
	Longitude          *float64 `json:"longitude"`
	CfAlarmTimeperiod  string   `json:"cf_alarm_timeperiod" gorm:"type:varchar(255)"`
	CfAlarmDestination string   `json:"cf_alarm_destination" gorm:"type:varchar(255)"`
	CfAlarmInterfaces  bool     `json:"cf_alarm_interfaces"`
	CfBackupOxidized   bool     `json:"cf_backup_oxidized"`
	CfConnectionMethod string   `json:"cf_connection_method" gorm:"type:varchar(255)"`
	CfLocation         string   `json:"cf_location" gorm:"type:varchar(255)"`
	CfMonitorGrafana   bool     `json:"cf_monitor_grafana"`
	CfMonitorIcinga    bool     `json:"cf_monitor_icinga"`
	CfMonitorLibrenms  bool     `json:"cf_monitor_librenms"`
	CfSource           string   `json:"cf_source" gorm:"type:varchar(255)"`
	CfSourceID         uint     `json:"cf_source_id"`
	// CfParents is the comma-separated "parents" custom field.
	CfParents string `json:"cf_parents"`
	// CustomFields is the raw custom_fields object from Netbox, including
	// keys this package also copies onto typed Cf* fields. Callers that
	// need an instance-specific field not modelled here read it from here.
	CustomFields map[string]any `json:"custom_fields"`
	LibrenmsID   uint           `json:"librenms_id"`
	Interfaces   []NBInterface  `json:"interfaces"` // gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
	Tags         []NBTag        `json:"tags"`
}

func (*NBDevice) IsTag

func (d *NBDevice) IsTag(name string) bool

IsTag reports whether the device has a tag with the given name.

type NBInterface

type NBInterface struct {
	NBModel
	NBDeviceID  uint   `json:"nbdevice_id"`
	NetboxID    uint   `json:"netbox_id"`
	Name        string `json:"name" gorm:"type:varchar(255)"`
	Description string `json:"description" gorm:"type:varchar(255)"`
	Enabled     bool   `json:"enabled"`
	Type        string `json:"type" gorm:"type:varchar(255)"`
	VRF         string `json:"vrf" gorm:"type:varchar(255)"`
	CfRole      string `json:"cf_role" gorm:"type:varchar(255)"`
	// runtime data
	LineProtocolStatus string `gorm:"-"`
	InterfaceStatus    string `gorm:"-"`
	// CableID is the netbox ID of the cable terminated on this interface, 0
	// if none - populated by GraphQL reads (deviceListGraphQLbody's
	// interfaces{cable{id}}), not stored.
	CableID uint `gorm:"-"`
	// Label is dcim.Interface.label, Netbox's free-text interface label
	// (distinct from Name) - populated by GraphQL reads
	// (deviceListGraphQLbody's interfaces{label}), not stored.
	Label string `gorm:"-"`
	// ParentID is the netbox ID of this interface's parent interface, 0 if
	// none - populated by GraphQL reads (deviceListGraphQLbody's
	// interfaces{parent{id}}), not stored.
	ParentID uint `gorm:"-"`
	// UntaggedVLAN/TaggedVLANs are VIDs (not Netbox IDs), populated by
	// GraphQL reads (deviceListGraphQLbody's interfaces{untagged_vlan,
	// tagged_vlans}), not stored. UntaggedVLAN is 0 if unset.
	UntaggedVLAN int   `gorm:"-"`
	TaggedVLANs  []int `gorm:"-"`
	// VLANNames maps VID -> name for this interface's untagged/tagged/qinq
	// VLANs, from nested GraphQL vlan.name. Not stored on NBInterface.
	VLANNames map[int]string `gorm:"-"`
	// Mode is dcim.Interface.mode ("access", "tagged", "tagged-all" or
	// "q-in-q"), populated by GraphQL reads (deviceListGraphQLbody's
	// interfaces{mode}), not stored. "" if the interface isn't a switchport.
	Mode string `gorm:"-"`
	// CustomFields is the raw custom_fields object from Netbox. Typed
	// fields this package knows about (currently CfRole) are also copied
	// out separately.
	CustomFields map[string]any `json:"custom_fields"`

	Addresses []NBAddress `json:"addresses"` // gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
	Tags      []NBTag     `json:"tags"`
}

func (*NBInterface) IsTag

func (i *NBInterface) IsTag(name string) bool

IsTag reports whether the interface has a tag with the given name.

type NBL2VPN

type NBL2VPN struct {
	NetboxID   uint
	Name       string
	Slug       string
	Type       string // e.g. "evpl"
	Identifier int
}

NBL2VPN is an ipam.L2VPN row (e.g. an EVPL/ELINE service).

type NBL2VPNTermination

type NBL2VPNTermination struct {
	NetboxID    uint
	L2VPNID     uint
	InterfaceID uint
}

NBL2VPNTermination is an ipam.L2VPNTermination row, binding an L2VPN to one assigned interface (dcim.interface).

type NBModel

type NBModel struct {
	ID        uint      `json:"id" gorm:"primarykey"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type NBParent

type NBParent struct {
	NBModel
	NBDeviceID uint `json:"device_id"`
}

type NBPrefix

type NBPrefix struct {
	NetboxID uint
	Prefix   string
	Status   string
}

NBPrefix is an ipam.Prefix row.

type NBTag

type NBTag struct {
	NBModel
	NBDeviceID    uint   `json:"device_id"`
	NBInterfaceID uint   `json:"interface_id"`
	NetboxID      uint   `json:"netbox_id"`
	Name          string `json:"name" gorm:"type:varchar(255)"`
}

type NBTenant

type NBTenant struct {
	NBModel
	NetboxID   uint   `json:"netbox_id"`
	Name       string `json:"name" gorm:"type:varchar(255)"`
	Slug       string `json:"slug" gorm:"type:varchar(255)"`
	CfSource   string `json:"cf_source" gorm:"type:varchar(255)"`
	CfSourceID string `json:"cf_source_id" gorm:"type:varchar(255)"`
}

type NBVRF

type NBVRF struct {
	NetboxID    uint
	Name        string
	RD          string
	Description string
}

NBVRF is an ipam.VRF row.

type NBVlan

type NBVlan struct {
	NetboxID uint
	VID      int
	Name     string
	GroupID  uint
}

NBVlan is an ipam.VLAN row, scoped to a VLAN group.

type NBVlanGroup

type NBVlanGroup struct {
	NetboxID uint
	Name     string
	Slug     string
}

NBVlanGroup is an ipam.VLANGroup row. CreateVlanGroup always creates groups as global (unscoped).

type NetboxAddress

type NetboxAddress struct {
	ID      uint          `json:"id,string"`
	Address string        `json:"address"`
	Role    string        `json:"role"`
	VRF     *NetboxVRFRef `json:"vrf"`
}

type NetboxAddressREST

type NetboxAddressREST struct {
	ID      uint   `json:"id"`
	Address string `json:"address"`
}

NetboxAddressREST is an ipam.IPAddress row as returned by the REST API (unlike NetboxAddress, whose `id,string` tag matches the GraphQL ID scalar, this one's id is a plain JSON number).

type NetboxClient

type NetboxClient struct {
	P      ConfigNetbox
	Device []*NBDevice
	// contains filtered or unexported fields
}

client

func NewNetboxClient

func NewNetboxClient(p ConfigNetbox) (*NetboxClient, error)

---------------------------------------------------------------------------

Functions

---------------------------------------------------------------------------

func (*NetboxClient) AddressCreate

func (nb *NetboxClient) AddressCreate(address string) (*NetboxAddressREST, error)

Create an address, not assigned to any interface.

func (*NetboxClient) AddressDelete

func (nb *NetboxClient) AddressDelete(address_id int) error

Delete an address

func (*NetboxClient) AddressUpdate

func (nb *NetboxClient) AddressUpdate(address_id int, changes map[string]any) error

Update an address (e.g. its role or vrf). PATCH /api/ipam/ip-addresses/{id}/

func (*NetboxClient) CreateCable

func (nb *NetboxClient) CreateCable(aInterfaceID, bInterfaceID uint) (*NBCable, error)

CreateCable creates a cable directly connecting two interfaces.

func (*NetboxClient) CreateCableWithOptions

func (nb *NetboxClient) CreateCableWithOptions(aInterfaceID, bInterfaceID uint, extra map[string]any) (*NBCable, error)

CreateCableWithOptions is CreateCable plus arbitrary extra REST fields (e.g. "label": "uplink"). extra may be nil.

func (*NetboxClient) CreateDevice

func (nb *NetboxClient) CreateDevice(name string, extra map[string]any) (*NetboxDeviceREST, error)

CreateDevice creates a physical device. extra may set site, device_type, role, platform, status, tags, custom_fields, and any other writable device field alongside name - same shape as CreateTenant.

func (*NetboxClient) CreateInterfaceAddress

func (nb *NetboxClient) CreateInterfaceAddress(interfaceID uint, address string, extra map[string]any) (*NBAddress, error)

CreateInterfaceAddress creates an ip address assigned to interfaceID, plus arbitrary extra REST fields (e.g. "role": "anycast", "vrf": {"name": ...}) - unlike InterfaceAddressCreate, which only sends address/status, this is for callers that also set VRF and role (Netbox models those on the address, not the interface).

func (*NetboxClient) CreateInterfaceWithOptions

func (nb *NetboxClient) CreateInterfaceWithOptions(deviceID uint, name string, extra map[string]any) (*NetboxInterfaceREST, error)

CreateInterfaceWithOptions is InterfaceCreate plus arbitrary extra REST fields (e.g. "type": "lag", "vrf": {"name": ...}) - for callers that know more about the interface being created than InterfaceCreate's fixed "virtual" type allows for.

func (*NetboxClient) CreateL2VPN

func (nb *NetboxClient) CreateL2VPN(name, slug, l2vpnType string, identifier int) (*NBL2VPN, error)

CreateL2VPN creates a new L2VPN (e.g. an ELINE service), identified by identifier (the pseudowire ID) - l2vpnType is one of Netbox's L2VPN type choices, e.g. "evpl". identifier is omitted from the payload when 0 (Netbox treats it as optional; same-device patches often have no PWID).

func (*NetboxClient) CreateL2VPNTermination

func (nb *NetboxClient) CreateL2VPNTermination(l2vpnID, interfaceID uint) (*NBL2VPNTermination, error)

CreateL2VPNTermination terminates l2vpnID on interfaceID - unlike a cable's a_terminations/b_terminations (object_type/object_id), an L2VPN termination uses Netbox's generic "assigned object" fields (assigned_object_type/assigned_object_id).

func (*NetboxClient) CreatePrefix

func (nb *NetboxClient) CreatePrefix(prefix string) (*NBPrefix, error)

CreatePrefix creates a new prefix with status "active".

func (*NetboxClient) CreateSite

func (nb *NetboxClient) CreateSite(name, slug string, extra map[string]any) (*NetboxSiteREST, error)

CreateSite creates a Netbox site. Status defaults to "active"; extra may set latitude/longitude and any other writable site field. Name and slug must be unique.

func (*NetboxClient) CreateTenant

func (nb *NetboxClient) CreateTenant(name, slug string, changes map[string]any) (*NetboxTenantREST, error)

CreateTenant creates a tenant. Netbox requires both name and slug to be unique; changes may set "custom_fields" (map[string]any) and any other writable tenant field alongside name/slug.

func (*NetboxClient) CreateVRF

func (nb *NetboxClient) CreateVRF(name, rd, description string) (*NBVRF, error)

CreateVRF creates a VRF. rd is omitted when empty (Netbox treats it as optional).

func (*NetboxClient) CreateVlan

func (nb *NetboxClient) CreateVlan(vid int, name string, groupID uint) (*NBVlan, error)

CreateVlan creates a new VLAN in groupID.

func (*NetboxClient) CreateVlanGroup

func (nb *NetboxClient) CreateVlanGroup(name, slug string) (*NBVlanGroup, error)

CreateVlanGroup creates a new, global (unscoped) VLAN group.

func (*NetboxClient) DeleteCable

func (nb *NetboxClient) DeleteCable(cableID uint) error

DeleteCable deletes a cable.

func (*NetboxClient) DeleteDevice

func (nb *NetboxClient) DeleteDevice(id int) error

Delete a device in netbox, specificed by device id

func (*NetboxClient) DeleteL2VPN

func (nb *NetboxClient) DeleteL2VPN(l2vpnID uint) error

DeleteL2VPN deletes an L2VPN.

func (*NetboxClient) DeleteL2VPNTermination

func (nb *NetboxClient) DeleteL2VPNTermination(terminationID uint) error

DeleteL2VPNTermination deletes an L2VPN termination.

func (*NetboxClient) EnsureTag

func (nb *NetboxClient) EnsureTag(name, slug string) (*NetboxNamedRef, error)

EnsureTag returns the tag with the given slug, creating it (name+slug) if it does not already exist.

func (*NetboxClient) GetCable

func (nb *NetboxClient) GetCable(cableID uint) (*NBCable, error)

GetCable fetches one cable by id - interfaces already carry their own cable id (NBInterface.CableID, from the GraphQL device query), so this is how a caller resolves it to the cable's actual terminations.

func (*NetboxClient) GetCables

func (nb *NetboxClient) GetCables() ([]*NBCable, error)

GetCables fetches every cable in Netbox via the REST API, paginating until "next" is null. Unlike GetCable, which resolves one already-known cable id, this is how a caller discovers the full set of cables - e.g. to build a device connectivity map, not just the ones this package itself created. Cables not directly connecting two interfaces (see isInterfaceToInterface) are silently skipped rather than represented with a wrong/zero AInterface or BInterface.

func (*NetboxClient) GetCustomField

func (nb *NetboxClient) GetCustomField(name string) (*NBCustomField, error)

GetCustomField looks up extras.CustomField by exact name via REST. Returns nil, nil if none matches.

func (*NetboxClient) GetDevice

func (nb *NetboxClient) GetDevice(name string, id int) (*NBDevice, error)

GetDevice fetches one device by name or id, returning nil, nil (not an error) if none matches - mirroring pynetbox's own .get(), which returns None rather than raising for an unknown name.

func (*NetboxClient) GetDeviceRoleBySlug

func (nb *NetboxClient) GetDeviceRoleBySlug(slug string) (*NetboxNamedRef, error)

GetDeviceRoleBySlug looks up a device role by slug. Returns nil, nil if none matches.

func (*NetboxClient) GetDeviceType

func (nb *NetboxClient) GetDeviceType(manufacturer string, model string) (*NetboxDeviceTypeDetail, error)

GetDeviceType looks up a device type by manufacturer name+model, and fetches its interface templates in the same nested GraphQL query (device_type_list.interfacetemplates), rather than a separate REST request.

func (*NetboxClient) GetDevices

func (nb *NetboxClient) GetDevices() ([]*NBDevice, error)

GetDevices fetches every physical device, via getDevicesFlat's flat-queries-fetched-concurrently strategy (netboxtool_flat.go) rather than GetDevices_'s single nested query - benchmarked at ~8x faster on a 1400-device/37000-interface instance (see cmd/benchmark.go). A name/id-filtered single-device lookup doesn't have the same problem, so GetDevice still goes through GetDevices_.

func (*NetboxClient) GetDevices_

func (nb *NetboxClient) GetDevices_(name string, id int) ([]*NBDevice, error)

GetDevices_ fetches devices via a single nested device_list query (device_type/role/site/platform and interfaces/ip addresses are all resolved server-side, see deviceListGraphQLbody). name/id, if set, filter device_list itself so only the matching device is fetched.

func (*NetboxClient) GetInterfaceCable

func (nb *NetboxClient) GetInterfaceCable(cableID uint) (*NBCable, error)

GetInterfaceCable is GetCable for the webhook/single-object path: a 404 or a cable that is not exactly one dcim.interface on each end returns nil, nil (a two-interface connection model cannot represent those).

func (*NetboxClient) GetInterfaceTypeChoices

func (nb *NetboxClient) GetInterfaceTypeChoices() ([]InterfaceTypeChoice, error)

GetInterfaceTypeChoices returns the Interface.type choice list from OPTIONS /api/dcim/interfaces/. NetBox has no /dcim/interface-types/ resource; types are a ChoiceSet.

func (*NetboxClient) GetL2VPN

func (nb *NetboxClient) GetL2VPN(l2vpnID uint) (*NBL2VPN, error)

GetL2VPN fetches one L2VPN by id.

func (*NetboxClient) GetL2VPNByIdentifier

func (nb *NetboxClient) GetL2VPNByIdentifier(identifier int) (*NBL2VPN, error)

GetL2VPNByIdentifier looks up an existing L2VPN by its identifier (pseudowire ID), returning nil (no error) if none exists. Fallback when the on-device name doesn't match the Netbox L2VPN name but the pseudowire ID does.

func (*NetboxClient) GetL2VPNByName

func (nb *NetboxClient) GetL2VPNByName(name string) (*NBL2VPN, error)

GetL2VPNByName looks up an existing L2VPN by its exact name, returning nil (no error) if none exists.

func (*NetboxClient) GetL2VPNTerminations

func (nb *NetboxClient) GetL2VPNTerminations(l2vpnID uint) ([]*NBL2VPNTermination, error)

GetL2VPNTerminations returns every termination on l2vpnID (typically 1-2 for an EVPL). Paginated the same way as GetCables in case a multipoint L2VPN ever lands here with more than one page of ends.

func (*NetboxClient) GetL2VPNs

func (nb *NetboxClient) GetL2VPNs(l2vpnType string) ([]*NBL2VPN, error)

GetL2VPNs fetches every L2VPN in Netbox via the REST API, paginating until "next" is null. When l2vpnType is non-empty (e.g. "evpl"), only that type is returned.

func (*NetboxClient) GetManufacturer

func (nb *NetboxClient) GetManufacturer(name string, id int) (*NetboxManufacturer, error)

GetManufacturer looks up a manufacturer by its display name (e.g. "Cisco") or, if id is set (id > 0), by id.

func (*NetboxClient) GetPlatformBySlug

func (nb *NetboxClient) GetPlatformBySlug(slug string) (*NetboxNamedRef, error)

GetPlatformBySlug looks up a platform by slug. Returns nil, nil if none matches.

func (*NetboxClient) GetPrefix

func (nb *NetboxClient) GetPrefix(prefix string) (*NBPrefix, error)

GetPrefix looks up an existing prefix by its exact CIDR value, returning nil (no error) if none exists.

func (*NetboxClient) GetSite

func (nb *NetboxClient) GetSite(id uint) (*NetboxSite, error)

GetSite fetches one site by id via REST. Returns nil, nil if the site is gone, is the placeholder "Default" site, or has no coordinates — the same filter GetSites applies, so a webhook create/update of an unplottable site is a no-op (or a delete of a previously-plotted row).

func (*NetboxClient) GetSiteByName

func (nb *NetboxClient) GetSiteByName(name string) (*NetboxNamedRef, error)

GetSiteByName looks up a site by its exact display name, including the placeholder "Default" site that GetSites skips. Returns nil, nil if none matches.

func (*NetboxClient) GetSiteBySlug

func (nb *NetboxClient) GetSiteBySlug(slug string) (*NetboxNamedRef, error)

GetSiteBySlug looks up a site by slug, including "Default". Returns nil, nil if none matches.

func (*NetboxClient) GetSites

func (nb *NetboxClient) GetSites() ([]*NetboxSite, error)

GetSites fetches every Netbox site, for callers that need the full site inventory rather than just the sites referenced by a device (e.g. the network map, which also plots sites with no devices of their own). Sites with no coordinates, and the placeholder "Default" site, are skipped - same filtering parseDevices applies to a device's own site reference.

func (*NetboxClient) GetTagBySlug

func (nb *NetboxClient) GetTagBySlug(slug string) (*NetboxNamedRef, error)

GetTagBySlug looks up a tag by slug. Returns nil, nil if none matches.

func (*NetboxClient) GetTenant

func (nb *NetboxClient) GetTenant(source, sourceID string) (*NBTenant, error)

GetTenant looks up the single tenant whose custom fields match source/sourceID (see TenantCustomFields), via a server-side filtered REST call instead of GetTenants' full-table fetch - the point being O(1) instead of O(tenant count) for a caller that only ever wants one row. Returns nil, nil (not an error) if no tenant matches.

func (*NetboxClient) GetTenants

func (nb *NetboxClient) GetTenants() ([]*NBTenant, error)

GetTenants fetches all tenants via a single tenant_list query.

func (*NetboxClient) GetVM

func (nb *NetboxClient) GetVM(name string, id int) (*NBDevice, error)

GetVM fetches one virtual machine by name or id, returning nil, nil (not an error) if none matches - mirroring GetDevice/pynetbox's own .get(), which returns None rather than raising for an unknown name.

func (*NetboxClient) GetVM_

func (nb *NetboxClient) GetVM_(name string, id int) ([]*NBDevice, error)

GetVM_ is the virtual_machine equivalent of GetDevices_: a single nested virtual_machine_list query resolves site and interfaces/ip addresses server-side, and name/id, if set, filter virtual_machine_list itself so only the matching VM is fetched.

func (*NetboxClient) GetVMs

func (nb *NetboxClient) GetVMs() ([]*NBDevice, error)

GetVMs is GetDevices' virtual-machine equivalent - see getVMsFlat.

func (*NetboxClient) GetVRFByName

func (nb *NetboxClient) GetVRFByName(name string) (*NBVRF, error)

GetVRFByName looks up an existing VRF by its exact name, returning nil (no error) if none exists.

func (*NetboxClient) GetVlan

func (nb *NetboxClient) GetVlan(vid int, groupID uint) (*NBVlan, error)

GetVlan looks up an existing VLAN by VID within groupID, returning nil (no error) if none exists.

func (*NetboxClient) GetVlanGroup

func (nb *NetboxClient) GetVlanGroup(name string) (*NBVlanGroup, error)

GetVlanGroup looks up an existing VLAN group by its exact name, returning nil (no error) if none exists.

func (*NetboxClient) InterfaceAddressCreate

func (nb *NetboxClient) InterfaceAddressCreate(intf NetboxInterface, address NetboxAddress, status string) (*NetboxAddressREST, error)

Create an address on an interface

func (*NetboxClient) InterfaceCreate

func (nb *NetboxClient) InterfaceCreate(device_id int, name string) (*NetboxInterfaceREST, error)

Create an interface on a device. Netbox's graphql API is read-only, so this goes through the REST API instead (POST /api/dcim/interfaces/). "virtual" is used as the interface type since callers don't supply hardware-specific type info.

func (*NetboxClient) InterfaceDelete

func (nb *NetboxClient) InterfaceDelete(interface_id int) error

Delete an interface

func (*NetboxClient) InterfaceUpdate

func (nb *NetboxClient) InterfaceUpdate(interface_id int, changes map[string]any) error

Update an interface. changes is any, not map[string]string, so callers can send nested values (e.g. {"vrf": {"name": "MGMT"}}). PATCH /api/dcim/interfaces/{id}/

func (*NetboxClient) NetboxAPICall

func (nb *NetboxClient) NetboxAPICall(grapqlQuery string, name string, id int, start, limit int) ([]byte, error)

Helper function, to fetch a page of

  • all devices/vms
  • one device/vm, selected by name
  • one device/vm, selected by id

start is a cursor (the Netbox object id to resume from, "id >= start"), not a row offset - Netbox's offset-based pagination costs more the further into the table you page (Postgres has to scan and discard `offset` rows), while cursor pagination is a constant-cost index seek regardless of position. See fetchAllPages.

func (*NetboxClient) RequireCustomField

func (nb *NetboxClient) RequireCustomField(name string, objectTypes ...string) error

RequireCustomField returns an error unless a custom field with the given name exists and is assigned to every object type in objectTypes (e.g. "dcim.device"). Callers that only care that the field exists can pass no object types.

func (*NetboxClient) RestGet

func (nb *NetboxClient) RestGet(endpoint string, out any) error

RestGet, RestPost and RestPatch expose the REST helpers for callers that talk to extras endpoints this package does not wrap (custom fields, webhooks, event rules).

func (*NetboxClient) RestPatch

func (nb *NetboxClient) RestPatch(endpoint string, payload, out any) error

func (*NetboxClient) RestPost

func (nb *NetboxClient) RestPost(endpoint string, payload, out any) error

func (*NetboxClient) UpdateCable

func (nb *NetboxClient) UpdateCable(cableID, aInterfaceID, bInterfaceID uint, extra map[string]any) error

UpdateCable replaces both terminations and any extra REST fields (e.g. "label") on an existing cable.

func (*NetboxClient) UpdateCableTermination

func (nb *NetboxClient) UpdateCableTermination(cableID uint, side int, interfaceID uint) error

UpdateCableTermination replaces one side of an existing cable with interfaceID - side 1 is the A side, side 2 is the B side.

func (*NetboxClient) UpdateDevice

func (nb *NetboxClient) UpdateDevice(deviceID uint, changes map[string]any) error

UpdateDevice updates a Netbox device with arbitrary (possibly nested, e.g. custom_fields) values. https://netboxlabs.com/docs/netbox/en/stable/rest-api/overview/#update

func (*NetboxClient) UpdateL2VPN

func (nb *NetboxClient) UpdateL2VPN(l2vpnID uint, changes map[string]any) error

UpdateL2VPN applies changes (field -> new value) to an existing L2VPN.

func (*NetboxClient) UpdateSite

func (nb *NetboxClient) UpdateSite(siteID uint, changes map[string]any) error

UpdateSite patches a Netbox site with arbitrary writable fields (latitude/longitude, name, slug, …).

func (*NetboxClient) UpdateTenant

func (nb *NetboxClient) UpdateTenant(tenantID uint, changes map[string]any) error

UpdateTenant updates a Netbox tenant with arbitrary (possibly nested, e.g. custom_fields) values.

func (*NetboxClient) UpdateVM

func (nb *NetboxClient) UpdateVM(vmID uint, changes map[string]any) error

UpdateVM updates a Netbox virtual machine with arbitrary (possibly nested) values

func (*NetboxClient) UpdateVlan

func (nb *NetboxClient) UpdateVlan(id uint, changes map[string]any) error

UpdateVlan applies changes (field -> new value) to an existing VLAN.

type NetboxCustomFields

type NetboxCustomFields struct {
	AlarmDestination string `json:"alarm_destination"`
	AlarmInterfaces  bool   `json:"alarm_interfaces"`
	AlarmTimeperiod  string `json:"alarm_timeperiod"`
	Alias            string `json:"alias"`
	BackupOxidized   bool   `json:"backup_oxidized"`
	Location         string `json:"location"`
	MonitorIcinga    bool   `json:"monitor_icinga"`
	MonitorGrafana   bool   `json:"monitor_grafana"`
	// MonitorLibrenms is a *bool, not bool: netbox returns null for a
	// custom field that has never been set on a device (no default
	// configured), and that must be distinguished from an explicit
	// false - unset devices default to monitored, matching the old
	// (pre-Go) sync script's behavior. See the copy into
	// dbdevice.CfMonitorLibrenms below.
	MonitorLibrenms *bool  `json:"monitor_librenms"`
	Parents         string `json:"parents"`
	ConnectMethod   string `json:"connection_method"`
	LibrenmsID      int    `json:"librenms_id"`
	// All is the raw custom_fields object, including keys that also have
	// typed fields above. Callers that need an instance-specific field
	// this package does not model read it from All (or from
	// NBDevice.CustomFields, which is a copy).
	All map[string]any `json:"-"`
}

func (*NetboxCustomFields) UnmarshalJSON

func (c *NetboxCustomFields) UnmarshalJSON(data []byte) error

type NetboxDeviceManufacturer

type NetboxDeviceManufacturer struct {
	ID   uint `json:"id,string"`
	Name string
}

type NetboxDeviceREST

type NetboxDeviceREST struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
}

NetboxDeviceREST is a dcim.Device row as returned by the REST API (unlike JSONDevice, whose `id,string` tag matches the GraphQL ID scalar, this one's id is a plain JSON number).

type NetboxDeviceType

type NetboxDeviceType struct {
	ID           uint   `json:"id,string"`
	Model        string `json:"model"`
	Manufacturer *NetboxDeviceManufacturer
}

type NetboxDeviceTypeDetail

type NetboxDeviceTypeDetail struct {
	ID              uint                      `json:"id,string"`
	Model           string                    `json:"model"`
	Manufacturer    NetboxDeviceManufacturer  `json:"manufacturer"`
	DefaultPlatform *NetboxPlatform           `json:"default_platform"`
	CF              NetboxCustomFields        `json:"custom_fields"`
	Interfaces      []NetboxInterfaceTemplate `json:"interfacetemplates"`
}

NetboxDeviceTypeDetail is a device type together with its interface templates, both resolved server-side in a single nested GraphQL query (see deviceTypeGraphQLbody).

type NetboxInterface

type NetboxInterface struct {
	ID           uint            `json:"id,string"`
	Name         string          `json:"name"`
	IP_addresses []NetboxAddress `json:"ip_addresses"`
	CustomFields struct {
		InterfaceRole string `json:"interface_role"`
	} `json:"custom_fields"`
}

type NetboxInterfaceREST

type NetboxInterfaceREST struct {
	ID          uint   `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

NetboxInterfaceREST is a dcim.Interface row as returned by the REST API (unlike NetboxInterface, whose `id,string` tag matches the GraphQL ID scalar, this one's id is a plain JSON number).

type NetboxInterfaceTemplate

type NetboxInterfaceTemplate struct {
	ID          uint   `json:"id,string"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Type        string `json:"type"`
}

NetboxInterfaceTemplate is a dcim.InterfaceTemplate row, as attached to a device type.

type NetboxManufacturer

type NetboxManufacturer struct {
	ID          uint   `json:"id,string"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Comments    string `json:"comments"`
}

NetboxManufacturer is a dcim.Manufacturer row, fetched via GraphQL (see manufacturerListGraphQLbody).

type NetboxNamedRef

type NetboxNamedRef struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
	Slug string `json:"slug"`
}

NetboxNamedRef is a REST row that has id/name/slug - sites, device roles, platforms and tags all share this shape. GraphQL IDs are strings; REST IDs are numbers, so this is distinct from NetboxSite/NetboxRole.

type NetboxPlatform

type NetboxPlatform struct {
	ID   uint   `json:"id,string"`
	Name string `json:"name"`
}

type NetboxRole

type NetboxRole struct {
	ID   uint   `json:"id,string"`
	Name string `json:"name"`
}

type NetboxSite

type NetboxSite struct {
	ID        uint       `json:"id,string"`
	Name      string     `json:"name"`
	Latitude  *NBDecimal `json:"latitude"`
	Longitude *NBDecimal `json:"longitude"`
}

type NetboxSiteREST

type NetboxSiteREST struct {
	ID        uint     `json:"id"`
	Name      string   `json:"name"`
	Slug      string   `json:"slug"`
	Latitude  *float64 `json:"latitude"`
	Longitude *float64 `json:"longitude"`
}

NetboxSiteREST is a dcim.Site row as returned by the REST API (plain JSON numbers for id/latitude/longitude, unlike GraphQL NetboxSite).

type NetboxTag

type NetboxTag struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type NetboxTenantREST

type NetboxTenantREST struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
	Slug string `json:"slug"`
}

NetboxTenantREST is a tenancy.Tenant row as returned by the REST API (unlike JSONTenant, whose `id,string` tag matches the GraphQL ID scalar, this one's id is a plain JSON number).

type NetboxVRFRef

type NetboxVRFRef struct {
	ID   uint   `json:"id,string"`
	Name string `json:"name"`
}

NetboxVRFRef is the minimal VRF reference nested under an interface or ip address in GraphQL responses.

type NetboxVlanRef

type NetboxVlanRef struct {
	ID   uint   `json:"id,string"`
	VID  int    `json:"vid"`
	Name string `json:"name"`
}

Response from the interfaces field nested under - device_list (dcim.Interface) - virtual_machine_list (virtualization.VMInterface)

type Response

type Response struct {
	Status  string `json:"status"`
	Message string `json:"message"`
	Data    any    `json:"data"`
}

API structures

type TenantCustomFields

type TenantCustomFields struct {
	Source   string `json:"source"`
	SourceID string `json:"source_id"`
}

TenantCustomFields holds the subset of a tenant's custom_fields this package knows about: "source"/"source_id" identify which external system (and which record within it, e.g. a CRM customer id) a tenant was synced from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL