From 2064f32199da7572bd4f502802aac84fc995fd8d Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 17 Feb 2026 11:38:52 +0100 Subject: [PATCH] fix(storage): fix FormatAndMount for container environment Bug 1 (sfdisk): Add wipefs before sfdisk; change partition type from ,,,L (unsupported GPT shorthand) to ,, (default Linux GUID); add --force --wipe always flags to handle existing partition tables. Bug 2 (mount): Replace fstab-lookup mount with explicit device path: mount -t ext4 -o defaults,noatime /host-dev/sdb1 /mnt/hdd_1 Container's /etc/fstab is Docker's auto-generated one, not the host's. Bug 3 (mount propagation): Change /mnt volume to long-form bind with propagation: rshared so mounts created inside container propagate to the host. Requires mount --make-rshared /mnt on host before restart. Safety: Use req.MountName (ASCII) for ext4 -L label (16-byte limit; UTF-8 display label stays in settings.json). Add findmnt verification after mount. Improve progress messages with command details. Smart partition: In storageInitAPIHandler, if disk already has exactly 1 empty partition (no filesystem), skip wipefs+sfdisk and format the existing partition directly. Co-Authored-By: Claude Sonnet 4.5 --- controller/docker-compose.yml | 9 +++- controller/internal/storage/format_linux.go | 46 ++++++++++++++------- controller/internal/web/storage_handlers.go | 16 +++++++ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/controller/docker-compose.yml b/controller/docker-compose.yml index 82b47d5..c6579a8 100644 --- a/controller/docker-compose.yml +++ b/controller/docker-compose.yml @@ -22,8 +22,13 @@ services: - /opt/docker/stacks:/opt/docker/stacks # Backup directories (restic repo + db dumps) - /srv/backups:/srv/backups - # All external storage — /mnt/* for multi-storage + restore - - /mnt:/mnt:rw + # All external storage — rshared propagation so mounts created inside + # the container (disk init) propagate to the host and vice versa + - type: bind + source: /mnt + target: /mnt + bind: + propagation: rshared # Host /sys — for CPU temperature reading (read-only) - /sys:/host/sys:ro # Host OS info — for monitoring page system info diff --git a/controller/internal/storage/format_linux.go b/controller/internal/storage/format_linux.go index b0fd616..886c0a5 100644 --- a/controller/internal/storage/format_linux.go +++ b/controller/internal/storage/format_linux.go @@ -69,10 +69,18 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, // --- Step 2: Partition (if requested) --- partDev := req.DevicePath if req.CreatePartition { - send("partitioning", "Partíció létrehozása...", 15) + // Wipe existing partition table and filesystem signatures first + send("partitioning", fmt.Sprintf("wipefs -a %s ...", HostDevicePath(req.DevicePath)), 12) + _ = exec.Command("wipefs", "-a", HostDevicePath(req.DevicePath)).Run() + time.Sleep(500 * time.Millisecond) - sfdiskInput := "label: gpt\n,,,L\n" - cmd := exec.Command("sfdisk", HostDevicePath(req.DevicePath)) + // Create GPT with single partition spanning whole disk. + // ",," = start=default, size=default(fill disk), type=default(Linux filesystem GUID). + // --force: overwrite even if device appears busy. + // --wipe always: wipe filesystem signatures from newly created partitions. + send("partitioning", fmt.Sprintf("sfdisk --force --wipe always %s ...", HostDevicePath(req.DevicePath)), 15) + sfdiskInput := "label: gpt\n,,\n" + cmd := exec.Command("sfdisk", "--force", "--wipe", "always", HostDevicePath(req.DevicePath)) cmd.Stdin = strings.NewReader(sfdiskInput) if out, err := cmd.CombinedOutput(); err != nil { return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err) @@ -93,17 +101,15 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, } // --- Step 3: Format --- - send("formatting", "Fájlrendszer formázása (ext4)...", 30) - - label := req.Label - if label == "" { - label = req.MountName - } - if len(label) > 16 { - label = label[:16] + // Use ASCII-safe mount name for ext4 filesystem label (16-byte limit). + // The display label (req.Label) stays in settings.json for the UI. + fsLabel := req.MountName + if len(fsLabel) > 16 { + fsLabel = fsLabel[:16] } - mkfsCmd := exec.Command("mkfs.ext4", "-L", label, "-F", HostDevicePath(partDev)) + send("formatting", fmt.Sprintf("mkfs.ext4 -L %s -F %s ...", fsLabel, HostDevicePath(partDev)), 30) + mkfsCmd := exec.Command("mkfs.ext4", "-L", fsLabel, "-F", HostDevicePath(partDev)) var mkfsOut bytes.Buffer mkfsCmd.Stdout = &mkfsOut mkfsCmd.Stderr = &mkfsOut @@ -114,12 +120,11 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, send("formatting", "Formázás kész", 60) // --- Step 4: Mount --- - send("mounting", "Csatlakoztatás: "+mountPath+"...", 65) - if err := os.MkdirAll(mountPath, 0755); err != nil { return "", fail("mounting", "Csatlakoztatási mappa nem hozható létre: "+mountPath, err) } + send("mounting", fmt.Sprintf("UUID lekérése: blkid %s ...", HostDevicePath(partDev)), 65) uuidOut, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(partDev)).Output() if err != nil { return "", fail("mounting", "UUID lekérése sikertelen", err) @@ -136,10 +141,21 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, return "", fail("mounting", "fstab bejegyzés hozzáadása sikertelen", err) } - if out, err := exec.Command("mount", mountPath).CombinedOutput(); err != nil { + // Mount by device path explicitly — container's /etc/fstab != host fstab, + // so "mount /mnt/hdd_1" (fstab lookup) won't work. + send("mounting", fmt.Sprintf("mount -t ext4 %s %s ...", HostDevicePath(partDev), mountPath), 70) + if out, err := exec.Command("mount", "-t", "ext4", "-o", "defaults,noatime", + HostDevicePath(partDev), mountPath).CombinedOutput(); err != nil { return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err) } + // Verify mount actually worked (don't just trust exit code) + verifyOut, verifyErr := exec.Command("findmnt", "-n", "-o", "SOURCE", "--target", mountPath).Output() + if verifyErr != nil || strings.TrimSpace(string(verifyOut)) == "" { + return "", fail("mounting", "A csatlakoztatás nem ellenőrizhető: mount sikerült, de a meghajtó nem látható", + fmt.Errorf("mount point %s not found after mount", mountPath)) + } + send("mounting", "Csatlakoztatva: "+mountPath, 80) // --- Step 5: Permissions + subdirs --- diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index a943de5..d77c6d2 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -201,6 +201,22 @@ func (s *Server) storageInitAPIHandler(w http.ResponseWriter, r *http.Request) { SetDefault: req.SetDefault, } + // Smart partition: if disk has exactly 1 partition with no filesystem, + // skip destructive repartitioning and format the existing partition directly. + if fmtReq.CreatePartition { + if scanResult, scanErr := storage.ScanDisks(); scanErr == nil { + for _, disk := range scanResult.AvailableDisks { + if disk.Path == req.DevicePath && len(disk.Partitions) == 1 && disk.Partitions[0].FSType == "" { + s.logger.Printf("[INFO] Disk %s has 1 empty partition (%s) — skipping repartition", + req.DevicePath, disk.Partitions[0].Path) + fmtReq.DevicePath = disk.Partitions[0].Path + fmtReq.CreatePartition = false + break + } + } + } + } + go func() { progressCh := make(chan storage.FormatProgress, 32) // Collect progress