package signedjobs import ( "context" "encoding/json" "fmt" "log/slog" "gitea.dooplex.hu/admin/felhom-agent/internal/storage" ) // opStorageWipe is the op class this executor serves (mirrors reconcile.ClassStorageWipe; the // literal avoids importing reconcile here just for the string). const opStorageWipe = "storage_wipe" // WipeOps is the privileged storage surface the wipe executor needs (a subset of storage.HostOps). type WipeOps interface { InspectDevice(ctx context.Context, device string) (storage.DeviceProbe, error) Format(ctx context.Context, device, fstype string) error } // wipeParams is the verified params of a storage_wipe op. The device is named by a DURABLE id // (never a mutable /dev path), so execution re-resolves it to the exact physical device. type wipeParams struct { DurableID string `json:"durable_id"` FSType string `json:"fstype"` } // WipeExecutor is the slice-10B storage-wipe consumer — it CLOSES the 8C data-bearing-format // `pending_signature` gap. Given a gate-VERIFIED+bound storage_wipe op, it: // 1. resolves the op's DURABLE device id → the current /dev path (vanished/replaced → refuse); // 2. re-derives that device's durable id and requires it to MATCH the signed id (resource-level // anti-retarget — "wipe device X" wipes exactly X, not whatever is at /dev/sdb now); // 3. re-inspects (8C classifier) to confirm it is STILL the data-bearing target; // 4. mkfs. // // A path-only binding, a device that vanished/changed, or a target that is no longer data-bearing // is refused EVEN WITH a valid signature. The gate has already burned the nonce durably before we // run, so an interrupted wipe can't be re-authorized by replaying the blob. type WipeExecutor struct { ops WipeOps resolve func(durableID string) (string, error) derive func(device string) (string, error) logger *slog.Logger } // NewWipeExecutor wires the production durable resolver/deriver (storage package). func NewWipeExecutor(ops WipeOps, logger *slog.Logger) *WipeExecutor { if logger == nil { logger = slog.Default() } return &WipeExecutor{ ops: ops, resolve: storage.ResolveDurableDevice, derive: storage.DeviceDurableID, logger: logger, } } // Execute implements signedjobs.Executor for the storage_wipe op class. func (w *WipeExecutor) Execute(ctx context.Context, op string, params json.RawMessage) error { if op != opStorageWipe { return ErrNoExecutor // not ours — the runner leaves it queued for the owning slice } var p wipeParams if err := json.Unmarshal(params, &p); err != nil { return fmt.Errorf("wipe: bad params: %w", err) } if p.DurableID == "" { // The whole point of the durable binding: a path-only op is refused (anti-retarget). return fmt.Errorf("wipe: op has no durable_id — refusing a path-only wipe binding") } // 1. Resolve the durable id → current device (gone/replaced → refuse). device, err := w.resolve(p.DurableID) if err != nil { return fmt.Errorf("wipe: durable id %q no longer resolves (device removed/replaced?) — refusing: %w", p.DurableID, err) } // 2. Anti-retarget: re-derive the resolved device's durable id; it MUST equal the signed id. got, err := w.derive(device) if err != nil { return fmt.Errorf("wipe: cannot re-derive durable id for %s — refusing: %w", device, err) } if got != p.DurableID { return fmt.Errorf("wipe: durable-id mismatch — %s now has id %q, signed id was %q — refusing", device, got, p.DurableID) } // 3. Re-inspect (8C classifier) — confirm the resolved device is STILL the data-bearing target. probe, err := w.ops.InspectDevice(ctx, device) if err != nil { return fmt.Errorf("wipe: re-inspect %s failed — refusing: %w", device, err) } if !probe.Probed { return fmt.Errorf("wipe: %s did not probe cleanly at execution — refusing", device) } if !probe.DataBearing() { // The signed op authorized wiping a DATA-BEARING device; if it is now blank, the target // changed since signing — refuse rather than wipe the wrong (or an unexpected) device. return fmt.Errorf("wipe: %s is no longer data-bearing (target changed since signing) — refusing", device) } // 4. Execute. The nonce was durably burned by the gate's Verify BEFORE this point (crash-safety: // a replay after an interrupted wipe is rejected). This log line is the audit trail of a // destructive, operator-authorized wipe actually running. w.logger.Warn("wipe: executing operator-signed data-bearing wipe", "device", device, "durable_id", p.DurableID, "fstype", p.FSType, "data_reason", probe.Reason()) if err := w.ops.Format(ctx, device, p.FSType); err != nil { return fmt.Errorf("wipe: mkfs %s %s: %w", p.FSType, device, err) } w.logger.Warn("wipe: operator-signed wipe complete", "device", device, "durable_id", p.DurableID) return nil }