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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user