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>
247 lines
7.8 KiB
Go
247 lines
7.8 KiB
Go
// Package api holds the JSON shapes of Steward's REST API, shared by the
|
|
// server and the Go client (and so the CLI).
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"steward/internal/model"
|
|
"steward/internal/validate"
|
|
"steward/internal/version"
|
|
)
|
|
|
|
// Prefix starts every API path.
|
|
const Prefix = "/api/v1"
|
|
|
|
// ErrorBody is every error response.
|
|
type ErrorBody struct {
|
|
Error ErrorInfo `json:"error"`
|
|
}
|
|
|
|
// ErrorInfo describes an error.
|
|
type ErrorInfo struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Problems []validate.Problem `json:"problems,omitempty"`
|
|
// Result is the preview of a refused change.
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
}
|
|
|
|
// Whoami describes the caller.
|
|
type Whoami struct {
|
|
User *model.User `json:"user"`
|
|
Admin bool `json:"admin"`
|
|
Method string `json:"method"`
|
|
Permissions []string `json:"permissions"`
|
|
CSRFToken string `json:"csrf_token,omitempty"`
|
|
Build version.Build `json:"build"`
|
|
}
|
|
|
|
// Login is the login request.
|
|
type Login struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// PasswordChange changes the caller's password.
|
|
type PasswordChange struct {
|
|
Current string `json:"current"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// OptionInput sets an option: by name ("routers") or number ("43"), with
|
|
// a type for numbers the catalog doesn't know (default hex).
|
|
type OptionInput struct {
|
|
Option string `json:"option"`
|
|
Value string `json:"value"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// SettingsInput changes the settings (unset fields stay).
|
|
type SettingsInput struct {
|
|
LeaseTime *int `json:"lease_time,omitempty"`
|
|
Options *[]OptionInput `json:"options,omitempty"`
|
|
PingCheck *bool `json:"ping_check,omitempty"`
|
|
PingTimeout *int `json:"ping_timeout_ms,omitempty"`
|
|
DeclineHold *int `json:"decline_hold,omitempty"`
|
|
NotAuthoritative *bool `json:"not_authoritative,omitempty"`
|
|
}
|
|
|
|
// SubnetInput creates or changes a subnet (unset fields stay). Pools are
|
|
// "FIRST-LAST" or prefixes.
|
|
type SubnetInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Interface *string `json:"interface,omitempty"`
|
|
Prefix *string `json:"prefix,omitempty"`
|
|
Pools *[]string `json:"pools,omitempty"`
|
|
LeaseTime *int `json:"lease_time,omitempty"`
|
|
Options *[]OptionInput `json:"options,omitempty"`
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// ReservationInput creates or changes a reservation (unset fields stay).
|
|
type ReservationInput struct {
|
|
MAC *string `json:"mac,omitempty"`
|
|
ClientID *string `json:"client_id,omitempty"`
|
|
IP *string `json:"ip,omitempty"`
|
|
Hostname *string `json:"hostname,omitempty"`
|
|
Options *[]OptionInput `json:"options,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// Subnet is a subnet with its usage.
|
|
type Subnet struct {
|
|
*model.Subnet
|
|
Usage Usage `json:"usage"`
|
|
}
|
|
|
|
// Usage counts a subnet's addresses.
|
|
type Usage struct {
|
|
// PoolSize is the number of addresses in the pools; Leased of them
|
|
// are held by clients, Declined held back.
|
|
PoolSize int `json:"pool_size"`
|
|
Leased int `json:"leased"`
|
|
Declined int `json:"declined"`
|
|
// Reservations in the subnet, and how many of them have a lease now.
|
|
Reservations int `json:"reservations"`
|
|
ReservationsLeased int `json:"reservations_leased"`
|
|
}
|
|
|
|
// Reservation is a reservation with where it is.
|
|
type Reservation struct {
|
|
*model.Reservation
|
|
SubnetID int64 `json:"subnet_id"`
|
|
SubnetName string `json:"subnet"`
|
|
// Lease is the reservation's current lease, if any.
|
|
Lease *Lease `json:"lease,omitempty"`
|
|
}
|
|
|
|
// Lease is a lease with its state now.
|
|
type Lease struct {
|
|
*model.Lease
|
|
SubnetName string `json:"subnet"`
|
|
// Reservation is the ID of the reservation for the address, if any.
|
|
Reservation int64 `json:"reservation,omitempty"`
|
|
// ReservedHostname is the reservation's host name.
|
|
ReservedHostname string `json:"reserved_hostname,omitempty"`
|
|
}
|
|
|
|
// MutationResponse answers a configuration change.
|
|
type MutationResponse struct {
|
|
Applied bool `json:"applied"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
HistoryID int64 `json:"history_id,omitempty"`
|
|
Changes []model.Change `json:"changes"`
|
|
Warnings []validate.Problem `json:"warnings,omitempty"`
|
|
// Object is the created or changed object.
|
|
Object json.RawMessage `json:"object,omitempty"`
|
|
}
|
|
|
|
// HostInterface is one of the server's network interfaces.
|
|
type HostInterface struct {
|
|
Name string `json:"name"`
|
|
Addresses []string `json:"addresses"`
|
|
// Subnets served on it.
|
|
Subnets []int64 `json:"subnets"`
|
|
}
|
|
|
|
// InterfaceStatus says whether DHCP is served on an interface.
|
|
type InterfaceStatus struct {
|
|
Name string `json:"name"`
|
|
Listening bool `json:"listening"`
|
|
Error string `json:"error,omitempty"`
|
|
Since time.Time `json:"since"`
|
|
Addresses []string `json:"addresses"`
|
|
Subnets []int64 `json:"subnets"`
|
|
}
|
|
|
|
// Status is the dashboard's overview.
|
|
type Status struct {
|
|
Build version.Build `json:"build"`
|
|
Started time.Time `json:"started"`
|
|
Interfaces []InterfaceStatus `json:"interfaces"`
|
|
Subnets []Subnet `json:"subnets"`
|
|
Problems []validate.Problem `json:"problems"`
|
|
// Leases counts leases by state.
|
|
Leases map[string]int `json:"leases"`
|
|
// Messages counts DHCP messages since the start, by type.
|
|
Messages map[string]uint64 `json:"messages"`
|
|
// PingProblem says why addresses can't be pinged before an offer.
|
|
PingProblem string `json:"ping_problem,omitempty"`
|
|
Database string `json:"database"`
|
|
}
|
|
|
|
// TokenInput creates a token.
|
|
type TokenInput struct {
|
|
Name string `json:"name"`
|
|
// Grants ("leases:read") or a role restrict the token; none: all of
|
|
// the user's permissions.
|
|
Grants []string `json:"grants,omitempty"`
|
|
Role string `json:"role,omitempty"`
|
|
ExpiresIn string `json:"expires_in,omitempty"` // 90d, 720h
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// Token is a token; Secret is only set when it is created.
|
|
type Token struct {
|
|
*model.Token
|
|
GrantStrings []string `json:"grant_strings"`
|
|
Secret string `json:"secret,omitempty"`
|
|
}
|
|
|
|
// UserInput creates or changes a user.
|
|
type UserInput struct {
|
|
Username *string `json:"username,omitempty"`
|
|
Password *string `json:"password,omitempty"`
|
|
Admin *bool `json:"admin,omitempty"`
|
|
Grants *[]string `json:"grants,omitempty"`
|
|
Role *string `json:"role,omitempty"`
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// User is a user with grants written out.
|
|
type User struct {
|
|
*model.User
|
|
GrantStrings []string `json:"grant_strings"`
|
|
}
|
|
|
|
// OptionDef is an option of the catalog.
|
|
type OptionDef struct {
|
|
Code int `json:"code"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Help string `json:"help"`
|
|
Common bool `json:"common,omitempty"`
|
|
}
|
|
|
|
// Catalog lists the options Steward knows, the value types and the codes
|
|
// that can't be set.
|
|
type Catalog struct {
|
|
Options []OptionDef `json:"options"`
|
|
Types []string `json:"types"`
|
|
Reserved map[int]string `json:"reserved"`
|
|
Roles map[string][]string `json:"roles"`
|
|
}
|
|
|
|
// Event is a DHCP message in or out.
|
|
type Event struct {
|
|
Time time.Time `json:"time"`
|
|
Interface string `json:"interface"`
|
|
Type string `json:"type"`
|
|
MAC string `json:"mac"`
|
|
IP string `json:"ip,omitempty"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// ImportResult reports a Kea import.
|
|
type ImportResult struct {
|
|
MutationResponse
|
|
Leases int `json:"leases"`
|
|
Notes []string `json:"notes"`
|
|
Skipped []string `json:"skipped"`
|
|
}
|