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:
@@ -0,0 +1,85 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"vetting/internal/model"
|
||||
)
|
||||
|
||||
// Measurements persists timestamped numeric samples: temps, fan speeds,
|
||||
// PSU voltages, fio IOPS, iperf throughput, SMART attributes. The schema
|
||||
// stores (kind, key, value, unit) so Phase 5 reports can group freely
|
||||
// without new tables per source.
|
||||
type Measurements struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (m *Measurements) Create(ctx context.Context, in model.Measurement) (int64, error) {
|
||||
if in.TS.IsZero() {
|
||||
in.TS = time.Now().UTC()
|
||||
}
|
||||
res, err := m.DB.ExecContext(ctx, `
|
||||
INSERT INTO measurements(run_id, stage_id, ts, kind, key, value, unit)
|
||||
VALUES(?,?,?,?,?,?,?)
|
||||
`, in.RunID, nullInt64(in.StageID), in.TS, in.Kind, in.Key, in.Value, in.Unit)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert measurement: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// CreateBatch inserts a batch in one transaction. The sensor endpoint
|
||||
// hands us ~5–20 samples per tick; a single commit keeps SQLite happy.
|
||||
func (m *Measurements) CreateBatch(ctx context.Context, rows []model.Measurement) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx, err := m.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
now := time.Now().UTC()
|
||||
for _, r := range rows {
|
||||
if r.TS.IsZero() {
|
||||
r.TS = now
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO measurements(run_id, stage_id, ts, kind, key, value, unit)
|
||||
VALUES(?,?,?,?,?,?,?)
|
||||
`, r.RunID, nullInt64(r.StageID), r.TS, r.Kind, r.Key, r.Value, r.Unit); err != nil {
|
||||
return fmt.Errorf("insert measurement: %w", err)
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// ListForRun returns all measurements for a run. Callers filter by kind
|
||||
// in memory; the row count is small per run (≈thousands).
|
||||
func (m *Measurements) ListForRun(ctx context.Context, runID int64) ([]model.Measurement, error) {
|
||||
rows, err := m.DB.QueryContext(ctx, `
|
||||
SELECT id, run_id, stage_id, ts, kind, key, value, COALESCE(unit,'')
|
||||
FROM measurements WHERE run_id = ? ORDER BY ts, id
|
||||
`, runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.Measurement
|
||||
for rows.Next() {
|
||||
var meas model.Measurement
|
||||
var stageID sql.NullInt64
|
||||
if err := rows.Scan(&meas.ID, &meas.RunID, &stageID, &meas.TS, &meas.Kind, &meas.Key, &meas.Value, &meas.Unit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stageID.Valid {
|
||||
v := stageID.Int64
|
||||
meas.StageID = &v
|
||||
}
|
||||
out = append(out, meas)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user