- Python 94.2%
- HTML 5%
- Nix 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
When reconcile marked an object missing it appended a '[tag] last_seen=... present_in_omada=false' note to the object's description, which is varchar(200) on the models it writes. The matcher anchored the note to the START of the string while the note is appended at the END, so an existing note was never found: every reconcile run stacked a fresh ~80-char line, and client IPs already carrying one note grew past 200 on the next marking, killing the whole run with 'value too long for type character varying(200)' (live: run 62). The matcher now finds the note anywhere (the leading newline separator is left outside the match so a replace keeps it), the note is truncated to the column limit as a safety net, and _persist treats a too-long value as a per-object conflict instead of an unhandled run-killer. Marking a device missing no longer writes a description note it cannot store (dcim.Device has no such field). |
||
| docs | ||
| LICENSES | ||
| netbox_sync_omada | ||
| nix | ||
| tests | ||
| .copier-answers.yml | ||
| .editorconfig | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| COMPATIBILITY.md | ||
| flake.lock | ||
| flake.nix | ||
| Makefile | ||
| mise.toml | ||
| pyproject.toml | ||
| pyrightconfig.json | ||
| README.md | ||
| REUSE.toml | ||
| TODO.md | ||
| uv.lock | ||
Netbox sync from Omada Controller
A Netbox plugin to sync devices, interfaces, IP addresses, VLANs, prefixes, wireless LANs and clients from a TP-Link Omada controller.
➡️ Quick Links: Repository · Issues
🌱 Status
Netbox sync from Omada Controller is pretty new and still under development. It is alpha software: try it against a preview and a throwaway NetBox first.
What works today: the Omada Open API client (read-only), the sync engine with an external-identity ledger, all eight feature blocks, reconciliation including grace-gated optional deletion, the full web UI (preview/apply, tabs, settings) and the background jobs with scheduling.
Not there yet: the REST API and an automated test suite — see CHANGELOG.md and COMPATIBILITY.md.
📚 Documentation
- docs/nix.md — installing with Nix / NixOS, the flake, the module and hash updates.
- COMPATIBILITY.md — supported NetBox and Python versions.
- CHANGELOG.md — what changed and when.
NetBox support: 4.6.0 – 4.7.99 (the plugin deliberately avoids 4.7-only APIs and symbol changes). Python support: 3.12 – 3.14.
🗃️ Installation
The plugin must be installed into the same Python environment as NetBox, not into a separate virtualenv, and NetBox's version must match the supported range above.
From PyPI
source /opt/netbox/venv/bin/activate
pip install netbox-sync-omada
Or, as a dependency of a uv-managed NetBox deployment:
uv add netbox-sync-omada
Enable the plugin
In your NetBox configuration.py:
PLUGINS = ["netbox_sync_omada"]
PLUGINS_CONFIG = {
"netbox_sync_omada": {
# Optional — every setting has a default, see "Settings" below.
# Dry-run by default: preview collects and compares but never writes.
"default_sync_mode": "preview",
"default_sync_tag": "synced-omada",
"default_sync_interval_minutes": None,
"mark_missing_devices": "offline",
"allow_delete_missing": False,
"delete_grace_seconds": 2_592_000,
"history_keep_days": 90,
"history_keep_count": 1000,
},
}
Then migrate, collect static files if your deployment requires it, and restart both the NetBox
web process and the rqworker process:
python manage.py migrate
python manage.py collectstatic --no-input
systemctl restart netbox netbox-rq
With Nix / NixOS
This repository is a flake exposing the package, a NixOS module and a development shell:
inputs.netbox-sync-omada.url = "git+https://git.h.oluflorenzen.de/finkregh/netbox-sync-omada.git";
# in a NixOS configuration
services.netboxSyncOmada = {
enable = true;
settings = { default_sync_tag = "synced-omada"; };
};
See docs/nix.md for the module options, the local-checkout variant and how to fill in the packaging hash at release time.
🚀 Usage
1. Provide the Open API credentials
The database never stores a secret. An endpoint stores only the name of an environment
variable (kind env, the default) or the path of a file (kind file, optionally written as
file:/run/secrets/omada-client-secret) that holds the value. The value is resolved on demand, used
for one sync run, and never persisted, logged or exposed through the UI.
Create the credentials in the Omada controller (Settings → Controller → Open API) first, then make them available to NetBox:
# Environment variables for both the web process and the worker.
export OMADA_CLIENT_ID="<open-api-client-id>"
export OMADA_CLIENT_SECRET="<open-api-client-secret>"
For a systemd deployment put them in an EnvironmentFile= (mode 0600) on both netbox.service
and netbox-rq.service; a file reference works the same way but is easier to rotate. The web process
needs the credentials too, because the Detect, prefill and preview actions run there.
2. Create the endpoint
Omada Sync → Endpoints → Add:
| Field | Notes |
|---|---|
| Base URL | Controller root, e.g. https://omada.example.internal:8043, or your regional Omada Pro cloud controller. |
| Controller id | omadacId — not a secret. Use Detect to read it from GET /api/info, or type it in. |
| Credential refs | The env-var names or file: paths from step 1, plus the matching reference kind. |
| Verify SSL / CA file | Self-hosted controllers usually ship a self-signed certificate; either upload a CA bundle or turn verification off deliberately. |
| Sync tag | Ownership boundary. Objects carrying this tag belong to the endpoint; objects without it are never modified or removed. |
| Site / VLAN group | Defaults for objects that are not bound to a site mapping. The plugin never creates sites or VLAN groups. |
| Feature toggles | Devices, interfaces/switch ports, management IPs, VLANs, prefixes, wireless LANs, cables; clients are off by default (they churn). |
3. Preview and apply
Preview enqueues a read-only preview run in the background (the controller is polled once, on an RQ worker, never on the web request thread — so a large or slow controller cannot stall a web worker) and shows a page that refreshes until the run is ready, with the job's live log inline and an “Open live log” link. The review then lists every proposed create/update/mark-missing/delete per item; from there you can Apply the whole run or select individual items and apply only those. The approved observation is signed and carried into the background job, so an apply writes exactly what you approved — it never re-polls upstream.
4. Schedule it
Set Sync interval (min) on the endpoint and make sure the NetBox worker is running
(python manage.py rqworker). An hourly system job enqueues one sync per due endpoint; an endpoint
can never run twice concurrently, and repeated failures back off instead of hammering the controller.
Leaving the interval empty means "manual only".
To schedule every new endpoint instead of opting each one in by hand, set
default_sync_interval_minutes in PLUGINS_CONFIG (with Nix: delegatedSync.settings). New
endpoints then inherit that interval; endpoints that already exist keep their own value, so change
those in the UI. The same applies to the other plugin-wide defaults — they seed an endpoint when it
is created. Scheduled and API-triggered runs have no user behind them, so their changelog entries
are attributed to a disabled netbox-sync-omada service account rather than to nobody.
The default mode is preview (dry-run). A scheduled or API run only collects and compares
unless mode=apply is requested or an operator flips default_sync_mode to apply in the
settings singleton. This is what lets a fresh deployment sync its first data without writing
a single object until the output has been reviewed.
The endpoint list shows a computed Sync state badge (Running / Queued / Retry due / Retry / Due / Waiting / Disabled) and a Schedule badge, both sortable, plus a Last test column. Each row has Sync now and Test connection buttons, and the list offers bulk Sync Selected, Clear Stuck Syncs and Recalculate Schedule actions that honour the active filters. The endpoint's Status tab repeats the actions next to the scheduler state and the last connection test. An endpoint also has a Devices tab (the devices it owns, from the identity ledger), and every device page has an Omada Sync tab showing which endpoint owns it and how many tagged interfaces, IPs and VLANs that endpoint manages on it. The endpoint edit form has an “Existing devices” set for adopting devices NetBox already has (created by hand or an earlier tool): pick the device and give the Omada serial and/or MAC of the controller device it stands for, and the sync updates it instead of creating a duplicate (serial first, then MAC). A binding whose Omada identity never appears in a run is reported as a conflict.
5. Missing objects and deletion
Reconciliation is always tag-scoped. Missing devices/interfaces/IP addresses are marked
(offline / disable / deprecated) and get an explanatory note that is removed again when the
object reappears.
Deletion is off by default and needs two independent switches — the plugin-wide
allow_delete_missing and the endpoint's own flag — plus a grace period
(delete_grace_seconds, default 30 days). Only IP addresses and cables are ever eligible, only when
the plugin created them, and devices, interfaces, VLANs, prefixes and wireless LANs can never be
deleted by any configuration. Every run's created objects can be reverted from the Sync runs page.
6. Collection options
Every endpoint has a Collection behaviour card with the knobs that change what the sync collects, as opposed to how it reconciles it. All of them are also writable over the REST API.
| Option | Default | Effect |
|---|---|---|
ignored_ssids |
empty | SSIDs whose wireless network and clients are never written. An entry may be an ssidId or an SSID name. Empty means "sync everything". |
infer_interface_types |
on | Derive the interface type from the port's static type enum: copper ports become 1000base-t, pluggable cages 1000base-x-sfp. Turn it off to leave every port as other. The negotiated link speed is deliberately never used — a down port reports 10 Mbps regardless and an empty cage reports no useful speed. |
annotate_unknown_mappings |
on | Append a note to the description of an interface the sync could not map (for example a port whose VLAN the controller never reported); description is the only free-text field dcim.Interface has. Off means the skip is only reported in the sync run. |
metadata_write_mode |
only when changed | Whether the omada_metadata custom field is rewritten on every sync. if_changed suppresses the write entirely when the stored value is already equal, which is what keeps a no-op sync from producing a page of changelog entries — the engine cannot suppress them by narrowing update_fields, only by not saving at all. |
management_subnets |
empty | network -> mask length, e.g. {"192.168.0.0/16": 24}. Omada reports management addresses and client leases as bare hosts, so the mask has to be derived; a configured subnet beats a prefix found in NetBox and is the escape hatch when an aggregate (or a sibling sync) would otherwise win. The entry's own mask is ignored — each /24 subnet inside the mapped network is matched. |
create_dhcp_ipranges |
on | Create an ipam.IPRange from the DHCP pool bounds the controller reports. |
create_unknown_clients |
off | Create a device for a client MAC that no device matched. |
sync_client_attributes |
off | Write the extra client attributes (OS name, device category, vendor) onto the object. |
client_hostname_as_device_name |
on | Name a device created from a client sighting after the client's hostname when it is RFC-1123-safe, otherwise fall back to the MAC. |
The last three only apply while sync_clients is enabled, and create_dhcp_ipranges /
management_subnets only while sync_prefixes / sync_management_ips are. create_unknown_clients,
annotate_unknown_mappings, infer_interface_types and metadata_write_mode are also on the
endpoints' bulk-edit form.
7. Settings
Omada Sync → Settings holds the plugin-wide defaults (sync mode, tag, per-model missing policy,
deletion gates and grace, history retention). Endpoint values win over the defaults, and
PLUGINS_CONFIG seeds the singleton only on its first access — once the row exists, edit it in the
UI. default_sync_mode (preview/apply) is the mode the scheduler and the REST sync action use
when a request does not name one; it defaults to preview, so nothing writes until it is explicitly
set to apply. The plugin uses the normal netbox_sync_omada logger, so its verbosity is
controlled by NetBox's LOGGING.
The Scheduler safety card holds three operational bounds: sync_job_timeout_seconds caps one
collection phase, sync_stale_job_marker_minutes is how long a claimed sync slot may look in flight
before a dead worker's claim is freed, and sync_missed_schedule_grace_minutes re-anchors a schedule
that is overdue by more than that after downtime instead of queueing a catch-up run for every
endpoint at once.
8. REST API
The models are also available over NetBox's REST API under /api/plugins/sync-omada/, using your
normal NetBox API tokens. The resources are:
| Path | Methods | Notes |
|---|---|---|
endpoints/ |
GET, POST, PUT, PATCH, DELETE | full CRUD |
endpoints/{id}/sync/ |
POST | queue a run; 202 with a job_id |
site-mappings/ |
GET, POST, PUT, PATCH, DELETE | full CRUD |
runs/ |
GET | sync history, read-only |
runs/{id}/revert/ |
POST | queue a revert of that run's created objects |
external-objects/ |
GET | the upstream-identity ledger, read-only |
settings/ |
GET, PUT, PATCH | the singleton policy row; no create/delete |
client_id_ref and client_secret_ref are references, never values: they name an environment
variable or a file: path (see Provide the Open API credentials above). A value that looks like a
resolved secret is rejected with 400 rather than stored, so a client secret can never be written
through the API.
Triggering a sync is a POST to the sync action. It returns 409 when the endpoint is disabled or
when a run already holds the endpoint's slot, so a client cannot queue two runs for one endpoint;
the slot the UI uses and the slot the API uses are the same one.
curl -sS -X POST \
-H "Authorization: Token $NETBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "apply"}' \
https://netbox.example.com/api/plugins/sync-omada/endpoints/1/sync/
# {"job_id": 42, "status": "queued"}
mode is apply (default) or preview; a preview run computes the plan without writing anything.
The action requires the change_omadaendpoint permission, and the revert action requires
delete_syncrun — the same permissions the corresponding UI buttons need.
🧑💻 Development
Preparation
We're using uv to manage this project and its dependencies.
After cloning the repository using Git, a simple uv sync should get everything you need.
git clone https://git.h.oluflorenzen.de/finkregh/netbox-sync-omada.git netbox-sync-omada
cd netbox-sync-omada
uv sync
Nix
A development shell with Python, uv, Ruff and the Nix formatter is available:
nix develop
# or run a single command inside it
nix develop -c statix check .
nix build .#default
nix flake check --all-systems
nix fmt
direnv
We recommend using direnv to add installed dependencies to your $PATH, so that you don't need to prepend uv run to every command.
For example, after installing direnv, you can use direnv edit in this repository's directory and add the following line:
PATH_add .venv/bin
The rest of this readme assumes that you have .venv/bin in your $PATH.
EditorConfig
Make sure your editor or IDE supports the EditorConfig standard, so that your code adheres to the project's preferred indentation, line lengths, etc.
Makefile
There is a Makefile that contains frequently used commands to speed up development, but it's optional to use.
The following targets exist:
fmt: Use Ruff to format and lint the code.qa: Use mypy for static type analysis.reuse: Usereuse lintto make sure every file contains licensing information.test: Use pytest for automated testing and code coverage.noqa: Addnoqastatements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.
make all, or simply make, will run fmt, qa, reuse, and test.
You should do this before every commit and fix all issues that are reported.
make test currently exits with "no tests ran": the automated test suite is still to be written.
Until then, the way to exercise the plugin is a local NetBox checkout with this repository installed
editable, PLUGINS = ["netbox_sync_omada"] in its configuration, and a preview against a real
controller.
📃 License
This project is licensed under the terms of the MIT License.
The project also conforms to the REUSE Specification, version 3.3.
You can use the reuse tool to interpret the machine-readable licensing information.