8367ec2a9f
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>
149 lines
3.8 KiB
Go
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
|
|
}
|