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>
This commit is contained in:
commit
3c0808591f
111 files changed
+17851
No files matched your search
@@ -0,0 +1,79 @@
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user