v0.141.0: F6 initialize-to-usable (detached crash-safe init job + status poll) + F7 Vissza back-routes

- F6: POST /api/storage/init runs format→mount→register as a DETACHED single-flight job (context.Background, netAddState shape) the wizard polls via GET /api/storage/init/status; 3-step progress; register-last marker-last crash-safety. Fixes the client-disconnect-aborts-mount bug. No agent change (chain reaches FileBrowser sync = controller-only). Red-proof TestStorageInit_DetachedSurvivesClientDisconnect.
- F7: storage_init/attach Vissza → /storage (was /settings). Test TestStorageWizardBackAnchors_PointToStorage.
This commit is contained in:
2026-07-17 09:20:59 +02:00
parent 5be2449267
commit 0025a3b090
7 changed files with 365 additions and 38 deletions
+37 -11
View File
@@ -108,13 +108,19 @@ type storageInitResult struct {
// UUID → assign → register. A USER-DATA data-bearing device requires the customer's confirmation
// (NeedsConfirmation); a SYSTEM/BACKUP device requires an operator signature (Refused+Opsign). In
// either refusal it performs NO further (destructive or mount) action.
func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fstype, where, label string, setDefault, confirmed bool, durableID string) (storageInitResult, error) {
func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fstype, where, label string, setDefault, confirmed bool, durableID string, progress func(string)) (storageInitResult, error) {
setP := func(phase string) {
if progress != nil {
progress(phase)
}
}
if !validFSTypes[fstype] {
return storageInitResult{}, fmt.Errorf("nem támogatott fájlrendszer: %q (ext4 vagy xfs)", fstype)
}
// 1. Format — the AGENT inspects the device and tiers it by role. A data-bearing user-data device
// is allowed only with the customer's confirmation bound to its durable id; system/backup needs
// an operator signature.
setP(storageInitPhaseFormatting)
fr, err := agent.FormatDisk(ctx, device, fstype, confirmed, durableID)
if errors.Is(err, agentapi.ErrNeedsConfirmation) {
// USER-DATA: surface the type-to-confirm requirement + the durable id to confirm against.
@@ -135,6 +141,7 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs
}
// 2. Resolve the NEW fs UUID. A freshly-formatted RAW device isn't in /disks (not enrolled yet), so
// resolve via the raw-device scan too (Impl-2b) — the device now appears there with its new durable_id.
setP(storageInitPhaseMounting)
uuid := resolveEnrollUUID(ctx, agent, device)
if uuid == "" {
return storageInitResult{}, fmt.Errorf("formázás kész, de az új fájlrendszer-azonosító nem feloldható — frissítsen és használja a Csatolás funkciót")
@@ -147,6 +154,10 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs
return storageInitResult{}, fmt.Errorf("csatlakoztatás sikertelen: %w", err)
}
s.attachIntoGuest(ctx, agent, where)
// 5. Register the stable path — THE LAST step (marker-last, F6/Scenario B): a crash before this
// leaves at most a mounted-but-unregistered drive (surfaced by the list), never a broken
// registration, and AddStoragePath dedups a re-run so there is never a duplicate entry.
setP(storageInitPhaseRegistering)
stable := stablePathForName(path.Base(where))
if err := s.registerStoragePath(stable, label, setDefault); err != nil {
return storageInitResult{}, err
@@ -273,6 +284,8 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/api/storage/init" && r.Method == http.MethodPost:
s.handleStorageInit(w, r)
case r.URL.Path == "/api/storage/init/status" && r.Method == http.MethodGet:
s.handleStorageInitStatus(w, r)
case r.URL.Path == "/api/storage/attach" && r.Method == http.MethodPost:
s.handleStorageAttach(w, r)
case r.URL.Path == "/api/storage/eject" && r.Method == http.MethodPost:
@@ -527,20 +540,33 @@ func (s *Server) handleStorageInit(w http.ResponseWriter, r *http.Request) {
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
return
}
res, err := s.runStorageInit(r.Context(), agent, req.Device, req.FSType, where, req.Label, req.SetDefault, req.Confirmed, req.DurableID)
if err != nil {
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
// F6: run format→mount→register as a DETACHED job (off the request context) the wizard polls via
// /api/storage/init/status. A closed tab / lost connection can no longer abort the post-mkfs
// mount+register leg. The confirm/refuse verdicts (fast, pre-mkfs) surface through the same poll.
started := s.startStorageInit(agent, storageInitParams{
device: req.Device, fstype: req.FSType, where: where, label: req.Label,
setDefault: req.SetDefault, confirmed: req.Confirmed, durableID: req.DurableID,
})
if !started {
writeDiskJSON(w, http.StatusConflict, false, "már folyamatban van egy meghajtó-inicializálás", nil)
return
}
if res.NeedsConfirmation {
writeDiskJSON(w, http.StatusConflict, false, "ügyfél-megerősítés szükséges", res)
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"started": true, "phase": storageInitPhaseFormatting})
}
// handleStorageInitStatus reports the last / in-flight drive-init job (deep copy; "idle" when never
// ran). The wizard polls this for the 3-step progress + the confirm/refuse verdicts (F6).
func (s *Server) handleStorageInitStatus(w http.ResponseWriter, r *http.Request) {
job := s.storageInit.snapshot()
if job == nil {
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"phase": "idle"})
return
}
if res.Refused {
writeDiskJSON(w, http.StatusConflict, false, "operátori aláírás szükséges", res)
return
}
writeDiskJSON(w, http.StatusOK, true, "", res)
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{
"phase": job.Phase, "device": job.Device, "where": job.Where, "error": job.Error,
"reason": job.Reason, "durable_id": job.DurableID, "opsign": job.Opsign,
"started_at": job.StartedAt, "updated_at": job.UpdatedAt,
})
}
// storageImpactReq / handleStorageImpact return the deployed apps whose data lives on a given mount —