e15d1d30ea
gates / gates (push) Successful in 47s
R-1124: ForgetRetiredDrives (start-up + the 20 s tick) disables and removes the host mount unit of a DECOMMISSIONED drive whose disk is absent and unmounted; the agent then no longer lists it. A present decommissioned drive keeps its unit (one-click re-enroll). The file goes only through the new felhom-priv-apply verb `unit-forget`, which re-checks marker, disabled, unmounted and absent as root. R-1012: the read-only root helper felhom-thin-check <vg> <pool> (bundle) lists the pool's thin ids (metadata snapshot reserved and released) next to LVM's before and after. BackupRunner.SetThinCheck runs it before a snapshot-mode backup (log) and again after a failure, appending the finding in plain words to the error the controller mails to the operator. Two reads, intersected; fails open; never removes. Red-proofs: felhom.eu audits/agent-allowlist-rotation-2026-10-17/P1/. Code only; the CHANGELOG entry follows the release. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
156 lines
6.1 KiB
Go
156 lines
6.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// forgetRunner records every privileged call and plays the root half of `felhom-priv-apply unit-forget`: the unit file
|
|
// disappears from the unit dir, so the CONSEQUENCE (the drive is no longer listed) is observable, not just the calls.
|
|
type forgetRunner struct {
|
|
unitDir string
|
|
calls [][]string
|
|
}
|
|
|
|
func (r *forgetRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
|
return r.Run(ctx, name, args...)
|
|
}
|
|
|
|
func (r *forgetRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
|
|
r.calls = append(r.calls, append([]string{name}, args...))
|
|
if len(args) == 2 && args[0] == "unit-forget" {
|
|
_ = os.Remove(filepath.Join(r.unitDir, args[1]))
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
type forgetCase struct {
|
|
intent DriveIntent
|
|
present bool // the by-uuid link resolves
|
|
mounted bool
|
|
enabled bool // the wants-symlink exists
|
|
mountErr bool
|
|
}
|
|
|
|
const forgetUUID = "4ebfd36d-87d3-4b63-839e-a5a710fc799a"
|
|
|
|
func runForget(t *testing.T, c forgetCase, intent IntentReader) (int, *forgetRunner, *RegistryKnownTargets, string) {
|
|
t.Helper()
|
|
unitDir := t.TempDir()
|
|
unit := "mnt-media_a.mount"
|
|
writeUnit(t, unitDir, unit, renderMountUnit(MountSpec{Name: forgetUUID, UUID: forgetUUID, Where: "/mnt/media_a", FSType: "ext4"}))
|
|
if c.enabled {
|
|
if err := os.MkdirAll(filepath.Join(unitDir, "multi-user.target.wants"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(unitDir, unit), filepath.Join(unitDir, "multi-user.target.wants", unit)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
devRoot := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(devRoot, "by-uuid"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.present {
|
|
dev := filepath.Join(devRoot, "sdz1")
|
|
if err := os.WriteFile(dev, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(dev, filepath.Join(devRoot, "by-uuid", forgetUUID)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
withDevDiskRoot(t, devRoot)
|
|
hr := &fakeHostReader{}
|
|
if c.mounted {
|
|
hr.mounts = []Mount{{MountPoint: "/mnt/media_a", FSType: "ext4"}}
|
|
}
|
|
if c.mountErr {
|
|
hr.mountsErr = os.ErrPermission
|
|
}
|
|
rr := &forgetRunner{unitDir: unitDir}
|
|
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir,
|
|
StageDir: t.TempDir(), Host: hr, Logger: quietLogger()})
|
|
if intent == nil {
|
|
intent = &fakeIntent{m: map[string]DriveIntent{"uuid:" + forgetUUID: c.intent}}
|
|
}
|
|
n := ops.ForgetRetiredDrives(context.Background(), intent)
|
|
return n, rr, NewRegistryKnownTargets(unitDir, intent, quietLogger()), unitDir
|
|
}
|
|
|
|
// The R-1124 case: decommissioned, then unplugged. The unit is disabled and removed through the root checker, systemd
|
|
// is reloaded, and the CONSEQUENCE — the agent no longer lists the drive — holds. RED-PROOF (P1/redproof-forget.txt):
|
|
// with ForgetRetiredDrives' body replaced by `return 0` this test fails (the drive is still listed).
|
|
func TestForgetRetiredDrives_DecommissionedAndGone_UnitRemovedAndNotListed(t *testing.T) {
|
|
n, rr, known, _ := runForget(t, forgetCase{intent: IntentDecommissioned, enabled: true}, nil)
|
|
if n != 1 {
|
|
t.Fatalf("removed = %d, want 1 (calls %v)", n, rr.calls)
|
|
}
|
|
pa := Binaries{}.withDefaults()
|
|
want := [][]string{
|
|
{pa.Systemctl, "disable", "--", "mnt-media_a.mount"},
|
|
{pa.PrivApply, "unit-forget", "mnt-media_a.mount"},
|
|
{pa.Systemctl, "daemon-reload"},
|
|
}
|
|
if !reflect.DeepEqual(rr.calls, want) {
|
|
t.Fatalf("calls = %v\nwant %v", rr.calls, want)
|
|
}
|
|
got, err := known.Known(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("the forgotten drive is still listed: %+v", got)
|
|
}
|
|
}
|
|
|
|
// Already disabled (the Tester 1 shape after a manual disable): no disable call, the removal still runs.
|
|
func TestForgetRetiredDrives_AlreadyDisabled_NoDisableCall(t *testing.T) {
|
|
n, rr, _, _ := runForget(t, forgetCase{intent: IntentDecommissioned}, nil)
|
|
pa := Binaries{}.withDefaults()
|
|
want := [][]string{{pa.PrivApply, "unit-forget", "mnt-media_a.mount"}, {pa.Systemctl, "daemon-reload"}}
|
|
if n != 1 || !reflect.DeepEqual(rr.calls, want) {
|
|
t.Fatalf("n=%d calls=%v want %v", n, rr.calls, want)
|
|
}
|
|
}
|
|
|
|
// Everything that must KEEP its unit: a decommissioned drive still plugged in (re-enroll is one click — H3), one still
|
|
// mounted, an enrolled or ejected drive that is merely absent (it comes back), a drive with no recorded intent, and an
|
|
// unreadable mount table. No privileged call at all, and the drive stays listed where it was listed.
|
|
// RED-PROOF: with the present-drive check removed, the "present" case calls unit-forget and fails.
|
|
func TestForgetRetiredDrives_KeepsEveryOtherUnit(t *testing.T) {
|
|
for name, c := range map[string]forgetCase{
|
|
"decommissioned but present": {intent: IntentDecommissioned, present: true, enabled: true},
|
|
"decommissioned but mounted": {intent: IntentDecommissioned, mounted: true},
|
|
"enrolled and absent": {intent: IntentEnrolled},
|
|
"ejected and absent": {intent: IntentEjected},
|
|
"no intent": {intent: IntentNew},
|
|
"mount table unreadable": {intent: IntentDecommissioned, mountErr: true},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
n, rr, _, unitDir := runForget(t, c, nil)
|
|
if n != 0 || len(rr.calls) != 0 {
|
|
t.Fatalf("n=%d calls=%v — this unit must be kept", n, rr.calls)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(unitDir, "mnt-media_a.mount")); err != nil {
|
|
t.Fatalf("unit file gone: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// No intent store on the host (open failure): nothing is known to be retired, so nothing is forgotten.
|
|
func TestForgetRetiredDrives_NilIntent_ForgetsNothing(t *testing.T) {
|
|
unitDir := t.TempDir()
|
|
writeUnit(t, unitDir, "mnt-media_a.mount", renderMountUnit(MountSpec{Name: "x", UUID: forgetUUID, Where: "/mnt/media_a"}))
|
|
rr := &forgetRunner{unitDir: unitDir}
|
|
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, UnitDir: unitDir, StageDir: t.TempDir(), Host: &fakeHostReader{}, Logger: quietLogger()})
|
|
if n := ops.ForgetRetiredDrives(context.Background(), nil); n != 0 || len(rr.calls) != 0 {
|
|
t.Fatalf("n=%d calls=%v", n, rr.calls)
|
|
}
|
|
}
|