bda568b25c
Go service for Proxmox homelab cluster provisioning. Handles PXE boot, Proxmox autoinstall (answer file generation), cluster join via SSH, and Infrastructure API registration. - Host state machine (registered → pxe_ready → installing → ready) - dnsmasq supervisor with MAC-based allowlist - iPXE script and Proxmox answer file generation - First-boot phone-home → cluster join → infra registration - Operation locking with expiry (409 on conflict) - SSE event hub for real-time dashboard updates - Admin dashboard (host grid, detail, registration form) - Config-driven server types with hot-reload - Docker deployment (multi-stage fat image) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"provisioning/internal/model"
|
|
)
|
|
|
|
type Operations struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func (s *Operations) Create(ctx context.Context, op model.Operation) (int64, error) {
|
|
res, err := s.DB.ExecContext(ctx, `
|
|
INSERT INTO operations(host_id, kind, state, image_id)
|
|
VALUES(?,?,?,?)
|
|
`, op.HostID, op.Kind, model.OpActive, nullInt64(op.ImageID))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("insert operation: %w", err)
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (s *Operations) Complete(ctx context.Context, id int64) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, model.OpCompleted, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Operations) Fail(ctx context.Context, id int64, errMsg string) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now'), error_message = ? WHERE id = ?`, model.OpFailed, errMsg, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Operations) ListByHost(ctx context.Context, hostID int64) ([]model.Operation, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `
|
|
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
|
|
FROM operations WHERE host_id = ? ORDER BY started_at DESC
|
|
`, hostID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list operations: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Operation
|
|
for rows.Next() {
|
|
var op model.Operation
|
|
var startedAt string
|
|
var completedAt sql.NullString
|
|
if err := rows.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
|
|
return nil, fmt.Errorf("scan operation: %w", err)
|
|
}
|
|
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
|
|
if completedAt.Valid {
|
|
t, _ := time.Parse(time.RFC3339, completedAt.String)
|
|
op.CompletedAt = &t
|
|
}
|
|
out = append(out, op)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Operations) GetActive(ctx context.Context, hostID int64) (*model.Operation, error) {
|
|
row := s.DB.QueryRowContext(ctx, `
|
|
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
|
|
FROM operations WHERE host_id = ? AND state = ? ORDER BY started_at DESC LIMIT 1
|
|
`, hostID, model.OpActive)
|
|
var op model.Operation
|
|
var startedAt string
|
|
var completedAt sql.NullString
|
|
if err := row.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get active operation: %w", err)
|
|
}
|
|
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
|
|
return &op, nil
|
|
}
|
|
|
|
func nullInt64(v int64) any {
|
|
if v == 0 {
|
|
return nil
|
|
}
|
|
return v
|
|
}
|