- interfaces.exclude in steward.yaml (names or shell patterns) leaves interfaces out of GET /interfaces, the web UI's picker and 'steward interface list'. Subnets on them are still served. - With the ping check on, active leases are pinged every minute; a client that answers gets a last-seen time (leases.last_seen, migration 4). Not answering never takes a client offline. - The activity log filters by subnet (--subnet, the Activity page, GET /events?subnet=). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.2 KiB
Go
99 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"steward/internal/model"
|
|
)
|
|
|
|
const leaseCols = `ip, subnet_id, mac, client_id, hostname, state, start, end, interface, updated, last_seen`
|
|
|
|
// 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
|
|
var seen sql.NullString
|
|
if err := rows.Scan(&ip, &l.SubnetID, &l.MAC, &l.ClientID, &l.Hostname, &l.State, &start, &end, &l.Interface, &updated, &seen); err != nil {
|
|
return nil, err
|
|
}
|
|
l.IP, _ = netip.ParseAddr(ip)
|
|
l.Start, l.End, l.Updated = parseTS(start), parseTS(end), parseTS(updated)
|
|
l.LastSeen = parseTSPtr(seen)
|
|
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, last_seen = excluded.last_seen`,
|
|
l.IP.String(), l.SubnetID, l.MAC, l.ClientID, l.Hostname, l.State, ts(l.Start), ts(l.End), l.Interface, ts(l.Updated), tsPtr(l.LastSeen)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// SetLastSeen records when leases' clients answered a ping, in one
|
|
// transaction.
|
|
func (s *Store) SetLastSeen(ctx context.Context, seen map[netip.Addr]time.Time) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
for ip, t := range seen {
|
|
if _, err := tx.ExecContext(ctx, `UPDATE leases SET last_seen = ? WHERE ip = ?`, ts(t), ip.String()); 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()
|
|
}
|