Nautobot SSoT integration for Hudu (one-way: Nautobot -> Hudu).
Project description
nautobot-ssot-hudu
A Nautobot SSoT integration that pushes data from Nautobot into Hudu.
Direction: Nautobot is the source of truth. Hudu is a Data Target — Nautobot writes Companies, Devices, and related records into Hudu.
Status: Beta. Seven entity types syncing end-to-end with full idempotency, 94 unit tests, validated against a live Hudu self-hosted instance. Not yet on PyPI.
Compatibility: Nautobot 3.0+, Python 3.10+, nautobot-ssot 4.2+, hudu-magic 0.4+.
Architecture
Built on the DiffSync library bundled with nautobot-ssot. The hudu-magic client library handles all Hudu API I/O.
Nautobot ORM ──> Nautobot DiffSync adapter ─┐
├──> diff ──> Hudu DiffSync adapter ──> hudu-magic ──> Hudu API
Hudu API ─┘ (read current state)
Mapping
| Nautobot | Hudu | Status |
|---|---|---|
tenancy.Tenant |
Company | ✅ name, description (notes) |
dcim.Device |
Asset (per-role layouts + configurable custom field map) | ✅ name + custom-field map + role-based layout selection |
ipam.Prefix |
Network | ✅ address (CIDR), name, description |
ipam.IPAddress |
IPAddress | ✅ address (host), dns_name, description |
ipam.VLAN |
VLAN | ✅ vid (1-4094), name, description |
dcim.Rack |
RackStorage | ✅ name, height (U), width (in), serial, asset_tag, description, desc_units |
dcim.Device rack/position/face |
RackStorageItem | ✅ asset placement: rack_name, start_unit, end_unit, side |
dcim.Location |
(no API equivalent) | ⚠️ Hudu does not expose Locations as a CRUD entity. Per-device location is captured via device_field_map["Location"] = "location.name". |
Cross-entity linkages (set automatically when both sides are synced):
- IPAddress → Asset (Hudu IP page shows the device it's assigned to, via Nautobot
IPAddress.interface_assignments → device) - Network → VLAN (Hudu Network page shows its VLAN, via Nautobot
Prefix.vlan)
Identity model:
| Entity | Identifier | Notes |
|---|---|---|
| Company | (name,) |
Both sides enforce uniqueness on name |
| Device | (company_name, name) |
Hudu Asset names unique only within a company |
| Network | (company_name, address) |
Networks are scoped per-company |
| IPAddress | (company_name, address) |
address is the host, no mask |
| VLAN | (company_name, vid) |
802.1Q tag, two companies can each own VLAN 100 |
| Rack | (company_name, name) |
Same scoping as Device |
| RackItem | (company_name, asset_name) |
One rack item per asset max |
We deliberately use human-readable identifiers (names/vids) across the diff boundary, not Hudu primary keys. If a Hudu record is deleted and recreated, its pk changes but the identifier stays the same — the next sync rebinds by name rather than diffing as "needs update."
Empty-string normalization: Both adapters coerce empty string "" to None when loading. Hudu stores unset fields as null; Nautobot CharField defaults are "". Without coercion every sync would emit spurious updates for blank fields.
Device custom field mapping
Operators choose which Nautobot Device attributes populate which Hudu custom-layout fields via PLUGINS_CONFIG:
PLUGINS_CONFIG = {
"nautobot_ssot_hudu": {
"instance_url": "https://acme.huducloud.com",
"secret_group_name": "Hudu Credentials",
"asset_layouts": {
# Default Hudu asset_layout_id for Devices whose role isn't
# explicitly mapped below. Unset/None → those devices skip.
"device": 7,
# Optional per-Nautobot-Role overrides. Keys are role names;
# values are Hudu asset_layout_ids. Useful for MSPs documenting
# heterogeneous fleets across multiple Hudu layouts.
"device_by_role": {
"router": 8,
"switch": 9,
"firewall": 10,
},
},
# Hudu field label -> Nautobot Device attribute path (dotted)
"device_field_map": {
"Hostname": "name",
"Management IP": "primary_ip4.host",
"Model": "device_type.model",
"Serial": "serial",
"Status": "status.name",
"Location": "location.name",
},
}
}
The Hudu asset_layout must already have custom fields with matching labels. Field-resolution uses safe None-propagation: a Device without a primary_ip4 yields None for "Management IP" rather than raising AttributeError.
Limitations
- Locations don't sync as a separate entity. Hudu's REST API doesn't expose Locations as a manageable resource (verified empirically —
/api/v1/locationsreturns 404, no Locations admin page). Per-device location is captured via thedevice_field_map["Location"] = "location.name"entry, which appears as a custom-field string on each synced Asset. - Layout migration isn't supported. Hudu's API can't move an existing Asset between asset_layouts. If a Nautobot Device's role is reassigned and the new role maps to a different Hudu layout, the diff surfaces it but the writeback logs a warning and skips. Operator must delete + recreate manually.
hudu-magiclibrary quirks are documented indevelopment/hudu/HUDU_API_QUIRKS.md— several endpoints reject the lib's auto-paginated?page=1parameter, and several update operations are missing from the lib's resource registry. The plugin works around all of them.
Hudu prep
Before the first sync, the operator must:
- Create the asset_layout(s) that Devices will live in. Hudu doesn't ship a built-in "Network Device" layout — each operator defines their own. Each layout's custom fields must have labels matching the keys in
device_field_map(e.g. "Hostname", "Management IP", "Model", "Serial", "Status", "Location"). - Generate an API key in Hudu (Admin → API Keys → New API Key). Scope: Full access. The "Delete data" permission is required if you plan to use
hard_delete=True; "View passwords" and "Export data" can stay off — the plugin doesn't read passwords or export bulk data. - Note the layout IDs — visible in Hudu's Admin → Asset Layouts list. You'll wire them into
PLUGINS_CONFIG.asset_layouts.device(and optionallydevice_by_role).
Run-time options
Exposed as Job parameters in the Nautobot UI:
dryrun(framework-provided) — calculate the diff but don't write. Default:True. This is the canonical control; we don't redeclare it.hard_delete— when an entity exists in Hudu but no longer in Nautobot, archive it (default, recoverable via Hudu UI) or hard-delete (irreversible).
Development
For static checks:
uv sync --extra dev
uv run pytest
uv run ruff check
For end-to-end testing against a real Nautobot instance, see development/README.md — a self-contained 4-container stack (postgres + redis + nautobot-web + nautobot-worker) with the plugin source bind-mounted for hot-reload and a seed script that creates synthetic Tenants + a Hudu SecretsGroup.
cd development/
cp .env.example .env && $EDITOR .env
make build && make up
make seed
License
Private / proprietary. Not yet published.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nautobot_ssot_hudu-2026.5.4.tar.gz.
File metadata
- Download URL: nautobot_ssot_hudu-2026.5.4.tar.gz
- Upload date:
- Size: 272.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6934cff5939d1b384e4c0596c323d34257e34afc899432a202b9e294c66bdbf
|
|
| MD5 |
658a7b32be61e8cb54dfe8654152c874
|
|
| BLAKE2b-256 |
3976405ec2be693e6e7db5067283e7a7c80b7e5a2b57f0a01a0979346638367a
|
File details
Details for the file nautobot_ssot_hudu-2026.5.4-py3-none-any.whl.
File metadata
- Download URL: nautobot_ssot_hudu-2026.5.4-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9604a09209f476e9253ef6b7d39c6d81b812b5489b13e6363de4ac1b574bfdb9
|
|
| MD5 |
abd37861f622038707d88d3f8c0e1b6a
|
|
| BLAKE2b-256 |
eb3910bffe2ba5d8bb2ab973e36c292307ff7b342c20ae9b4b8ba64bd86d91a3
|