drakelid/librenms-netbox-context

LibreNMS NetBox Context - NetBox device, interface, cabling and circuit context inside LibreNMS device and port views

Maintainers

Package info

github.com/Drakelid/netboxcontext

pkg:composer/drakelid/librenms-netbox-context

Transparency log

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.1 2026-08-20 08:04 UTC

This package is auto-updated.

Last update: 2026-08-20 08:04:29 UTC


README

# LibreNMS NetBox Context NetBox device, interface, cabling and circuit context, shown inside the LibreNMS pages an engineer is already looking at — and compared against what LibreNMS actually observes. NetBox is the source of truth for **intent**: identity, location, ownership, interface configuration, cabling, VLANs and circuits. LibreNMS is the source of truth for **reality**: availability, observed speed, administrative and operational state, MTU as configured on the box. This plugin puts the two side by side and says plainly where they disagree. ```text NETBOX LIBRENMS intent observation │ │ │ site, rack, tenant │ up/down, traffic │ interface config │ observed speed, MTU │ cabling, circuits │ admin/oper state │ │ └──────────► NETBOX CONTEXT ◄─────┘ "What is this? Where is it? Who owns it? What should it be? What is it connected to? Does reality match NetBox?" ``` --- ## Table of contents 1. [Overview](#overview) 2. [What it looks like](#what-it-looks-like) 3. [Architecture](#architecture) 4. [Requirements](#requirements) 5. [Supported LibreNMS versions](#supported-librenms-versions) 6. [Supported NetBox versions](#supported-netbox-versions) 7. [Installation](#installation) 8. [Development installation](#development-installation) 9. [NetBox API token permissions](#netbox-api-token-permissions) 10. [Configuration](#configuration) 11. [Initial synchronisation](#initial-synchronisation) 12. [Scheduler](#scheduler) 13. [Device matching](#device-matching) 14. [Interface matching](#interface-matching) 15. [Manual mappings](#manual-mappings) 16. [Device context panel](#device-context-panel) 17. [Interface context tab](#interface-context-tab) 18. [Custom fields](#custom-fields) 19. [Cache and staleness behaviour](#cache-and-staleness-behaviour) 20. [Troubleshooting](#troubleshooting) 21. [Performance](#performance) 22. [Security](#security) 23. [Upgrading](#upgrading) 24. [Uninstalling](#uninstalling) 25. [LibreNMS daily.sh and upgrade persistence](#librenms-dailysh-and-upgrade-persistence) 26. [Using the context from another plugin](#using-the-context-from-another-plugin) 27. [Development](#development) --- ## Overview The plugin adds three things to LibreNMS: - a **NetBox Device Context** panel on the device overview page, including the device's documented cabling and how it compares with what LibreNMS discovered; - a **NetBox Interface Context** tab on the port page; - its own pages under **NetBox Context** in the navigation — status, device and interface mappings, estate-wide **topology drift**, and settings. All of them read from a local cache that a scheduled job keeps up to date. Opening a device page never calls NetBox, so a NetBox outage degrades the panel to "here is what we last knew, and how old it is" instead of slowing down or breaking LibreNMS. Beyond mirroring NetBox fields, it compares the two systems and flags: - interface speed, MTU, description and administrative-state differences; - device hostname, management IP, serial, hardware and platform differences; - cabling that disagrees with the neighbours LibreNMS discovered; - interfaces present in LibreNMS but not documented in NetBox; - devices that cannot be matched to NetBox at all; - matches that are **ambiguous** — reported, never guessed; - cached data that has gone stale. Version 1 is strictly read-only. It never writes to NetBox and never changes LibreNMS devices or ports. --- ## What it looks like Device overview panel: ```text ┌─────────────── NetBox Device Context ────────────────┐ │ NetBox status Active │ │ Site Haugesund │ │ Location HGS Sentrum │ │ Device role Aggregation Switch │ │ Tenant HK Fiber │ │ Manufacturer Cisco │ │ Model C9300L-24P-4G │ │ Serial FCW12345678 │ │ Rack HGS-R01 / U32 │ │ Primary IP 10.20.40.12/24 │ │ OOB IP 10.250.40.12 │ │ Tags access, critical │ │ │ │ NetBox <-> LibreNMS │ │ Hostname sw01… sw01… ✓ Match │ │ Management IP 10.20.40.12 10.20.40.12 ✓ Match │ │ Serial number FCW1234… FCW1234… ✓ Match │ │ Platform Cisco IOS IOS-XE ⚠ Mismatch │ │ │ │ Last NetBox sync: 2 minutes ago [Open in NetBox] │ └──────────────────────────────────────────────────────┘ ``` Port tab: ```text ┌────────────── NetBox Interface Context ──────────────┐ │ NetBox interface TenGigabitEthernet1/1/1 │ │ Description HGS-ACCESS-042 UPLINK │ │ Type SFP+ (10GE) │ │ Configured speed 10 Gbps │ │ MTU 9216 │ │ Mode Tagged │ │ Tagged VLANs 100, 210, 300, 4010 │ │ │ │ CONNECTION │ │ hgs-access-042 / Te1/1/2 │ │ cable FO-HGS-004821 │ │ Remote site Haugesund Sentrum │ │ │ │ CIRCUIT │ │ Circuit ID HKF-38291 │ │ Provider HK Fiber │ │ │ │ CONFIGURATION COMPARISON │ │ Speed 10 Gbps 1 Gbps ⚠ Mismatch │ │ MTU 9216 9216 ✓ Match │ │ Description HGS-UPL… HGS-UPL… ✓ Match │ │ Admin state Enabled Enabled ✓ Match │ └──────────────────────────────────────────────────────┘ ``` Status indicators are always a symbol **and** a word, so state is never carried by colour alone. --- ## Architecture ```text NetBox REST API │ ▼ NetBoxApiClient ─── pagination, retries, redirect rules, token handling │ ▼ Normalizers ─────── DeviceNormalizer, InterfaceNormalizer, │ ConnectionNormalizer, CircuitNormalizer ▼ DTOs ────────────── NetBoxDeviceDto, NetBoxInterfaceDto, NetBoxCircuitDto │ ▼ Sync services ───── DeviceSyncService, InterfaceSyncService, │ CircuitSyncService (chunked upserts, safe reconciliation) ▼ Local cache ─────── netbox_context_devices / _interfaces / _circuits │ ▼ Matching ────────── DeviceMatcher, InterfaceMatcher, MatchingService │ → netbox_context_device_mappings / _interface_mappings ▼ Context ─────────── DeviceContextService, InterfaceContextService, │ ComparisonService, FreshnessService ▼ Hooks ───────────── DeviceOverview (panel), PortTab (tab), Settings, MenuEntry ``` Two properties hold throughout: - **UI code makes no decisions.** Blade templates render a prepared DTO. All matching, comparison and formatting happens in services that are testable on their own. - **UI requests never call NetBox.** Every page render reads local, indexed tables. Directory layout: ```text librenms-netbox-context/ ├── composer.json ├── config/netbox-context.php ├── database/migrations/ ├── resources/views/ │ ├── hooks/ device-overview, port-tab │ ├── admin/ settings panel, status, mapping pages │ ├── components/ comparison, custom fields, freshness │ └── layouts/ ├── routes/web.php ├── src/ │ ├── NetBoxContextServiceProvider.php │ ├── Console/ test, sync, match, status commands │ ├── Contracts/ public interfaces for other plugins │ ├── DTO/ │ ├── Enums/ │ ├── Exceptions/ │ ├── Hooks/ │ ├── Http/ controllers, middleware │ ├── Models/ │ ├── Services/ Api, Sync, Matching, Comparison, Context │ └── Support/ IP, hostname, speed, text, URL, redaction helpers └── tests/ Unit, Feature, Scale ``` --- ## Requirements - PHP 8.2 or later - LibreNMS 24.9.0 or later (see below) - A reachable NetBox instance and a **read-only** API token - Nothing else: no additional services, no queue worker, no frontend framework --- ## Supported LibreNMS versions The plugin uses LibreNMS's package-plugin API (`librenms/plugin-interfaces`), which first shipped in **LibreNMS 24.9.0**. It was developed and reviewed against **26.7.0**. The device overview panel uses the same panel markup LibreNMS 26.x uses for its own overview panels. On older releases the panel still renders correctly; it simply inherits that release's default styling. No LibreNMS core file is modified. ### One documented deviation LibreNMS decides whether to show the port page's **Plugins** tab with ```php PluginManager::hasHooks(App\Plugins\Hooks\PortTabHook::class, ...) // abstract class ``` but renders that tab's content with ```php PluginManager::call(LibreNMS\Interfaces\Plugins\Hooks\PortTabHook::class, ...) // interface ``` Package plugins publish against the interface — as LibreNMS's own `PluginProvider` does for local plugins too — so the tab renders but the menu entry never appears (verified in 26.7.0). Rather than patch core, the plugin publishes a second, deliberately empty hook (`Hooks\PortTabMenu`) under the abstract class purely to satisfy the menu check. It renders nothing; the panel itself still comes from the interface registration, so there is no duplication if a future LibreNMS calls both. It is only registered when that abstract class exists, and it honours the same authorisation rule as the tab, so the menu entry does not appear for a user who would see nothing under it. --- ## Supported NetBox versions Designed against the NetBox REST API as it exists in **3.5 through 4.x**. Rather than targeting one point release, all NetBox JSON passes through a normalisation layer that: - accepts both `role` (NetBox ≥ 3.6) and `device_role` (older) on devices; - tolerates any nested object being `null` or absent; - accepts choice fields as either `{"value": …, "label": …}` or a bare string; - treats a missing optional field as "not documented", never as an error; - reads `connected_endpoints` (the traced path) in preference to `link_peers` (the immediate cable peer), and falls back cleanly when a version does not populate one of them. If NetBox does not expose an endpoint (for example the circuits module is unused), the connection test reports it and that feature degrades; the rest of the plugin keeps working. --- ## Installation Production installation is through Composer, using LibreNMS's plugin manager: ```bash cd /opt/librenms # Install the package ./lnms plugin:add drakelid/librenms-netbox-context ^1.0 # Run the plugin's migrations php artisan migrate --force ``` Then: 1. Open **Overview → Plugins** (or `/plugins`) in LibreNMS and enable **netbox-context** if it is not already enabled. 2. Open its **Settings** page. 3. Enter the NetBox URL and API token, save, and press **Test connection**. 4. Run the first synchronisation (see [Initial synchronisation](#initial-synchronisation)). > Verify the exact plugin-manager syntax against your LibreNMS version with > `./lnms list plugin`. `plugin:add` has existed since LibreNMS 22.2.0. --- ## Development installation Use a Composer path repository so edits take effect without a release: ```bash cd /opt/librenms composer config repositories.netbox-context \ '{"type":"path","url":"/opt/librenms-netbox-context","symlink":true}' ./lnms plugin:add drakelid/librenms-netbox-context @dev php artisan migrate ``` Enable plugin error reporting while developing, so a broken hook shows the error instead of silently disabling the plugin: ```bash ./lnms config:set plugins.show_errors true ``` --- ## NetBox API token permissions **Read-only is sufficient. Do not grant write permissions.** The plugin never issues anything but `GET`. The token needs read access to: | NetBox object | Endpoint | Used for | |---|---|---| | Status | `/api/status/` | connection test, version detection | | Devices | `/api/dcim/devices/` | device context and matching | | Interfaces | `/api/dcim/interfaces/` | interface context, cabling, matching | | IP addresses | `/api/ipam/ip-addresses/` | addresses assigned to interfaces | | Circuits | `/api/circuits/circuits/` | circuit context (optional) | Interface payloads carry their cable, VLAN and VRF information inline, so no separate cable or VLAN permission is required. In NetBox, create a token with **Write enabled** unchecked, and — if you use object permissions — grant `view` on `dcim.device`, `dcim.interface`, `ipam.ipaddress` and `circuits.circuit`. --- ## Configuration Settings resolve in this order, highest first: 1. **Environment variables** (URL and token only) 2. **Plugin settings** stored in `netbox_context_settings` 3. **`config/netbox-context.php`** defaults ### Environment ```bash NETBOX_CONTEXT_URL=https://netbox.example.net NETBOX_CONTEXT_API_TOKEN=0123456789abcdef0123456789abcdef01234567 ``` Setting these keeps credentials out of the database entirely, and means a database restore into another environment does not carry NetBox access with it. When the token comes from the environment, the settings page says so. ### Connecting NetBox from the UI The plugin has its own settings page at **`/plugin/netbox-context/settings`**, reachable from the *Settings* tab next to Status and the mapping pages, and from the plugin's entry in the LibreNMS Plugins menu. Enter the NetBox URL and a read-only API token, then press **Save and test** — it stores the credentials and immediately runs the full connection test, reporting reachability, TLS, authentication and read access to each endpoint separately. Nothing is synchronised until that succeeds. The same connection form also appears on LibreNMS's own plugin-admin page (**Plugins → netbox-context → Settings**), so either route works. Both require the `netbox-context.admin` ability, which LibreNMS administrators and holders of `plugin.admin` inherit. ### All settings Under either settings screen: - **Connection** — URL, API token, TLS verification, connect and request timeouts, API page size, retry attempts. - **Intervals** — device, interface and circuit sync intervals; stale threshold; database chunk size; maximum tagged VLANs shown. - **Matching** — which device and interface matching signals are enabled. - **Comparison** — which fields are compared, and how descriptions are normalised before comparison. - **Display** — which panels and sections are shown. - **Custom fields** — which NetBox custom fields to display. The **API token** field is always blank when the page loads. **Leaving it blank keeps the stored token**; there is a separate **Remove token** button. The token is never rendered, never returned to the browser, and is stored encrypted with Laravel's application key. ### NetBox-side filters On a shared NetBox you can limit what is synchronised, in `config/netbox-context.php`: ```php 'sync' => [ 'device_filters' => ['site' => ['haugesund', 'bergen']], 'interface_filters' => [], ], ``` These are passed to NetBox as query parameters, so the filtering happens server-side. --- ## Initial synchronisation ```bash cd /opt/librenms ./lnms netbox-context:test # verify connectivity and permissions ./lnms netbox-context:sync --devices # devices, then automatic matching ./lnms netbox-context:sync --interfaces # interfaces, cabling, addresses ./lnms netbox-context:sync --circuits # optional ./lnms netbox-context:status # what happened ``` Or everything at once: ```bash ./lnms netbox-context:sync --full ``` A sync automatically runs the matching pass afterwards unless you pass `--no-match`. To re-run matching alone: ```bash ./lnms netbox-context:match ./lnms netbox-context:match --device=42 # one device ./lnms netbox-context:match --prune # also drop mappings for deleted objects ``` Expect the first interface sync of a large NetBox to take several minutes; it is paginated and chunked, and subsequent runs only write objects that changed. --- ## Scheduler The plugin registers its own schedule with Laravel: ```text netbox-context:sync --due every 5 minutes, without overlapping netbox-context:match --prune daily at 03:20 ``` `--due` consults the configured per-dataset intervals (devices every 5 minutes, interfaces every 15, circuits every 30 by default) and runs only what is actually due — so the five-minute tick is cheap. This requires LibreNMS's own scheduler cron entry, which a standard install already has: ```cron * * * * * librenms /usr/bin/php /opt/librenms/artisan schedule:run >> /dev/null 2>&1 ``` If you prefer explicit cron entries instead, disable nothing and simply add: ```cron */5 * * * * librenms /opt/librenms/lnms netbox-context:sync --devices */15 * * * * librenms /opt/librenms/lnms netbox-context:sync --interfaces */30 * * * * librenms /opt/librenms/lnms netbox-context:sync --circuits ``` Concurrent runs are prevented by an expiring cache lock, so an overlapping manual run is skipped rather than doubling the API load. --- ## Device matching Matching runs in a fixed order and stops at the first step that identifies exactly one NetBox device. | # | Step | Confidence | Notes | |---|---|---|---| | 1 | Manual or locked mapping | 100 | An operator decision. Never overridden. | | 2 | Persisted mapping | 100 | The previous answer, re-verified against the cache. | | 3 | Exact hostname | 95 | LibreNMS `hostname` vs NetBox `name`, case-insensitive, trailing dot ignored. | | 4 | Primary IP | 90 | Management address vs NetBox primary IPv4/IPv6, canonicalised, prefix stripped. | | 5 | Unique short hostname | 80 | `sw01` matches `sw01.example.net` — only if exactly one candidate. | | 6 | Serial number | 75 | **Off by default.** Enable only if your serials are trustworthy. | Rules that hold at every step: - **Ambiguity is never resolved by guessing.** If a step finds more than one candidate, the mapping is recorded as `ambiguous`, the candidates are stored, and matching stops — it does not fall through to a weaker signal. - **DNS is never consulted.** A name that resolves somewhere is not evidence of identity. - Every mapping records the method, the confidence and a human-readable reason: ```text Method: exact_hostname Confidence: 95 Reason: LibreNMS hostname 'sw01.example.net' matches the NetBox device name exactly. ``` --- ## Interface matching The parent device must be mapped first. Candidates only ever come from that NetBox device — interfaces are never matched across devices. | # | Step | Confidence | Notes | |---|---|---|---| | 1 | Manual or locked mapping | 100 | Never overridden. | | 2 | Persisted mapping | 100 | Re-verified against the device's interfaces. | | 3 | Exact name | 95 | LibreNMS `ifName` (or `ifDescr` when there is no `ifName`) vs NetBox `name`, case-insensitive. | | 4 | Canonical name | 85 | Vendor abbreviation expansion, when enabled. | Canonicalisation expands a **known leading abbreviation** and leaves the numeric part alone: ```text Te1/1/1 -> tengigabitethernet1/1/1 Gi0/1 -> gigabitethernet0/1 Po10 -> port-channel10 Hu1/0/1 -> hundredgigabitethernet1/0/1 ``` What it deliberately does not do: - no substring or fuzzy matching; - no matching on descriptions — two uplinks routinely share a description, and a port mapped that way would attribute one circuit's context to another; - no rewriting of unknown prefixes: Juniper's `ae0`, `xe-0/0/0` and `irb.100` pass through untouched. The abbreviation table is not claimed to be universal. Extend or override it in `config/netbox-context.php`: ```php 'matching' => [ 'interface' => [ 'abbreviations' => ['swp' => 'swport'], ], ], ``` A canonical name that is not unique on the device is reported as ambiguous. ### Mapping states ```text MATCHED automatic match, resolved MANUAL set by an administrator AMBIGUOUS several candidates; manual mapping required UNMATCHED not documented in NetBox STALE the mapped NetBox object is no longer in the cache ``` --- ## Manual mappings **Plugins → netbox-context → Settings → Device mappings / Interface mappings**, or directly: - `/plugin/netbox-context/mappings/devices` - `/plugin/netbox-context/mappings/interfaces` Both pages filter by state, so "show me everything ambiguous" is one click. Panels for unmatched or ambiguous objects link straight to them. To map by hand, enter the NetBox object id and press **Set**. Optionally tick **lock**. - **Manual** mappings are never repointed by the automatic matcher. - **Locked** mappings are never repointed either, including mappings that were originally automatic — useful for pinning a correct match on a device whose hostname keeps changing. - **Clear manual mapping** drops the flags and lets matching decide again; the row itself is kept. - An interface may only be mapped to an interface on the device's own mapped NetBox device. Anything else is refused. Mapping changes require the `netbox-context.admin` ability and are logged with the username. --- ## Device context panel **The panel arrives fully folded.** A device page is scanned in seconds, so every section — device details, connections, topology, custom data and the comparison — is a fold that starts closed, and an engineer opens the one they came for: ```text ┌─────────────── NetBox Device Context ────────────────┐ │ ▸ Device details · Active · Haugesund │ │ ▸ Connections · 4 documented in NetBox │ │ ▸ Topology — NetBox ↔ discovered · ⚠ 1 finding │ │ ▸ NetBox ↔ LibreNMS · ⚠ 2 mismatches │ │ Last NetBox sync: 2 minutes ago [Open in NetBox] │ └──────────────────────────────────────────────────────┘ ``` Every closed header carries a count, so **folding hides detail and never hides a disagreement** — a shut comparison section still says `⚠ 2 mismatches`. The folds are native `
Details` elements: no JavaScript, no Bootstrap collapse, keyboard and screen-reader behaviour straight from the browser, and the content stays in the document so find-in-page still reaches it. Open/closed state is not remembered between page loads. Unmatched, ambiguous and error states are never folded — when something is wrong, the panel says so on arrival. Shows only fields NetBox actually has — no rows of dashes. Includes status, site, location, role, tenant, manufacturer, model, serial, asset tag, rack and position, primary and OOB IP, platform, tags, selected custom fields, an **Open in NetBox** link, and the consistency table. Device comparison covers hostname, management IP, serial number, hardware model and platform. It is deliberately tolerant of harmless differences: - one system holding the FQDN and the other the short name is a **match**; - `Cisco C9300L-24P-4G` against `C9300L-24P-4G` is a **match** (the manufacturer prefix is normalised away); - model and platform differences are raised at *notice* severity, not as warnings — a catalogue naming difference is not an incident. When there is no match the panel says so and shows what was looked for: ```text ⚠ This device is not matched to NetBox. Hostname checked sw01.example.net Short hostname checked sw01 Management IP checked 10.20.40.12 ``` ### Connections Below the context table, every cabled interface of the device as NetBox documents it — the device's physical neighbours on one screen, without opening a port at a time: ```text Connections · 4 documented in NetBox Interface Connects to Cable Te1/1/1 hgs-access-042 / Te1/1/2 FO-HGS-004821 Haugesund Sentrum Connected · smf-os2 Te1/1/2 hgs-access-043 / Te1/1/2 FO-HGS-004822 Haugesund Sentrum Connected · smf-os2 Te1/1/3 Rear port — ODF-01 / Port 18 FO-HGS-004823 Te1/1/4 Circuit termination — HKF-38291 FO-HGS-004824 ``` Details that matter: - **The far end links where it is most useful.** A neighbour this LibreNMS also monitors links to its LibreNMS device page; one it does not links out to NetBox. An *ambiguous* mapping links to neither — an unresolved match must not become a confident link to one of its candidates. - **Only cabled interfaces appear.** An access switch with four uplinks shows four rows, not forty-eight. - **Non-interface terminations are described, not flattened.** A patch panel is named as a front or rear port, exactly as on the port tab. - **Cable peers are distinguished from traced paths.** When NetBox reports only this cable's peer rather than tracing the whole path, the row says `Cable peer — path not traced by NetBox`, because that endpoint is the near side of a patch and not the far device. - **The list is capped** at `display.max_connections` (default 100) and states the total when it truncates: `Showing 100 of 384 documented connections.` - **It costs three queries** regardless of port count — the interfaces, then one batched lookup each for the far-end NetBox devices and their LibreNMS mappings. Set `display.connections` to off to remove the section entirely. ### Topology drift The cabling NetBox documents, held against the neighbours LibreNMS actually discovered over LLDP or CDP. It appears on the device panel and, for the whole estate, on the plugin's **Topology** page. ```text Topology — NetBox ↔ discovered 3 of 4 comparable links agree with NetBox. Interface State NetBox says LibreNMS discovered Te1/1/3 ⚠ Wrong neighbour hgs-access-043 / Te1/1/2 hgs-access-044 / Te1/1/2 (lldp) NetBox documents a cable to hgs-access-043; LibreNMS discovered hgs-access-044. ``` The five verdicts: | State | Meaning | Severity | |---|---|---| | ✓ Match | Both systems describe the same far end | — | | ⚠ Wrong neighbour | Both describe a far end and they disagree | warning | | ⚠ Not in NetBox | LibreNMS discovered a neighbour NetBox has no cable for | warning | | — Not discovered | NetBox documents a cable LibreNMS has not seen | notice | | — Not comparable | The far end could not be identified on both sides | — | **The categories are deliberately asymmetric, because the evidence is.** A neighbour LibreNMS has seen is proof a link exists. LibreNMS *not* seeing one proves almost nothing: LLDP and CDP only reveal peers that speak them, so a perfectly correct cable to a server, to a host with discovery disabled, or to a device this LibreNMS does not poll looks exactly like a missing one. That is why "not discovered" is a notice and carries its caveat in the UI, while "not in NetBox" is a warning. Rules that keep the output trustworthy: - **Every finding is anchored to a LibreNMS port with a resolved NetBox interface mapping.** Without that anchor there is no honest way to say "NetBox has no cable here" — only "we do not know which NetBox interface this is", which is a mapping problem the plugin already reports on the mapping pages. Cabled NetBox interfaces with no mapped port are counted and stated (*"2 cabled NetBox interfaces could not be compared…"*), never silently dropped. - **Far ends are compared by device id where possible**, falling back to hostname only when one side is a device LibreNMS does not monitor. Two devices sharing a hostname prefix do not become a match. - **Interface names are compared canonically**, so `Te1/1/2` against `TenGigabitEthernet1/1/2` is agreement, not drift. - **Cables to patch panels and circuit terminations are not compared at all.** No discovery protocol could ever confirm a rear port. - **Inactive link rows are ignored.** LibreNMS keeps them as history; they are not a current observation, so they cannot contradict NetBox. - **Whole pages of devices cost a fixed set of queries**, not a set per device. Set `display.topology_drift` to off to remove the section and the page's findings entirely. --- ## Interface context tab Under the port page's **Plugins** tab. Shows the NetBox interface, its description, type, configured speed, MTU, enabled state, 802.1Q mode, VLANs, addresses, VRF and custom fields. **Connection.** When NetBox traces the path to another device interface, both ends and the cable are named. When it does not — a front port, rear port, patch panel or circuit termination — the tab says what the termination actually is: ```text Connection documented in NetBox Remote termination type: Rear port Remote endpoint: ODF-01 / Port 18 ``` **Circuit.** When the path terminates on a circuit: circuit ID, provider, type, status, tenant, committed rate and description. **Comparison.** Speed, MTU, description and administrative state. A few details that matter in practice: - **Speeds are normalised to bit/s before comparison.** NetBox stores kbit/s; LibreNMS stores `ifSpeed` in bit/s and `ifHighSpeed` in Mbit/s. `ifSpeed` saturates at 4 294 967 295 on anything faster than ~4.29 Gbit/s, so `ifHighSpeed` is preferred and a saturated `ifSpeed` is treated as unknown rather than as a measurement. Without this, every 10G port would report a false mismatch. - **A missing value on either side is `Unknown`, not a mismatch.** NetBox not documenting an MTU is not a discrepancy. - **Administrative state is compared against `ifAdminStatus` only.** An interface that is admin up and oper down is an outage, which LibreNMS already alerts on — not a documentation problem. - **Description comparison is configurable**: trim, collapse repeated whitespace, optional case-insensitivity. Nothing is ever stripped from the middle of a description. --- ## Custom fields NetBox custom fields are fetched dynamically — no field name is hard-coded. Choose which to display in the settings page (comma-separated), or leave the list empty to show everything NetBox returns. Values are formatted for display: | NetBox value | Shown as | |---|---| | string | the string | | number | the number | | boolean | `Yes` / `No` | | list | comma-separated | | object reference | its `display` / `name` | | URL string | a link, **only** for `http`/`https` | | null or empty | the row is omitted entirely | Nothing NetBox returns is ever rendered as HTML. A value containing markup is escaped and shown as text, and a `javascript:` or `data:` "URL" is rendered as text rather than becoming a link. --- ## Cache and staleness behaviour Every panel states how old its data is. Past the configured threshold (60 minutes by default) it says so prominently: ```text ⚠ NetBox data is stale. Last successful synchronisation: 47 minutes ago (2026-08-19 09:04:11) ``` Freshness is measured from the last **successful** sync, not the last attempt. ### Reconciliation is only ever done from a complete run This is the most important behaviour in the plugin. A sync marks objects NetBox did not return as stale **only if it traversed its entire dataset successfully**. If it fails on page 37 of 50: - the run is recorded as `partial`, with `authoritative = false`; - what was fetched is still written to the cache; - **nothing is marked stale**, because the run knows nothing about the pages it never reached. Cached objects are **never deleted** by a sync, only flagged. An object that comes back next run simply clears its flag. Mapping rows — especially manual ones — are never touched by synchronisation at all. Sync outcomes: | Status | Meaning | |---|---| | `success` | Whole dataset traversed; reconciliation performed | | `partial` | Some data arrived, traversal incomplete; no reconciliation | | `failed` | Nothing usable arrived | --- ## Troubleshooting Start here: ```bash ./lnms netbox-context:status # or /plugin/netbox-context/status ./lnms netbox-context:test ``` The status page shows connection details, NetBox version, cache counts, mapping tallies and the last 20 synchronisation runs with their counters and errors. | Symptom | Likely cause and fix | |---|---| | Panel says "not matched to NetBox" | Compare the diagnostics against NetBox. Usually the hostname differs; enable short-hostname fallback or map manually. | | Panel says "ambiguous" | Two NetBox devices share the name. Map manually — the candidates are listed. | | "not documented in NetBox" on a port | The interface really is absent from NetBox, or its name differs beyond normalisation. Check the interface mappings page. | | Everything is stale | Synchronisation is not running. Check the LibreNMS scheduler cron, then run `netbox-context:sync --devices` by hand. | | `403 Forbidden` in the test | The token lacks read access to that object type. See [token permissions](#netbox-api-token-permissions). | | `401 Unauthorized` | Wrong or revoked token. Re-enter it; it is not retried, by design. | | `404` on circuits | The circuits module is not in use. Turn off circuit sync in the settings. | | "stored NetBox token could not be decrypted" in the log | `APP_KEY` changed. Re-enter the token. | | Sync says `partial` repeatedly | NetBox is timing out or rate limiting. Increase the request timeout, reduce the API page size. | | Cross-host redirect refused | NetBox is redirecting to a different hostname. Configure the URL NetBox actually serves on. | Plugin log lines are written to LibreNMS's own log (`logs/librenms.log`) and are prefixed `netbox-context:`. Synchronisation logs one aggregate line per run, not one per object: ```text netbox-context: interfaces sync success duration=18.4s objects=183442 created=0 updated=421 unchanged=182997 stale=24 errors=0 pages=734 ``` --- ## Performance Designed for 5 000+ devices and 250 000+ interfaces. **Rendering.** A device panel is three indexed queries (freshness, mapping, cached device); a port tab is at most six, and only reaches that when the interface is both cabled to a known device and terminated on a circuit. Neither grows with the size of the cache, and neither calls NetBox. The scale suite asserts these bounds directly. **Synchronisation.** Paginated API access, one page normalised and written at a time, bounded memory. Each chunk costs three statements regardless of chunk size: read current hashes, upsert what differs, stamp the rest as seen. Objects whose content hash is unchanged are not rewritten — on a settled network a sync of 183 000 interfaces updates a few hundred rows. **Matching.** Ports are matched against a per-device hash index built from one indexed query, so the cost is linear in ports. No interface is ever compared with another device's interfaces. **Indexes.** Mappings are unique on `device_id` / `port_id` and indexed on the NetBox side; interfaces carry composite indexes on `(netbox_device_id, name_normalized)` and `(netbox_device_id, name_canonical)`, which is exactly what the matcher queries. Tuning knobs: `connection.page_size` (API page size), `sync.chunk_size` (database chunk size), and the per-dataset intervals. --- ## Security **Credentials.** The API token is read from the environment first, otherwise from the plugin's own settings table where it is stored **encrypted** with Laravel's application key — not in LibreNMS's plaintext plugin-settings JSON. It is never rendered, never logged, and never sent to the browser. The settings page shows only `•••••••••••••••• configured`. An empty token field on save means "keep the current token"; removal is a separate, explicit action. **Transport.** TLS verification is on by default; disabling it is an administrator action with the consequence stated next to the checkbox. Only `http` and `https` are accepted, and credentials embedded in the URL are rejected. Connect and request timeouts are enforced. **Redirects.** Never followed automatically. A same-host redirect is re-issued by hand; a redirect to another host is refused outright, so the `Authorization` header is never handed to a host the operator did not configure. **Request targets.** The plugin only ever requests paths from a fixed set of endpoint constants, built against the configured base URL. No browser-supplied value reaches the HTTP client. Object links shown in the UI are constructed from the base URL and a numeric id, not from anything NetBox returned. Note that private (RFC1918) NetBox URLs are explicitly supported — NetBox normally lives on the management network, and an SSRF filter that blocked private ranges would break every real deployment. The control is that only a plugin administrator can set the base URL. **Authorisation.** - All plugin routes require authentication. - Plugin pages require `netbox-context.view`. - Settings, sync and mapping changes require `netbox-context.admin`, which LibreNMS administrators and holders of `plugin.admin` inherit. - The device panel and port tab defer to **LibreNMS's own device and port policies**. A user who cannot see a device in LibreNMS learns nothing about it here. - State-changing requests go through the `web` middleware group and carry CSRF protection. **Output.** Every NetBox-derived value is escaped by Blade. Nothing NetBox returns is rendered as HTML, and only `http`/`https` values become links. **Logging.** Errors are scrubbed by a redaction helper that removes known token values and anything shaped like an `Authorization` header before the message reaches a log, an exception or a page. --- ## Upgrading ```bash cd /opt/librenms ./lnms plugin:add drakelid/librenms-netbox-context ^1.0 # or composer update php artisan migrate --force ``` Schema changes always arrive as **new** migrations; released migrations are never edited. Check `CHANGELOG.md` for breaking changes before a major version. Cached NetBox data and mappings survive upgrades. If a new version adds cached fields, run a full sync afterwards to populate them: ```bash ./lnms netbox-context:sync --full ``` --- ## Uninstalling Disabling or removing the plugin **does not destroy anything**. Cached data and mappings — including every manual mapping — are left in place, so re-enabling restores the previous state. ```bash # 1. Disable (hooks stop rendering, routes return 404) ./lnms plugin:disable netbox-context # 2. Remove the package ./lnms plugin:remove drakelid/librenms-netbox-context # 3. Optional: purge plugin data. This is irreversible and destroys # every manual mapping. php artisan migrate:rollback --path=vendor/drakelid/librenms-netbox-context/database/migrations ``` Step 3 is never performed automatically. If you only want to drop the API token, use **Remove token** on the settings page instead. --- ## LibreNMS daily.sh and upgrade persistence The plugin is a **Composer package**, not files copied into a directory LibreNMS overwrites. Nothing it owns lives inside the LibreNMS source tree. After `daily.sh` or any normal LibreNMS upgrade: | Survives | Why | |---|---| | The package itself | It is a Composer dependency; `daily.sh` runs `composer install`, which reinstalls it. | | Plugin registration | Laravel package auto-discovery reads `extra.laravel.providers` from the package's own `composer.json`. | | Database tables | Plugin-owned migrations; `daily.sh` runs `artisan migrate`, which is additive. | | Configuration | Stored in `netbox_context_settings` (a plugin table) and/or the environment. | | API token | Same, encrypted; or in the environment, which upgrades do not touch. | | Mappings | `netbox_context_device_mappings` / `_interface_mappings` — never truncated by a sync or an upgrade. | | Scheduled sync | Registered by the service provider through Laravel's scheduler, not by a cron file that could be replaced. | No LibreNMS core file is modified, so there is nothing for an upgrade to conflict with. For a path-repository development install, note that `composer install` during `daily.sh` will keep resolving the symlink — keep the checkout in place, or switch to the Packagist version for production. --- ## Using the context from another plugin The correlation this plugin maintains is deliberately reusable. Two published interfaces are bound in the container: ```php use LibreNMS\Plugins\NetBoxContext\Contracts\DeviceContextProviderInterface; use LibreNMS\Plugins\NetBoxContext\Contracts\InterfaceContextProviderInterface; $context = app(DeviceContextProviderInterface::class)->forDevice($device); if ($context->hasNetBoxDevice()) { $site = $context->netboxDevice->site_name; $tenant = $context->netboxDevice->tenant_name; $issues = $context->mismatches(); } $portContext = app(InterfaceContextProviderInterface::class)->forPort($port); $circuit = $portContext->circuit?->cid; ``` Both answer from the local cache and never call NetBox, so they are safe to use in a page render. Implementations must keep that guarantee. --- ## Development ```bash composer install composer test # unit + feature suites composer test:unit composer test:feature composer test:scale # slow; builds large fixtures composer analyse # PHPStan level 5 (larastan) composer lint # Laravel Pint, check only composer fix # Pint, apply ``` The suite runs standalone on Orchestra Testbench, so the matchers, comparison engine, API client and sync services can be exercised without a LibreNMS install. Set `LIBRENMS_ROOT` — or keep a LibreNMS checkout beside this package — and the same tests run against the real LibreNMS models instead of the stubs in `tests/Stubs/librenms.php`. Coverage worth knowing about: - `tests/Unit/DeviceMatcherTest.php`, `InterfaceMatcherTest.php` — the full matching matrix, including ambiguity, locked mappings and "no false match". - `tests/Unit/ComparisonServiceTest.php` — including the 100G `ifSpeed` saturation case and admin-vs-oper state. - `tests/Unit/NormalizerTest.php` — NetBox schema variations, null nested objects, non-interface cable terminations. - `tests/Unit/SecurityTest.php` — token redaction, URL scheme rejection, cross-host redirect detection. - `tests/Feature/DeviceSyncTest.php` — **partial-sync safety**: a run that fails halfway must not declare anything deleted. - `tests/Feature/NetBoxApiClientTest.php` — pagination, retries, 401/403/404/ 429/500, malformed JSON, redirect handling. - `tests/Feature/ContextPanelTest.php` — both panels, including escaping of NetBox-supplied markup and authorisation denial. - `tests/Feature/AdminRoutesTest.php` — authorisation, token-preservation semantics, cross-device mapping rejection. - `tests/Scale/MatchingScaleTest.php` — linear matching and bounded query counts. --- ## Licence GPL-3.0-or-later. See [LICENSE](LICENSE). # netboxcontext