Metadata-Version: 2.5
Name: netbox-kea-ng
Version: 1.14.0
Summary: NetBox plugin for the Kea DHCP server (fork of netbox-kea)
Project-URL: Homepage, https://github.com/marcinpsk/netbox-kea
Project-URL: Repository, https://github.com/marcinpsk/netbox-kea
Project-URL: Issues, https://github.com/marcinpsk/netbox-kea/issues
Project-URL: Changelog, https://github.com/marcinpsk/netbox-kea/blob/main/CHANGELOG.md
Author-email: Marcin Zieba <marcinpsk@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: LICENSES/Apache-2.0.txt
Keywords: dhcp,kea,netbox,networking,plugin
Classifier: Environment :: Plugins
Classifier: Framework :: Django
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Requires-Dist: beautifulsoup4<5,>=4.12
Requires-Dist: netaddr<2.0.0,>=0.8
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: pyyaml<7.0,>=6.0
Requires-Dist: requests<3.0.0,>=2.27.0
Description-Content-Type: text/markdown

<!--
SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
SPDX-FileCopyrightText: 2023 Devon Mar <devon-mar@users.noreply.github.com>
SPDX-License-Identifier: Apache-2.0
-->

# netbox-kea-ng

[![PyPI](https://img.shields.io/pypi/v/netbox-kea-ng)](https://pypi.org/project/netbox-kea-ng/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/netbox-kea-ng)](https://pypi.org/project/netbox-kea-ng/)
[![CI](https://img.shields.io/github/actions/workflow/status/marcinpsk/netbox-kea/ci.yml?branch=main&label=tests)](https://github.com/marcinpsk/netbox-kea/actions/workflows/ci.yml)
[![Coverage](https://codecov.io/gh/marcinpsk/netbox-kea/branch/main/graph/badge.svg)](https://codecov.io/gh/marcinpsk/netbox-kea)
[![CodeQL](https://github.com/marcinpsk/netbox-kea/actions/workflows/codeql.yml/badge.svg)](https://github.com/marcinpsk/netbox-kea/actions/workflows/codeql.yml)
[![REUSE](https://api.reuse.software/badge/github.com/marcinpsk/netbox-kea)](https://api.reuse.software/info/github.com/marcinpsk/netbox-kea)
[![License](https://img.shields.io/github/license/marcinpsk/netbox-kea)](LICENSE)
[![Python](https://img.shields.io/pypi/pyversions/netbox-kea-ng)](https://pypi.org/project/netbox-kea-ng/)
[![NetBox](https://img.shields.io/badge/NetBox-%E2%89%A54.3.0-blue)](https://github.com/netbox-community/netbox)

> **Fork notice:** This is `netbox-kea-ng`, an independently maintained fork of
> [netbox-kea](https://github.com/devon-mar/netbox-kea) by
> [Devon Mar](https://github.com/devon-mar).
> It is published to PyPI as **`netbox-kea-ng`** and tracked in this repository.
> Upstream changes are periodically merged where applicable.

NetBox plugin for the [Kea DHCP](https://www.isc.org/kea/) server. Manage your DHCP infrastructure directly from NetBox — view daemon status, search and manage leases, manage host reservations, configure subnets/pools/options, and keep your NetBox IPAM synchronized with live Kea data via a background job.

## Features

### Core (from upstream)

- View Kea daemon status (DHCPv4/DHCPv6 daemons — and the Control Agent on Kea < 3.0)
- Full DHCPv4 and DHCPv6 support
- Search, view, delete and export DHCP leases
- Search for NetBox devices/VMs directly from DHCP leases
- View DHCP subnets from Kea configuration
- REST API and GraphQL support for Server objects

### Additions in this fork

**Host Reservations**
- Full CRUD for DHCPv4 and DHCPv6 reservations via [`host_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-host-cmds) and [`subnet_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-subnet-cmds) hooks
- Identifier types: hw-address (v4), DUID (v6), client-id, flex-id, circuit-id, remote-id
- Reservations that reserve no address — an identifier-only host (hostname, options or
  client classes only) and a DHCPv6 host that only delegates prefixes. Both are listed,
  created, edited and deleted by client identifier instead of by IP, and the IPAM sync
  reports them as *skipped*: there is no address to record in NetBox. Their **Lease**
  column matches the client identifier against the leases of the reservation's own
  subnet, there being no reserved address to match on
- Per-reservation DHCP options
- Journal entries on add/edit/delete
- Validated YAML or JSON bulk transfer. Export a Complete Snapshot from one server
  or the combined view, then import it from the matching DHCPv4 or DHCPv6 Reservations page

**Subnet Management**
- Add, edit and delete subnets (requires [`subnet_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-subnet-cmds) or `config-set`)
- Pool management (add/delete pools per subnet)
- Shared network management (add/edit/delete)
- Per-subnet and global DHCP option editing

**IPAM Sync**
- Sync active leases → NetBox `IPAddress` (status `active`)
- Sync reservations → NetBox `IPAddress` (status `reserved`)
- Sync button on individual leases and reservations
- Bulk sync for entire lease tables
- Pending-change detection: badge on leases where a reservation exists at a different IP
- MAC address sync → NetBox `MACAddress`
- Sets `dns_name` on IPAddress for automatic DNS sync via [netbox-dns](https://github.com/peteeckel/netbox-plugin-dns) IPAMDNSsync

**Periodic Background Sync** *(requires `rqworker`)*
- Automatic Kea→NetBox IPAM sync on a configurable interval (default 5 minutes)
- Syncs all leases and reservations from all configured servers
- Visible in NetBox **System → Background Jobs**

**DHCP Control**
- Enable/disable DHCPv4 and DHCPv6 daemons from the NetBox UI

**Server connection validation**
- Creating a Server or changing connection values checks every enabled DHCP service.
- Connection values include URLs, credentials, TLS settings, Control Agent routing and enabled DHCP families.
- Metadata changes, such as names, tags and sync settings, work while Kea is unavailable. Submitting unchanged connection values sends no connectivity request.
- Bulk edits reject the entire batch if one changed connection fails its check.

**Dual-URL Server**
- Optional separate URLs for the DHCPv4 and DHCPv6 endpoints
- Supports Kea 3.0+ (each daemon exposes its own HTTP control socket) and split v4/v6 deployments

**Global / Cross-Server Views**
- Combined dashboard, lease, reservation, subnet and shared-network views across all servers

**Lease Add / Edit / Bulk Import**
- Add and edit individual leases
- Bulk import leases from CSV

---

## Requirements

- NetBox 4.3 – 4.7
- Kea 3.0+ (recommended) — the plugin connects directly to each daemon's built-in HTTP control socket (`kea-dhcp4` / `kea-dhcp6`). The [Kea Control Agent](https://kea.readthedocs.io/en/latest/arm/agent.html) was deprecated in Kea 2.7 and removed in 3.0; on Kea < 3.0, point the server URL at the Control Agent instead.
- [`lease_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-lease-cmds) hook library (for lease search and management)
- [`stat_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-stat-cmds) hook library (for guarded Subnet lease searches unless `lease_query_max_unpaged_leases` is `0`)
- Kea 3.1.5+ for state-filtered Subnet lease searches. Earlier releases do not provide the scoped state commands, so these searches fail closed.
- [`host_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-host-cmds) hook library (optional, for reservation management — also requires `subnet_cmds` to resolve a reservation's subnet from its CIDR)
- [`subnet_cmds`](https://kea.readthedocs.io/en/latest/arm/hooks.html#hooks-subnet-cmds) hook library (optional, for subnet add/edit/delete, reservation management, and the subnet suggestions on the lease search and reservation forms)

The plugin degrades gracefully when optional hooks are absent — tabs for unavailable features are hidden automatically. Two pages offer the server's configured subnets as suggestions and read them through `subnet_cmds`; without that hook each says so in a banner rather than silently offering nothing:

- **Lease search** keeps working: the Search field offers no subnet suggestions, so type an exact configured subnet CIDR or ID.
- **Add reservation** cannot save, because resolving the entered CIDR to a Kea subnet ID needs `subnet_cmds`. Load the hook first.

---

## Compatibility

| netbox-kea-ng | NetBox | Kea |
|---|---|---|
| 1.x | 4.3 – 4.7 | 3.0+ recommended (2.4+ via Control Agent) |

On Kea 3.0+ the plugin talks directly to each DHCP daemon's HTTP control socket; on Kea < 3.0 it connects through the (now-deprecated) Control Agent. CI tests against **Kea 3.2.0** using the `memfile` lease database.

### netbox-branching

In a [netbox-branching](https://github.com/netboxlabs/netbox-branching) branch, live Kea and import operations are read-only. CI tests
NetBox 4.7 with netbox-branching 1.2.1. List `netbox_branching` last in `PLUGINS`.

- Kea Servers, sync settings and IPAM ownership links stay in main. A branch has
  no copy of them, so it shows main's values.
- DHCP Import Mappings follow imported Subnets and Global Reservations in a fresh branch.
  Deleting either target removes its branch mapping while main retains both. Discard preserves main.
  Squash merge deletes the matching main pair; revert restores both original identities, so reimport
  updates the restored object. This applies to IPv4, IPv6, instance deletion and queryset deletion.
  Mapping recovery adds no manual editor or branch-local import.
- Kea data is live, in main and in every branch.
- In a branch, the plugin refuses every change that comes through its web pages or its REST API,
  before it sends anything to Kea or writes to the database. A page shows HTTP 409 with a link to
  main; the REST API answers 409 with the code `branch_write_refused`. Plugin pages show a banner,
  and plugin and GraphQL responses carry an `X-NetBox-Kea-Sources` header.
- A change request that still selects a branch that you can no longer use changes nothing. A
  `_branch` query that names a merged, archived, or not ready branch, and an `active_branch` cookie
  that names such a branch or a deleted one, get HTTP 409 with the code `branch_selection_unusable`.
  netbox-branching answers HTTP 400 first when a `_branch` query names a deleted branch, and when
  an API `X-NetBox-Branch` header names a deleted or unusable branch. Select main (`?_branch=`)
  and try again.
- Code that runs outside a web request (a custom script, for example) is also refused in a branch.
  A save or a delete of a Kea Server, sync settings or an IPAM ownership link raises
  `BranchActive`, and so does a Kea command that changes Kea, from a client that
  `Server.get_client()` returns. The periodic IPAM sync job fails when it runs in a branch.
- A merge fails, and changes nothing, when the branch deletes a VRF that a Kea server in main now
  syncs into. Clear or change that server's Sync VRF, then merge again.
- In a branch, a delete of an IP address, Prefix or IP Range that a Kea server owns is refused, and
  so is a delete of a device or virtual machine that holds such an IP address, because the ownership
  data exists in main only. NetBox shows the refusal as an error message; the REST API answers 400.

**DHCP mapping recovery.** Select the squash strategy for affected branches. Recovery refuses the
whole action if main has conflicting changes, history is incomplete, a source Server is missing,
an identity has been reused, or restoration would violate a source or target constraint. A busy
transaction refuses with a retry message. Resolve the reported conflict before retrying; newer main
state is preserved. Older branches keep independent read pages, but their DHCP Plugin tab requires
a fresh branch. A mapping, target or required Tag branching exemption also requires a fresh branch after
the exemption is removed. Existing branch schemas and missing history are not retrofitted.
Apply Tag changes separately on main or in a Tag-only branch, then create a fresh branch for DHCP
changes. Recovery refuses mixed Tag changes and mapped DHCP target replay, including unrelated Tag
changes in the same branch. A required missing, renamed or replaced Tag also causes a refusal.

Mappings lost under the previous policy require explicit operator repair. Preserve the existing
target, verify its Server, family and source identity from trusted records, and repair the association
on main through an audited operator procedure. A matching target name does not prove that association.
See [the recovery contract](docs/design/dhcp-import-mapping-branching.md).

**Upgrade with open branches.** Earlier releases let netbox-branching copy the Kea servers table
into each new branch. Nothing removes that copy: branch sync no longer updates it, and branch
migrate does not drop it. NetBox still reads Kea servers from main, but a VRF delete in the branch
checks the old copy, not main. A merge also applies to main every Kea server change that the branch
recorded before the upgrade. Before you upgrade, merge or delete every branch that is not yet
merged, and create new branches after the upgrade. If a branch changed a Kea server password, delete
it and make the change again in main: the change log holds a placeholder, not the password, so a
merge can write the placeholder to main. A merged branch keeps its schema until you
archive it; archive it when you no longer need to revert it.

---

## Installation

### 1. Install the package

Add `netbox-kea-ng` to your `local_requirements.txt` (or install with pip):

```bash
pip install netbox-kea-ng
```

### 2. Enable the plugin

In `configuration.py`:

```python
PLUGINS = ["netbox_kea"]
```

Optionally configure plugin settings (see [Configuration](#configuration)):

```python
PLUGINS_CONFIG = {
    "netbox_kea": {
        "kea_timeout": 30,
        "lease_query_max_unpaged_leases": 1000,
        "sync_interval_minutes": 5,
        "sync_leases_enabled": True,
        "sync_reservations_enabled": True,
        "sync_prefixes_enabled": True,
        "sync_ip_ranges_enabled": True,
        "sync_max_leases_per_server": 50000,
        "stale_ip_cleanup": "remove",
    }
}
```

### 3. Run migrations

```bash
./manage.py migrate
```

### 4. Start the background worker (required for periodic sync)

The periodic IPAM sync job runs via NetBox's built-in `rqworker`. If you're not already running it:

```bash
./manage.py rqworker
```

The `Kea IPAM Sync` job will appear under **System → Background Jobs** and runs on the configured interval.

---

## Configuration

All settings are under `PLUGINS_CONFIG["netbox_kea"]`:

| Setting | Default | Description |
|---|---|---|
| `kea_timeout` | `30` | HTTP request timeout in seconds for Kea API calls |
| `lease_query_max_unpaged_leases` | `1000` | Reject an unpaged Subnet lease query when its Kea statistics count exceeds this limit. Set to `0` to disable this safety check |
| `stale_ip_cleanup` | `"remove"` | What to do with stale IPs after sync: `"remove"` (delete), `"deprecate"` (set status=deprecated), `"none"` (skip) |
| `sync_interval_minutes` | `5` | Initial interval of the background sync job (minutes). Edit it later on the **Sync Jobs** page |
| `sync_enabled` | `True` | Initial state of the global sync switch. Edit it later on the **Sync Jobs** page |
| `sync_leases_enabled` | `True` | Sync active DHCP leases to NetBox IPAM |
| `sync_reservations_enabled` | `True` | Sync Kea reservations to NetBox IPAM |
| `sync_prefixes_enabled` | `True` | Sync Kea subnets to NetBox IPAM as IP Prefixes |
| `sync_ip_ranges_enabled` | `True` | Sync Kea pools to NetBox IPAM as IP Ranges |
| `sync_max_leases_per_server` | `50000` | Hard cap on leases fetched per server per sync run. Set to `0` for no limit |

NetBox refuses to start when one of these settings has a value of the wrong type or outside its range.
The error names the setting, the allowed values, and the given value. A number must be an integer, not a string
or a boolean. `kea_timeout` must be at least 1, `sync_interval_minutes` must be from 1 to 1440, and the other
numbers must be at least 0.

`./manage.py migrate` reads `sync_interval_minutes`, `sync_enabled` and the four `sync_*_enabled`
toggles once, when it creates the Sync Configuration. After that, the **Sync Jobs** page holds these values,
and a later change to these settings in `PLUGINS_CONFIG` has no effect.

Subnet lease searches use `stat-lease4-get` or `stat-lease6-get` before an
unpaged lease command. Kea statistics can reject a query that is already too
large. They cannot prove that an unqualified query is below the limit because
stored expired states are not included in all statistics. Select Active or
Declined to narrow a large query. Other states require an exact IP address or
client identifier search. The guard fails closed when the `stat_cmds` hook is
not available. State-filtered searches also fail closed when Kea does not
support the scoped state commands. Set the limit to `0` only when you accept
unbounded responses.

A Configuration Change holds a PostgreSQL advisory lock in an open transaction
for all of its Kea requests, up to and including the save to disk. A PostgreSQL
`idle_in_transaction_session_timeout` or a connection pooler timeout shorter than
the longest Configuration Change ends that transaction and releases the lock
while the change runs, so set these timeouts longer.

---

## Server Configuration

### Single-URL (Control Agent, or a single-protocol daemon)

Point one `Server` URL at a Kea endpoint that serves every enabled protocol — a Control Agent (Kea < 3.0, which fronts both DHCPv4 and DHCPv6), or a single DHCP daemon's HTTP control socket (Kea 3.0+) when the server runs only DHCPv4 *or* only DHCPv6. A dual-stack Kea 3.0+ deployment needs one URL per daemon — see **Dual-URL** below.

| Field | Description |
|---|---|
| `CA / Server URL` (`ca_url`) | URL of the Kea HTTP endpoint — a DHCP daemon control socket (Kea 3.0+) or the Control Agent (Kea < 3.0), e.g. `https://kea.example.com:8000` |
| `DHCPv4` | Enable DHCPv4 lease/reservation/subnet management |
| `DHCPv6` | Enable DHCPv6 lease/reservation/subnet management |
| `CA Username` (`ca_username`) / `CA Password` (`ca_password`) | HTTP Basic Auth credentials (if required) |
| `CA File Path` | Path to a custom CA certificate file for TLS verification |
| `SSL Verification` | Enable/disable TLS certificate verification (enabled by default) |

### Dual-URL (separate v4/v6 processes)

When DHCPv4 and DHCPv6 have separate endpoints — the norm on Kea 3.0+, where each daemon exposes its own HTTP control socket:

| Field | Description |
|---|---|
| `DHCPv4 URL` | URL of the DHCPv4 daemon's HTTP control socket (or its Control Agent on Kea < 3.0) |
| `DHCPv6 URL` | URL of the DHCPv6 daemon's HTTP control socket (or its Control Agent on Kea < 3.0) |

The main `CA URL` (`ca_url`) is required and acts as a fallback for any protocol without a dedicated URL.
By default, both `DHCPv4 URL` and `DHCPv6 URL` use CA-level credentials; see **Per-protocol credentials** below for optional overrides.

---

### Per-protocol credentials

When connecting directly to DHCP daemons (bypassing the Control Agent), you can configure
separate credentials per protocol:

| Field | Description |
|-------|-------------|
| `dhcp4_username` | Username for the DHCPv4 daemon (overrides `ca_username` for DHCPv4) |
| `dhcp4_password` | Password for the DHCPv4 daemon (overrides `ca_password` for DHCPv4) |
| `dhcp6_username` | Username for the DHCPv6 daemon (overrides `ca_username` for DHCPv6) |
| `dhcp6_password` | Password for the DHCPv6 daemon (overrides `ca_password` for DHCPv6) |

If per-protocol credentials are not set, the CA-level credentials (`ca_username`/`ca_password`)
are used as the default for all connections.

---

### Per-server IPAM sync settings

Each server has optional overrides for the IPAM sync job:

| Field | Default | Description |
|---|---|---|
| `IPAM Sync Enabled` (`sync_enabled`) | `True` | Include this server in the periodic sync job |
| `Sync Leases` (`sync_leases_enabled`) | `True` | Sync active DHCP leases as NetBox IP Addresses |
| `Sync Reservations` (`sync_reservations_enabled`) | `True` | Sync DHCP reservations as NetBox IP Addresses |
| `Sync Prefixes` (`sync_prefixes_enabled`) | `True` | Sync Kea subnets as NetBox IP Prefixes |
| `Sync IP Ranges` (`sync_ip_ranges_enabled`) | `True` | Sync Kea pools as NetBox IP Ranges |
| `Deprecate stale Prefixes and IP Ranges` (`sync_deprecate_prefixes_and_ranges`) | `False` | Deprecate an owned Prefix or IP Range when this server drops its last ownership link as stale. These objects are never deleted |
| `Sync VRF` (`sync_vrf`) | None (global routing table) | VRF to assign when syncing Prefixes, IP Ranges, and lease and reservation IP Addresses. There is no global fallback: leave blank to use the global routing table (no VRF). NetBox refuses to delete a VRF while a server syncs into it |
| `First complete IPAM observation` (`ipam_first_complete_at`) | None | Read-only time when this server first completed all enabled job and DHCP import observations after the ownership upgrade |
| `Persist configuration` (`persist_config`) | `True` | Automatically save Kea config after each change via `config-write`. Disable when Kea config is managed externally (e.g. Ansible) |

These fields override the global `PLUGINS_CONFIG` values for that specific server.

---

## Background IPAM Sync

The `Kea IPAM Sync` job runs automatically when `rqworker` is active:

1. Iterates all configured `Server` objects
2. For each server: fetches all active leases (v4 + v6) and all reservations
3. Creates or updates NetBox `IPAddress` objects in the server's `sync_vrf`, and links each one to the server
   and its source (lease or reservation):
   - Leases → `status=dhcp`, `dns_name` set from the Kea hostname
   - In-subnet reservations → `status=reserved`, `dns_name` set from the Kea hostname
   - An address with both a lease and a reservation → `status=active`; the reservation hostname wins
   - Global reservations create and change no IP address
   - The description starts with the sync marker `[kea-sync: <kind>]`. Text after the marker is an operator note,
     and the sync keeps it. To release an address from the sync, remove the marker or move it away from the start.
4. Cleans up stale IPs (configurable via `stale_ip_cleanup`). A server's lease or reservation that Kea no longer
   reports loses its link. When the last link of every server to an address goes, `stale_ip_cleanup` applies, but
   only after a complete lease sync and a complete reservation sync. If the server disables reservation sync,
   a complete lease sync is sufficient. An unavailable `host_cmds` hook leaves the reservation source incomplete,
   because config-file reservations can still exist. Failed or truncated lease reads keep their stale links.
5. Links Subnets to Prefixes and Pools to IP Ranges in the server's `sync_vrf`. A complete Subnet or Pool phase
   drops its own stale links. These objects stay unchanged by default and are never deleted. The server that drops
   the last link can opt in to deprecation with `sync_deprecate_prefixes_and_ranges`. A deprecated object's last link
   stays marked stale until an applied report restores its active status, or another owner supersedes the stale link.
   Removing the description marker releases every link without deprecation. DHCP plugin references prevent deprecation.
6. One server failing does not block others
7. Summary logged per server and in total

Each execution groups its native changelog records under one request ID, including MAC address changes.
**Sync now** records the initiating user. Scheduled runs use the reserved user `netbox-kea-sync`.
A job whose initiating user was deleted also uses system attribution.

The job creates this attribution account inactive, with an unusable password and no permissions.
Keep it inactive and without access grants. If an existing account with that name has login access or
permissions, the job fails before synchronization and leaves the account unchanged.

Each server's summary reports `created`, `updated`, `errors`, `prefix_errors`, `conflicts`, `disagreements`,
`skipped` and `waiting`. The job total also reports `unowned`:

- **skipped**: reservations the sync deliberately did not write: global reservations and
  reservations that reserve no address. They are not errors and do not fail the job.
- **errors**: lease and reservation rows that failed to sync, snapshots that could not
  be read, and reservations that the snapshot could not read. Any error fails the job. A
  failed snapshot and the first 10 failed rows of each server and IP version are logged at
  warning level, with the source (lease or reservation), the address and the exception.
- **conflicts**: IPAM objects that the sync left unchanged: the description of the object does not start with the sync marker (it was created by hand, or an operator
  removed the marker),
  or the new marker and the note after it do not fit in the 200-character description.
  They are deduplicated per server across its phases. Up to 20 of
  them are named in the summary and the log, so the addresses to look at are visible
  without trawling debug output.
- **disagreements**: addresses that two Kea servers report with different facts (the
  hostname or the prefix length), or that one server reports twice with different facts,
  for example from two reservations in overlapping subnets. A lease and a reservation
  disagree only on the prefix length, and an empty hostname makes no claim. The address
  keeps its values until the reports agree, or until one of them goes.
- **unowned**: marker objects with no ownership link, including objects left by a deleted server. Only the job total
  reports this count, because these objects have no owning server. It counts each object once.
- **waiting**: adopted objects whose cleanup this server held during the run while a potential owner had not
  completed its initial observations. The job total counts objects still waiting after all servers finish.
  Counts include each object once across sources and address families.

View job history, next scheduled time and logs under **System → Background Jobs → Kea IPAM Sync**.

### Upgrade to ownership links

The first observations adopt marker objects that Kea still reports. No data migration assigns owners.
An object with a blank description stays unchanged unless an operator forces a claim.
When every configured server uses the same non-global `sync_vrf`, an unowned marker IP address in the global
VRF moves into that VRF with its primary key and changelog intact. A row already owned by another server does
not move. A destination address collision keeps the global row unowned and reports a conflict. Servers with
different VRFs keep the global row and create their own scoped rows. A global-VRF server adopts it in place.

Adopted objects keep their last link until every potential owner completes all relevant enabled observations.
Periodic jobs and DHCP imports record completion separately. An incomplete phase or missing family does not
count as complete. Disabled sources and families do not hold the barrier. The read-only
`ipam_first_complete_at` field shows when a server first completed its enabled observations. Newly enabled
sources still require complete observations, even when that timestamp is already set.

`stale_ip_cleanup = "remove"` now also removes the rows of expired leases, deleted Reservations and hostless
leases after their final ownership link becomes stale. On upgrade, lease removal applies only to leases that
expire after adoption. Rows already stale before the upgrade get no link, stay unchanged and count as
`unowned`. Review these rows before removing them manually.

To change the sync interval, edit it on the **Sync Jobs** page. You do not need to restart the worker: the new interval applies after the next scheduled run.

---

## DNS Integration

When [netbox-dns](https://github.com/peteeckel/netbox-plugin-dns) with IPAMDNSsync is installed:

1. The IPAM sync sets `dns_name` on `IPAddress` objects from the Kea hostname
2. IPAMDNSsync picks up `dns_name` changes via Django signals
3. A/AAAA/PTR records are created automatically (provided matching DNS views + zones exist)

No additional configuration is required — the integration is automatic when both plugins are present.

---

## Custom Links

Add custom links to NetBox models to navigate directly to Kea lease searches.

Replace `<Kea Server ID>` with your server's object ID (visible in the top-right corner of the server detail page as `netbox_kea.server:<ID>`).

### Show DHCP leases for a prefix

**Content type**: `IPAM > Prefix`

**URL**: `https://netbox.example.com/plugins/kea/servers/<Kea Server ID>/leases{{ object.prefix.version }}/?q={{ object.prefix }}&by=subnet`

### Show DHCP leases for a device/VM interface (by MAC)

**Content types**: `DCIM > Interface`, `Virtualization > Interface`

**DHCPv4 URL**: `https://netbox.example.com/plugins/kea/servers/<Kea Server ID>/leases4/?q={{ object.mac_address }}&by=hw`

**DHCPv6 URL**: `https://netbox.example.com/plugins/kea/servers/<Kea Server ID>/leases6/?q={{ object.mac_address }}&by=hw`

### Show DHCP leases for a device/VM (by hostname)

**Content types**: `DCIM > Device`, `Virtualization > Virtual Machine`

**DHCPv4 URL**: `https://netbox.example.com/plugins/kea/servers/<Kea Server ID>/leases4/?q={{ object.name|lower }}&by=hostname`

**DHCPv6 URL**: `https://netbox.example.com/plugins/kea/servers/<Kea Server ID>/leases6/?q={{ object.name|lower }}&by=hostname`

You can substitute `{{ object.name|lower }}` with a custom field: `{{ object.cf.<your_field>|lower }}`.

---

## Development

```bash
# Install dev dependencies
uv sync

# Lint
uv run ruff check .
uv run ruff format --check .

# REUSE compliance check
uv run reuse lint

# Format
uv run ruff format .

# Install pre-commit hooks
uv run pre-commit install

# Build wheel (required before integration tests)
uv build

# Run unit tests; both variables are required (see AGENTS.md for picking values)
TEST_DB_NAME=test_netbox_kea_local TEST_REDIS_HOST=localhost \
  uv run --native-tls pytest --reuse-db -n auto --maxschedchunk=1 -q

# Run integration tests (requires Docker — see tests/test_setup.sh)
./tests/test_setup.sh
uv run --native-tls pytest -p no:django tests/ --tracing=retain-on-failure -v --cov=netbox_kea --cov-report=xml
```

See [CHANGELOG](CHANGELOG.md) for version history.

---

## License

[Apache-2.0](LICENSE) — original code by [Devon Mar](https://github.com/devon-mar), fork maintained by [Marcin Zieba](https://github.com/marcinpsk).
