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.
Metadata
Release files for nautobot-ssot-hudu 2026.5.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| nautobot_ssot_hudu-2026.5.4.tar.gz | 272.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nautobot_ssot_hudu-2026.5.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 299.7 kB
Release files / nautobot_ssot_hudu-2026.5.4.tar.gz
| Download URL | nautobot_ssot_hudu-2026.5.4.tar.gz |
|---|---|
| Size | 272.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c6934cff5939d1b384e4c0596c323d34257e34afc899432a202b9e294c66bdbf
|
|
BLAKE2b-256 checksum How to use checksums |
3976405ec2be693e6e7db5067283e7a7c80b7e5a2b57f0a01a0979346638367a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / nautobot_ssot_hudu-2026.5.4-py3-none-any.whl
| Download URL | nautobot_ssot_hudu-2026.5.4-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9604a09209f476e9253ef6b7d39c6d81b812b5489b13e6363de4ac1b574bfdb9
|
|
BLAKE2b-256 checksum How to use checksums |
eb3910bffe2ba5d8bb2ab973e36c292307ff7b342c20ae9b4b8ba64bd86d91a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Security for nautobot-ssot-hudu 2026.5.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.