Files
steward/docs/api.md
T
abrown38andClaude Opus 5.5 8ee430276c Address order: sequential, reverse or random, per subnet and pool
- Which never-used pool address a new client gets: sequential (the
  lowest, as before), reverse or random, in the settings, a subnet or
  a single pool (FIRST-LAST:ORDER). When no never-used address is left,
  the one free the longest, whatever the order; returning clients keep
  theirs.
- A subnet's pools are used in the order listed, or one at random
  weighted by its never-used addresses. Pools now keep the order they
  are given in (no more sorting by address).
- CLI --address-order and --pool-order; --add-pool of an existing range
  changes its order in place. Web UI: per-pool order and a move-up
  button, pool and address order on the subnet and settings pages.
- Kea's allocator: random is imported. Schema migration 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:13:25 +01:00

161 lines
7.4 KiB
Markdown

# 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), with `steward token create NAME`, or with `stewardd
user token USER NAME` on the server.
* **Sessions**: `POST /auth/login` with `{"username", "password"}` sets a
cookie and returns `csrf_token`. Requests other than GET must send it as
`X-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](permissions.md) has what each call needs.
## Errors
```json
{"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:
```json
{"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=1` checks the change and returns what it would do, with
`"applied": false, "dry_run": true`.
* `before` is missing for a create and `after` for a delete.
* `warnings` are the problems the change brings; older ones are in
`/status`.
* `object` is 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, leaving out those matching `interfaces.exclude` |
| `GET /catalog` | the option catalog, value types, reserved codes, roles |
### Settings
`GET /settings`, `PATCH /settings` with any of:
```json
{"lease_time": 43200, "ping_check": true, "ping_timeout_ms": 500, "decline_hold": 86400,
"not_authoritative": false, "address_order": "sequential",
"options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}
```
`address_order` is `sequential` (left out when it is), `reverse` or
`random` ([subnets.md](subnets.md#address-order)).
`options` replaces the list. Each option is `{"option": NAME or NUMBER,
"value": TEXT}`, with `"type"` for numbers the catalog doesn't know
([options.md](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 |
```json
{"name": "office", "interface": "eth0", "prefix": "10.0.10.0/24",
"pools": ["10.0.10.100-10.0.10.199", "10.0.10.224/28:random"], "pool_order": "listed",
"address_order": "default", "lease_time": 0,
"options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}
```
`pools` and `options` replace the lists; the pools keep their order. A
pool's `:ORDER` is its own address order. `pool_order` is `listed` or
`random`; `address_order` is `sequential`, `reverse`, `random` or
`default` (the settings'). Subnets come back with pools as
`{"start", "end", "order"}`, and the defaults (`listed`, `default`) left
out. `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", "disabled", "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, and `last_seen` when the client last answered a ping (left out if it never has); `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=&subnet=&interface=&limit=]` | the latest DHCP messages, newest first (the last 1000 are kept in memory). `subnet` (ID, name or prefix) keeps messages for its addresses, and those without an address on its interface |
### 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](kea.md)); answers with `leases`, `notes`, `skipped` too |
### Certificate
Administrators only ([certificates.md](certificates.md)).
| | |
|---|---|
| `GET /certificate` | `{"configured", "source": "acme"\|"file"\|"none", "status": {"domain", "directory", "staging", "state": "none"\|"ok"\|"working"\|"error", "not_before", "not_after", "issuer", "renew_at", "last_error", "last_attempt", "next_attempt", "check"}}` |
| `POST /certificate/check` | run the check now (up to a few minutes); answers `{"time", "domain", "ok", "steps": [{"name", "status", "detail", "queries"}]}` |
| `POST /certificate/renew` | get a new certificate now, in the background (202); follow it with `GET /certificate` |
`GET /status` lists the certificate's problems (`certificate_failed`,
`certificate_expiring`, `certificate_staging`) for administrators.
### Users, tokens, audit
| | |
|---|---|
| `POST /auth/login`, `POST /auth/logout`, `GET /auth/whoami` | `whoami` has `token_id` when the caller uses a token |
| `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 |
| `POST /tokens/{id}/rotate` | a new secret: `{"grace": "1h", "expires_in": "90d", "no_expiry": false}`, all optional; answers with `secret` and `previous_secret_valid_until` |
| `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=]` | |