Files
steward/internal/api/types.go
T
abrown38andClaude Opus 5.5 3c0808591f Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
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>
2026-10-14 12:14:07 +01:00

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"`
}