b23ef64ee1
Generate a fresh ed25519 key pair at rebuild time, inject the public key into the Proxmox answer file, use the private key for cluster join over SSH, then remove the key from both the remote host and the database. This eliminates the need to manage static SSH keys in config/secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.4 KiB
Go
146 lines
4.4 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"provisioning/internal/model"
|
|
)
|
|
|
|
type Hosts struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
const hostColumns = `id, hostname, mac, server_type, state, ip_address, hardware_id, infra_host_id, notes, created_at, updated_at`
|
|
|
|
func scanHost(row interface{ Scan(dest ...any) error }, h *model.Host) error {
|
|
var ip, hwID sql.NullString
|
|
var infraID sql.NullInt64
|
|
var createdAt, updatedAt string
|
|
if err := row.Scan(&h.ID, &h.Hostname, &h.MAC, &h.ServerType, &h.State,
|
|
&ip, &hwID, &infraID, &h.Notes, &createdAt, &updatedAt); err != nil {
|
|
return err
|
|
}
|
|
h.IPAddress = ip.String
|
|
h.HardwareID = hwID.String
|
|
if infraID.Valid {
|
|
h.InfraHostID = infraID.Int64
|
|
}
|
|
h.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
|
h.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
|
return nil
|
|
}
|
|
|
|
func (s *Hosts) Create(ctx context.Context, h model.Host) (int64, error) {
|
|
h.MAC = normalizeMAC(h.MAC)
|
|
res, err := s.DB.ExecContext(ctx, `
|
|
INSERT INTO hosts(hostname, mac, server_type, notes)
|
|
VALUES(?,?,?,?)
|
|
`, h.Hostname, h.MAC, h.ServerType, h.Notes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("insert host: %w", err)
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (s *Hosts) List(ctx context.Context) ([]model.Host, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `SELECT `+hostColumns+` FROM hosts ORDER BY hostname COLLATE NOCASE`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list hosts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Host
|
|
for rows.Next() {
|
|
var h model.Host
|
|
if err := scanHost(rows, &h); err != nil {
|
|
return nil, fmt.Errorf("scan host: %w", err)
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Hosts) Get(ctx context.Context, id int64) (*model.Host, error) {
|
|
row := s.DB.QueryRowContext(ctx, `SELECT `+hostColumns+` FROM hosts WHERE id = ?`, id)
|
|
var h model.Host
|
|
if err := scanHost(row, &h); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get host: %w", err)
|
|
}
|
|
return &h, nil
|
|
}
|
|
|
|
func (s *Hosts) GetByMAC(ctx context.Context, mac string) (*model.Host, error) {
|
|
row := s.DB.QueryRowContext(ctx, `SELECT `+hostColumns+` FROM hosts WHERE mac = ?`, normalizeMAC(mac))
|
|
var h model.Host
|
|
if err := scanHost(row, &h); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get host by mac: %w", err)
|
|
}
|
|
return &h, nil
|
|
}
|
|
|
|
func (s *Hosts) UpdateState(ctx context.Context, id int64, state model.HostState) error {
|
|
res, err := s.DB.ExecContext(ctx, `UPDATE hosts SET state = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, state, id)
|
|
if err != nil {
|
|
return fmt.Errorf("update host state: %w", err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Hosts) UpdateIP(ctx context.Context, id int64, ip string, hardwareID string) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE hosts SET ip_address = ?, hardware_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, ip, hardwareID, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Hosts) UpdateInfraID(ctx context.Context, id int64, infraHostID int64) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE hosts SET infra_host_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, infraHostID, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Hosts) SetEphemeralKey(ctx context.Context, id int64, privateKey, publicKey string) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE hosts SET ssh_private_key = ?, ssh_public_key = ? WHERE id = ?`, privateKey, publicKey, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Hosts) GetEphemeralKey(ctx context.Context, id int64) (privateKey, publicKey string, err error) {
|
|
var priv, pub sql.NullString
|
|
err = s.DB.QueryRowContext(ctx, `SELECT ssh_private_key, ssh_public_key FROM hosts WHERE id = ?`, id).Scan(&priv, &pub)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return priv.String, pub.String, nil
|
|
}
|
|
|
|
func (s *Hosts) ClearEphemeralKey(ctx context.Context, id int64) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE hosts SET ssh_private_key = NULL, ssh_public_key = NULL WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Hosts) Delete(ctx context.Context, id int64) error {
|
|
res, err := s.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))
|
|
}
|