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
95 lines
3.6 KiB
Go
95 lines
3.6 KiB
Go
package signedjobs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/localapi"
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
|
|
)
|
|
|
|
// add_internal_place (felhom.eu architecture/16 §9, agent v0.158.0): the operator's job for an OLD one-disk box.
|
|
|
|
type ipRunner struct {
|
|
out, stderr string
|
|
err error
|
|
calls [][]string
|
|
}
|
|
|
|
func (r *ipRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
|
|
r.calls = append(r.calls, append([]string{name}, args...))
|
|
return []byte(r.out), []byte(r.stderr), r.err
|
|
}
|
|
func (r *ipRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
|
return r.Run(ctx, name, args...)
|
|
}
|
|
|
|
func ipExec(t *testing.T, r *ipRunner) (*InternalPlaceExecutor, string, *int) {
|
|
t.Helper()
|
|
file := filepath.Join(t.TempDir(), localapi.InternalPlaceFile)
|
|
e := NewInternalPlaceExecutor(r, file, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
n := 0
|
|
e.SetEnsure(func(context.Context) { n++ })
|
|
return e, file, &n
|
|
}
|
|
|
|
func TestAddInternalPlace_RunsTheHelperWritesTheRecordAndMounts(t *testing.T) {
|
|
r := &ipRunner{out: "created uuid=0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c gib=33\n"}
|
|
e, file, ensured := ipExec(t, r)
|
|
if err := e.Execute(context.Background(), "add_internal_place", []byte(`{}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(r.calls) != 1 || strings.Join(r.calls[0], " ") != InternalPlaceHelper+" create 0" {
|
|
t.Fatalf("the helper must be called exactly as `create 0`, got %v", r.calls)
|
|
}
|
|
rec, ok, err := localapi.ReadInternalPlace(file)
|
|
if err != nil || !ok || rec.FSUUID != "0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c" || rec.GiB != 33 || rec.MadeBy != "signed-job" {
|
|
t.Fatalf("record: %+v ok=%v err=%v", rec, ok, err)
|
|
}
|
|
if *ensured != 1 {
|
|
t.Fatalf("the mount must be asked for once, got %d", *ensured)
|
|
}
|
|
}
|
|
|
|
func TestAddInternalPlace_NoRoomOrRefusalOrGarbage_WritesNothing(t *testing.T) {
|
|
for name, r := range map[string]*ipRunner{
|
|
"skipped": {out: "skipped only 7 GiB free for it (pool 53, reserve 16, promised 102, planned 0; needs 20)"},
|
|
"refused": {err: errors.New("exit status 1"), stderr: "felhom-internal-place: REFUSED: holds something"},
|
|
"garbage": {out: "created uuid=../../etc gib=1"},
|
|
} {
|
|
e, file, ensured := ipExec(t, r)
|
|
if err := e.Execute(context.Background(), "add_internal_place", nil); err == nil {
|
|
t.Fatalf("%s: want an error", name)
|
|
}
|
|
if _, statErr := os.Stat(file); !os.IsNotExist(statErr) || *ensured != 0 {
|
|
t.Fatalf("%s: no record and no mount may follow, stat=%v ensured=%d", name, statErr, *ensured)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddInternalPlace_TakesNoParametersAndIgnoresOtherOps(t *testing.T) {
|
|
r := &ipRunner{out: "created uuid=0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c gib=33"}
|
|
e, _, _ := ipExec(t, r)
|
|
if err := e.Execute(context.Background(), "add_internal_place", []byte(`{"gib":900}`)); err == nil || len(r.calls) != 0 {
|
|
t.Fatalf("a size from the signer must be refused before the helper runs, err=%v calls=%v", err, r.calls)
|
|
}
|
|
if err := e.Execute(context.Background(), "decommission", []byte(`{}`)); !errors.Is(err, ErrNoExecutor) {
|
|
t.Fatalf("another op: want ErrNoExecutor, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddInternalPlace_IsSignatureOnly(t *testing.T) {
|
|
if got := reconcile.Classify(reconcile.ClassAddInternalPlace, reconcile.Provenance{}); got != reconcile.Destructive {
|
|
t.Fatalf("add_internal_place must need an operator signature, classified %v", got)
|
|
}
|
|
if string(reconcile.ClassAddInternalPlace) != opAddInternalPlace {
|
|
t.Fatalf("op string drift: %q vs %q", reconcile.ClassAddInternalPlace, opAddInternalPlace)
|
|
}
|
|
}
|