Initial commit: full Phases 1-6 implementation
CI / Lint + build + test (push) Has been cancelled

Post-repair hardware validation pipeline for Proxmox cluster hosts.
Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq
PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
This commit is contained in:
2026-04-17 21:32:10 -04:00
commit 9bb4b09a04
98 changed files with 11960 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
package db
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"path/filepath"
"sort"
"strings"
_ "modernc.org/sqlite"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// Open opens the SQLite DB at path, enabling foreign keys and WAL,
// and applies every embedded migration in filename order.
func Open(path string) (*sql.DB, error) {
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)", filepath.ToSlash(path))
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("ping sqlite: %w", err)
}
if err := migrate(db); err != nil {
_ = db.Close()
return nil, err
}
return db, nil
}
func migrate(db *sql.DB) error {
entries, err := fs.ReadDir(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("read migrations: %w", err)
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
names = append(names, e.Name())
}
}
sort.Strings(names)
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)`); err != nil {
return fmt.Errorf("ensure schema_migrations: %w", err)
}
for _, name := range names {
var applied int
if err := db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE name = ?`, name).Scan(&applied); err != nil {
return fmt.Errorf("check migration %s: %w", name, err)
}
if applied > 0 {
continue
}
content, err := migrationsFS.ReadFile("migrations/" + name)
if err != nil {
return fmt.Errorf("read migration %s: %w", name, err)
}
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin migration %s: %w", name, err)
}
if _, err := tx.Exec(string(content)); err != nil {
_ = tx.Rollback()
return fmt.Errorf("apply migration %s: %w", name, err)
}
if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, name); err != nil {
_ = tx.Rollback()
return fmt.Errorf("record migration %s: %w", name, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit migration %s: %w", name, err)
}
}
return nil
}
+93
View File
@@ -0,0 +1,93 @@
-- Phase 1 schema covers the full Vetting domain so future phases
-- only add data, never restructure.
CREATE TABLE IF NOT EXISTS hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
mac TEXT NOT NULL UNIQUE, -- lowercase colon form
wol_broadcast_ip TEXT NOT NULL,
wol_port INTEGER NOT NULL DEFAULT 9,
expected_spec_yaml TEXT NOT NULL,
pdu_config_json TEXT,
ipmi_config_json TEXT,
notes TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
state TEXT NOT NULL,
result TEXT, -- pass|fail|null
failed_stage TEXT,
next_boot_target TEXT, -- linux|memtest|linux-post-memtest (Phase 2+)
agent_token_hash TEXT NOT NULL,
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
report_path TEXT,
hold_ip TEXT,
override_flags_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_runs_host ON runs(host_id);
CREATE INDEX IF NOT EXISTS idx_runs_state ON runs(state);
CREATE TABLE IF NOT EXISTS stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
name TEXT NOT NULL,
ordinal INTEGER NOT NULL,
state TEXT NOT NULL, -- pending|running|passed|failed|skipped
started_at TIMESTAMP,
completed_at TIMESTAMP,
summary_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_stages_run_ordinal ON stages(run_id, ordinal);
CREATE TABLE IF NOT EXISTS measurements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
stage_id INTEGER REFERENCES stages(id) ON DELETE SET NULL,
ts TIMESTAMP NOT NULL,
kind TEXT NOT NULL, -- temp|power|iperf|fio|smart_attr
key TEXT NOT NULL,
value REAL,
unit TEXT
);
CREATE INDEX IF NOT EXISTS idx_measurements_run_kind_ts ON measurements(run_id, kind, ts);
CREATE TABLE IF NOT EXISTS artifacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
stage_id INTEGER REFERENCES stages(id) ON DELETE SET NULL,
kind TEXT NOT NULL,
path TEXT NOT NULL,
sha256 TEXT NOT NULL,
size_bytes INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS spec_diffs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
field TEXT NOT NULL,
expected TEXT,
actual TEXT,
severity TEXT NOT NULL, -- critical|warning|info
ignored INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER REFERENCES runs(id) ON DELETE CASCADE,
host_id INTEGER REFERENCES hosts(id) ON DELETE CASCADE,
ts TIMESTAMP NOT NULL,
level TEXT NOT NULL,
kind TEXT NOT NULL,
message TEXT NOT NULL,
data_json TEXT
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);