912f7c127e
gates / gates (push) Successful in 27s
- internal place: the record /var/lib/felhom-agent/internal-place.json -> mounted at /mnt/belso by fs-UUID like an enrolled drive (startup + 20 s tick), kind "internal" in GET /disks; never takes a household drive's /mnt/belso; eject/decommission/format refused (dm on the system disk). - configs/felhom-internal-place: the ruled size rule and the only lvcreate/mkfs of a system-disk volume (installer + the new signed op add_internal_place, operator key, no parameters). - partitions listed per free disk (empty / filesystem / in_use); a data-bearing part's wipe id binds disk id + part number + fs-UUID. - felhom-mkfs-guarded: Felhom's own sibling drive no longer blocks formatting the free part; an unlistable disk refuses. Tests red-proved (script mutations, Go mutations, the old wrapper measured against the new tests). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
92 lines
4.0 KiB
Go
92 lines
4.0 KiB
Go
package signedjobs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/localapi"
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
|
)
|
|
|
|
// opAddInternalPlace is the op class this executor serves (mirrors reconcile.ClassAddInternalPlace).
|
|
const opAddInternalPlace = "add_internal_place"
|
|
|
|
// InternalPlaceHelper is the root helper's path (configs/felhom-internal-place, from the config bundle).
|
|
const InternalPlaceHelper = "/usr/local/sbin/felhom-internal-place"
|
|
|
|
var reHelperResult = regexp.MustCompile(`^(created|existing|formatted) uuid=([0-9a-fA-F-]{36}) gib=([0-9]+)$`)
|
|
|
|
// InternalPlaceExecutor adds the internal data place to an OLD one-disk box (felhom.eu architecture/16 §9: „one-disk
|
|
// boxes get no internal place by themselves; adding one later is a signed operator job, not a household button").
|
|
// It runs the SAME root helper the installer runs — `felhom-internal-place create 0` (0 GiB planned: the guest's
|
|
// volumes already exist and are counted) — writes the record the agent mounts from, and asks the local API to mount
|
|
// it now. Every safety rule (the ruled size, never format what holds anything) lives in the helper, as root.
|
|
//
|
|
// The op carries no parameters: the size is the rule's, never the signer's. Destructive-class by default (the
|
|
// classifier's fail-safe; ClassAddInternalPlace documents it): it takes thin space on the system disk, so only an
|
|
// operator signature may start it.
|
|
type InternalPlaceExecutor struct {
|
|
runner proxmox.Runner
|
|
recordFile string
|
|
ensure func(context.Context) // late-bound: the local API server is built after the jobs runner
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewInternalPlaceExecutor wires the executor. ensure may be nil at construction and set later through SetEnsure.
|
|
func NewInternalPlaceExecutor(runner proxmox.Runner, recordFile string, logger *slog.Logger) *InternalPlaceExecutor {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &InternalPlaceExecutor{runner: runner, recordFile: recordFile, logger: logger}
|
|
}
|
|
|
|
// SetEnsure late-binds the mount step (localapi.Server.EnsureInternalPlace).
|
|
func (e *InternalPlaceExecutor) SetEnsure(f func(context.Context)) { e.ensure = f }
|
|
|
|
// Execute implements signedjobs.Executor for add_internal_place.
|
|
func (e *InternalPlaceExecutor) Execute(ctx context.Context, op string, params json.RawMessage) error {
|
|
if op != opAddInternalPlace {
|
|
return ErrNoExecutor
|
|
}
|
|
if p := bytes.TrimSpace(params); len(p) > 0 && !bytes.Equal(p, []byte("{}")) && !bytes.Equal(p, []byte("null")) {
|
|
return fmt.Errorf("add_internal_place: takes no parameters (the size is the ruled rule's), got %s", p)
|
|
}
|
|
if e.runner == nil || e.recordFile == "" {
|
|
return fmt.Errorf("add_internal_place: not wired on this host (no privileged runner or state dir)")
|
|
}
|
|
start := time.Now()
|
|
e.logger.Warn("add_internal_place: executing operator-signed job (the internal data place, `16` §9)")
|
|
out, stderr, err := e.runner.Run(ctx, InternalPlaceHelper, "create", "0")
|
|
line := strings.TrimSpace(string(out))
|
|
if err != nil {
|
|
return fmt.Errorf("add_internal_place: helper refused: %v: %s", err, strings.TrimSpace(string(stderr)))
|
|
}
|
|
if strings.HasPrefix(line, "skipped ") {
|
|
// Not an error the operator can retry away: the pool has no room by the ruled rule. Fail the job with the
|
|
// helper's own numbers so the hub shows why.
|
|
return fmt.Errorf("add_internal_place: %s", line)
|
|
}
|
|
m := reHelperResult.FindStringSubmatch(line)
|
|
if m == nil {
|
|
return fmt.Errorf("add_internal_place: unexpected helper output %q", line)
|
|
}
|
|
gib, _ := strconv.Atoi(m[3])
|
|
rec := localapi.InternalPlaceRecord{FSUUID: m[2], GiB: gib, MadeBy: "signed-job", MadeAt: time.Now().UTC().Format(time.RFC3339)}
|
|
if err := localapi.WriteInternalPlace(e.recordFile, rec); err != nil {
|
|
return fmt.Errorf("add_internal_place: writing the record: %w", err)
|
|
}
|
|
if e.ensure != nil {
|
|
e.ensure(ctx)
|
|
}
|
|
e.logger.Warn("add_internal_place: complete", "result", m[1], "fs_uuid", m[2], "gib", gib,
|
|
"duration_ms", time.Since(start).Milliseconds())
|
|
return nil
|
|
}
|