Files
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

80 lines
2.6 KiB
Go

package store
import (
"context"
"net/netip"
"time"
"steward/internal/model"
)
const leaseCols = `ip, subnet_id, mac, client_id, hostname, state, start, end, interface, updated`
// LoadLeases reads every lease.
func (s *Store) LoadLeases(ctx context.Context) ([]*model.Lease, error) {
rows, err := s.db.QueryContext(ctx, `SELECT `+leaseCols+` FROM leases`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*model.Lease
for rows.Next() {
l := &model.Lease{}
var ip, start, end, updated string
if err := rows.Scan(&ip, &l.SubnetID, &l.MAC, &l.ClientID, &l.Hostname, &l.State, &start, &end, &l.Interface, &updated); err != nil {
return nil, err
}
l.IP, _ = netip.ParseAddr(ip)
l.Start, l.End, l.Updated = parseTS(start), parseTS(end), parseTS(updated)
out = append(out, l)
}
return out, rows.Err()
}
// PutLease writes a lease (replacing the address's previous one). It
// returns once the write is durable enough that a crash doesn't lose it:
// the DHCP server answers the client only afterwards.
func (s *Store) PutLease(ctx context.Context, l *model.Lease) error {
return s.PutLeases(ctx, []*model.Lease{l})
}
// PutLeases writes several leases in one transaction (imports).
func (s *Store) PutLeases(ctx context.Context, ls []*model.Lease) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, l := range ls {
if _, err := tx.ExecContext(ctx, `INSERT INTO leases (`+leaseCols+`) VALUES (?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(ip) DO UPDATE SET subnet_id = excluded.subnet_id, mac = excluded.mac, client_id = excluded.client_id, hostname = excluded.hostname,
state = excluded.state, start = excluded.start, end = excluded.end, interface = excluded.interface, updated = excluded.updated`,
l.IP.String(), l.SubnetID, l.MAC, l.ClientID, l.Hostname, l.State, ts(l.Start), ts(l.End), l.Interface, ts(l.Updated)); err != nil {
return err
}
}
return tx.Commit()
}
// DeleteLease removes the lease of an address.
func (s *Store) DeleteLease(ctx context.Context, ip netip.Addr) error {
res, err := s.db.ExecContext(ctx, `DELETE FROM leases WHERE ip = ?`, ip.String())
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}
// PruneLeases deletes leases that ended before a time and aren't active
// (they are kept a while so returning clients get their old address).
func (s *Store) PruneLeases(ctx context.Context, before time.Time) (int64, error) {
res, err := s.db.ExecContext(ctx, `DELETE FROM leases WHERE end < ?`, ts(before))
if err != nil {
return 0, err
}
return res.RowsAffected()
}