Subnets served per interface, pools, reservations, options at three levels, leases in SQLite saved before the ACK, ping and decline conflict checks, validated changes with history and undo, users and tokens, an audit log, a Kea importer, a sandboxed systemd unit and Debian packages. Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.6 KiB
5.6 KiB
The REST API
Everything the web UI and the CLI do goes through this API, under
/api/v1. Requests and answers are JSON.
Authentication
- API tokens:
Authorization: Bearer steward_.... Make one in the web UI (your account), withsteward token create NAME, or withstewardd user token USER NAMEon the server. - Sessions:
POST /auth/loginwith{"username", "password"}sets a cookie and returnscsrf_token. Requests other than GET must send it asX-CSRF-Token. - The local socket (
/run/steward/steward.sock): root and stewardd's own user need nothing; others send a token.
Failed logins and token attempts are rate limited per address and per user name. permissions.md has what each call needs.
Errors
{"error": {"code": "validation", "message": "subnet iot (10.0.20.0/24): it overlaps subnet office (10.0.10.0/24)",
"problems": [{"severity": "error", "code": "subnet_overlap", "message": "...", "object": "subnet iot (10.0.20.0/24)"}]}}
| Status | Code | |
|---|---|---|
| 400 | bad_request |
the request can't be read (an unknown option name, a bad address) |
| 401 | unauthenticated, session_expired, login_failed |
|
| 403 | forbidden, csrf |
|
| 404 | not_found |
|
| 409 | conflict |
e.g. undoing a change after the objects changed again |
| 422 | validation |
the change would make the configuration invalid; problems lists why |
| 429 | rate_limited |
Changes
Calls that change the configuration answer:
{"applied": true, "history_id": 7,
"changes": [{"kind": "subnet", "id": 12, "label": "iot (10.0.20.0/24)", "before": {...}, "after": {...}}],
"warnings": [{"severity": "warning", "code": "interface_unknown", "message": "...", "object": "..."}],
"object": {...}}
?dry_run=1checks the change and returns what it would do, with"applied": false, "dry_run": true.beforeis missing for a create andafterfor a delete.warningsare the problems the change brings; older ones are in/status.objectis the created or changed object.
Creating answers 201, other changes 200.
Endpoints
Status
GET /healthz |
{"ok": true, "version"}, without authentication |
GET /status |
build, start time, interfaces (listening, error, addresses, subnets), subnets with usage, problems, leases (counts by state), messages (counts by type), ping_problem |
GET /interfaces |
the host's interfaces, their addresses and subnets |
GET /catalog |
the option catalog, value types, reserved codes, roles |
Settings
GET /settings, PATCH /settings with any of:
{"lease_time": 43200, "ping_check": true, "ping_timeout_ms": 500, "decline_hold": 86400,
"not_authoritative": false, "options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}
options replaces the list. Each option is {"option": NAME or NUMBER, "value": TEXT}, with "type" for numbers the catalog doesn't know
(options.md).
Subnets
GET /subnets |
every subnet, with usage (pool_size, leased, declined, reservations, reservations_leased) |
POST /subnets |
create; needs prefix and interface |
GET /subnets/{ref} |
by ID, name, or prefix with _ for / (10.0.10.0_24) |
PATCH /subnets/{ref} |
change the given fields |
DELETE /subnets/{ref} |
delete, with its reservations |
{"name": "office", "interface": "eth0", "prefix": "10.0.10.0/24",
"pools": ["10.0.10.100-10.0.10.199", "10.0.10.224/28"], "lease_time": 0,
"options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}
pools and options replace the lists; lease_time 0 means the
settings'.
Reservations
GET /reservations[?subnet=REF] |
each with subnet_id, subnet and its current lease |
POST /reservations |
{"ip", "mac", "client_id", "hostname", "options", "comment"}; ip and mac or client_id needed |
GET, PATCH, DELETE /reservations/{ref} |
by ID, address, or MAC address (when it has one reservation) |
Leases and activity
GET /leases[?subnet=&state=&interface=&q=] |
every lease, with its state now (active, expired, released, declined), subnet and its reservation; q searches address, MAC and host name |
GET /leases/{ip} |
one |
DELETE /leases/{ip} |
forget it (audited) |
POST /leases/{ip}/reserve |
make it a reservation; optional {"hostname", "comment"} |
GET /events[?mac=&interface=&limit=] |
the latest DHCP messages, newest first (the last 1000 are kept in memory) |
History and import
GET /history[?limit=&before=] |
changes, newest first |
GET /history/{id} |
one, with each object before and after |
POST /history/{id}/revert |
undo it; 409 if an object it touched has changed since, or it was undone already |
POST /import/kea |
{"config": TEXT, "leases": [TEXT, ...], "interfaces": {"PREFIX": "IFACE"}, "replace": false} (kea.md); answers with leases, notes, skipped too |
Users, tokens, audit
POST /auth/login, POST /auth/logout, GET /auth/whoami |
|
POST /me/password |
{"current", "password"} |
GET /me/tokens, POST /me/tokens |
{"name", "role", "grants", "expires_in"}; the answer's secret is shown once |
GET /tokens, DELETE /tokens/{id} |
all tokens (administrators), revoke |
GET, POST /users; PATCH, DELETE /users/{id} |
administrators; {"username", "password", "admin", "role", "grants", "disabled", "comment"} |
GET, POST /users/{id}/tokens |
|
GET /audit[?user=&action=&result=&q=&before=&limit=] |