Files
Vetting/internal/store/hosts.go
T
josh 8367ec2a9f
CI / Lint + build + test (push) Successful in 1m36s
Release / detect (push) Successful in 5s
Release / build-live-image (push) Has been skipped
Release / bundle (push) Successful in 49s
docs: comprehensive documentation expansion
Add 4 new doc files (configuration reference, development guide, API
reference with full request/response schemas, database schema), expand
the README with a feature list and how-it-works walkthrough, fix
missing Firmware and Burn stages in architecture.md and test-suite.md,
add threshold engine and host-mode agent sections, and add godoc
comments to 11 packages and 6 model types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 18:37:26 -04:00

149 lines
3.8 KiB
Go

// Package store is the repository layer for the orchestrator's SQLite
// database. Each store type (Hosts, Runs, Stages, etc.) wraps a
// *sql.DB and exposes hand-written SQL queries — no ORM.
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"vetting/internal/model"
)
type Hosts struct {
DB *sql.DB
}
var ErrNotFound = errors.New("not found")
const hostColumns = `id, name, mac, wol_broadcast_ip, wol_port, expected_spec_yaml,
COALESCE(pdu_config_json,''), COALESCE(ipmi_config_json,''),
notes, created_at, updated_at, last_seen_at`
func scanHost(row interface {
Scan(dest ...any) error
}, h *model.Host) error {
var lastSeen sql.NullTime
if err := row.Scan(&h.ID, &h.Name, &h.MAC, &h.WoLBroadcastIP, &h.WoLPort,
&h.ExpectedSpecYAML, &h.PDUConfigJSON, &h.IPMIConfigJSON,
&h.Notes, &h.CreatedAt, &h.UpdatedAt, &lastSeen); err != nil {
return err
}
if lastSeen.Valid {
t := lastSeen.Time
h.LastSeenAt = &t
}
return nil
}
func (h *Hosts) Create(ctx context.Context, in model.Host) (int64, error) {
in.MAC = normalizeMAC(in.MAC)
res, err := h.DB.ExecContext(ctx, `
INSERT INTO hosts(name, mac, wol_broadcast_ip, wol_port, expected_spec_yaml, pdu_config_json, ipmi_config_json, notes)
VALUES(?,?,?,?,?,?,?,?)
`, in.Name, in.MAC, in.WoLBroadcastIP, in.WoLPort, in.ExpectedSpecYAML, nullIfEmpty(in.PDUConfigJSON), nullIfEmpty(in.IPMIConfigJSON), in.Notes)
if err != nil {
return 0, fmt.Errorf("insert host: %w", err)
}
return res.LastInsertId()
}
func (h *Hosts) List(ctx context.Context) ([]model.Host, error) {
rows, err := h.DB.QueryContext(ctx, `
SELECT `+hostColumns+`
FROM hosts
ORDER BY name COLLATE NOCASE
`)
if err != nil {
return nil, fmt.Errorf("list hosts: %w", err)
}
defer rows.Close()
var out []model.Host
for rows.Next() {
var host model.Host
if err := scanHost(rows, &host); err != nil {
return nil, fmt.Errorf("scan host: %w", err)
}
out = append(out, host)
}
return out, rows.Err()
}
func (h *Hosts) Get(ctx context.Context, id int64) (*model.Host, error) {
row := h.DB.QueryRowContext(ctx, `
SELECT `+hostColumns+`
FROM hosts WHERE id = ?
`, id)
var host model.Host
err := scanHost(row, &host)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get host: %w", err)
}
return &host, nil
}
// GetByMAC looks up a host by its normalized MAC. Used by the host-mode
// heartbeat endpoint, which only has a MAC to go on.
func (h *Hosts) GetByMAC(ctx context.Context, mac string) (*model.Host, error) {
row := h.DB.QueryRowContext(ctx, `
SELECT `+hostColumns+`
FROM hosts WHERE mac = ?
`, normalizeMAC(mac))
var host model.Host
err := scanHost(row, &host)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get host by mac: %w", err)
}
return &host, nil
}
// UpdateLastSeen stamps the host row with the most recent heartbeat.
// Targeted UPDATE so it doesn't race with UI edits of other fields.
func (h *Hosts) UpdateLastSeen(ctx context.Context, mac string, t time.Time) error {
res, err := h.DB.ExecContext(ctx,
`UPDATE hosts SET last_seen_at = ? WHERE mac = ?`,
t.UTC(), normalizeMAC(mac))
if err != nil {
return fmt.Errorf("update last_seen_at: %w", err)
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (h *Hosts) Delete(ctx context.Context, id int64) error {
res, err := h.DB.ExecContext(ctx, `DELETE FROM hosts WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete host: %w", err)
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func normalizeMAC(m string) string {
return strings.ToLower(strings.TrimSpace(m))
}
func nullIfEmpty(s string) any {
if s == "" {
return nil
}
return s
}