Files
deploy-felhom-compose/controller/internal/storage/format_linux.go
T
admin 45f75a916c fix: P2+P3 bug fixes, hardening, and cleanup (18 files)
Bug fixes:
- Add applyEnvOverrides to LoadFromBytes (M05)
- Set state=failed on compose-up failure in selfupdate (M16)
- Clamp usableMB to min 0 in memory check (M22)
- Remove "manual" schedule from triggerAllCrossBackups (M23)
- Add mmcblk device handling for partition paths (M21)
- Fix stripPartition for mmcblk devices (L25)
- Fix TruncateStr for UTF-8 and negative maxLen (L05/L06)
- Fix AllDone to return false for empty restore plans (L14)
- Fix PushOnce to return actual errors (L39)
- Restore pending events on save failure in DrainPendingEvents (M03)
- Add duplicate check in AddStoragePath (M04)
- Call CleanupTempMounts after drive scan (H13)
- Log SetStep save errors (M25)

Hardening:
- Guard scheduler Start() against double-start (M14)
- Acquire mutex in scheduler Stop() before reading cancel (L24)
- Cap log lines parameter to 10000 (L31)
- Require POST for logout (L32)
- Use sync.Once for Server.Close() (L49)
- Panic on crypto/rand.Read failure in setup CSRF (L40)
- Validate Bearer token against Hub API key in CSRF (H16 fix)
- Replace custom hasPrefix with strings.HasPrefix (L13)
- Replace simpleHash with crc32.ChecksumIEEE (L48)

Cleanup:
- Remove dead imageName function (L02)
- Remove dead detectHostIPViaRoute function (L03)
- Rename shadowed copy variable to cp (L07)
- Copy DefaultEnabledEvents in GetNotificationPrefs early return (L09)
- Update BUGHUNT.md with comprehensive audit results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:47:52 +01:00

244 lines
9.6 KiB
Go

//go:build linux
package storage
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
)
// FormatAndMount formats a disk/partition and mounts it.
// Progress updates are sent on the progress channel.
// Returns the final mount path on success.
func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, error) {
send := func(step, msg string, pct int) {
progress <- FormatProgress{Step: step, Message: msg, Percent: pct}
}
fail := func(step, msg string, err error) error {
errStr := ""
if err != nil {
errStr = err.Error()
}
progress <- FormatProgress{Step: "error", Message: msg, Error: errStr, Percent: 0}
return fmt.Errorf("%s: %w", msg, err)
}
dbg := func(format string, args ...interface{}) {
if req.Logger != nil && req.Debug {
req.Logger.Printf("[DEBUG] FormatAndMount: "+format, args...)
}
}
mountPath := "/mnt/" + req.MountName
dbg("starting: device=%s mountName=%s createPartition=%v", req.DevicePath, req.MountName, req.CreatePartition)
// --- Step 1: Validate ---
send("validating", "Eszköz ellenőrzése...", 5)
if err := ValidateMountName(req.MountName); err != nil {
return "", fail("validating", "Érvénytelen csatlakoztatási név", err)
}
// C6: Validate DevicePath to prevent path traversal from user-supplied input.
if !strings.HasPrefix(req.DevicePath, "/dev/") {
return "", fail("validating", "Érvénytelen eszközútvonal: /dev/-vel kell kezdődnie", fmt.Errorf("invalid device path: must start with /dev/"))
}
if strings.Contains(req.DevicePath, "..") {
return "", fail("validating", "Érvénytelen eszközútvonal: nem tartalmazhat ..-t", fmt.Errorf("invalid device path: must not contain .."))
}
if _, err := os.Stat(HostDevicePath(req.DevicePath)); err != nil {
return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err)
}
isSystem, err := IsSystemDisk(req.DevicePath)
if err != nil {
return "", fail("validating", "Rendszermeghajtó ellenőrzése sikertelen", err)
}
if isSystem {
return "", fail("validating", "Ez a rendszermeghajtó — nem formázható!", fmt.Errorf("device is system disk"))
}
mounted, err := IsDeviceMounted(req.DevicePath)
if err != nil {
return "", fail("validating", "Csatlakoztatási állapot ellenőrzése sikertelen", err)
}
if mounted {
return "", fail("validating", "Az eszköz már csatlakoztatva van", fmt.Errorf("device already mounted"))
}
inUse, err := IsMountPathInUse(mountPath)
if err != nil {
return "", fail("validating", "Csatlakoztatási útvonal ellenőrzése sikertelen", err)
}
if inUse {
return "", fail("validating", "A csatlakoztatási útvonal már használatban van: "+mountPath, fmt.Errorf("mount path in use"))
}
send("validating", "Ellenőrzés kész", 10)
// --- Step 2: Partition (if requested) ---
partDev := req.DevicePath
if req.CreatePartition {
// Wipe existing partition table and filesystem signatures first
// H18: Log wipefs errors instead of silently discarding them.
dbg("step wipefs: wipefs -a %s", HostDevicePath(req.DevicePath))
send("partitioning", fmt.Sprintf("wipefs -a %s ...", HostDevicePath(req.DevicePath)), 12)
if err := exec.Command("wipefs", "-a", HostDevicePath(req.DevicePath)).Run(); err != nil {
// Non-fatal: some systems don't have wipefs; continue anyway
dbg("wipefs failed (non-fatal): %v", err)
send("partitioning", fmt.Sprintf("[WARN] wipefs sikertelen %s: %v (folytatás)", req.DevicePath, err), 13)
} else {
dbg("wipefs completed successfully")
}
time.Sleep(500 * time.Millisecond)
// 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.
dbg("step sfdisk: sfdisk --force --wipe always %s", HostDevicePath(req.DevicePath))
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 {
dbg("sfdisk failed: %s", util.TruncateStr(string(out), 500))
return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err)
} else {
dbg("sfdisk output: %s", util.TruncateStr(string(out), 500))
}
_ = exec.Command("partprobe", HostDevicePath(req.DevicePath)).Run()
time.Sleep(2 * time.Second)
partDev = req.DevicePath + "1"
if strings.Contains(req.DevicePath, "nvme") || strings.Contains(req.DevicePath, "mmcblk") {
partDev = req.DevicePath + "p1"
}
if _, err := os.Stat(HostDevicePath(partDev)); err != nil {
return "", fail("partitioning", "Partíció nem található a létrehozás után: "+partDev, err)
}
send("partitioning", "Partíció létrehozva: "+partDev, 25)
}
// --- Step 3: Format ---
// 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]
}
dbg("step mkfs.ext4: mkfs.ext4 -L %s -F %s", fsLabel, 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
if err := mkfsCmd.Run(); err != nil {
dbg("mkfs.ext4 failed: %s", util.TruncateStr(mkfsOut.String(), 500))
return "", fail("formatting", "Formázás sikertelen: "+mkfsOut.String(), err)
}
dbg("mkfs.ext4 output: %s", util.TruncateStr(mkfsOut.String(), 500))
send("formatting", "Formázás kész", 60)
// --- Step 4: Mount ---
if err := os.MkdirAll(mountPath, 0755); err != nil {
return "", fail("mounting", "Csatlakoztatási mappa nem hozható létre: "+mountPath, err)
}
dbg("step blkid: blkid -s UUID -o value %s", HostDevicePath(partDev))
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 {
dbg("blkid UUID failed: %v", err)
return "", fail("mounting", "UUID lekérése sikertelen", err)
}
uuid := strings.TrimSpace(string(uuidOut))
dbg("blkid returned UUID=%q", uuid)
if uuid == "" {
return "", fail("mounting", "UUID üres a formázás után", fmt.Errorf("empty UUID"))
}
// Backup fstab (non-fatal)
_ = BackupFstab(FstabPath)
dbg("step fstab: appending UUID=%s mountPath=%s fstype=ext4", uuid, mountPath)
if err := AppendFstabEntry(FstabPath, uuid, mountPath, "ext4", "defaults,nofail,noatime"); err != nil {
dbg("fstab append failed: %v", err)
return "", fail("mounting", "fstab bejegyzés hozzáadása sikertelen", err)
}
dbg("fstab entry added successfully")
// Mount by device path explicitly — container's /etc/fstab != host fstab,
// so "mount /mnt/hdd_1" (fstab lookup) won't work.
dbg("step mount: mount -t ext4 -o defaults,noatime %s %s", HostDevicePath(partDev), mountPath)
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 {
dbg("mount failed: %s", util.TruncateStr(string(out), 500))
// H19: Roll back fstab entry to prevent orphaned entry that hangs system on reboot.
_ = RemoveFstabEntry(FstabPath, uuid)
return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err)
}
// Verify mount actually worked (don't just trust exit code)
dbg("step verify: findmnt -n -o SOURCE --target %s", mountPath)
verifyOut, verifyErr := exec.Command("findmnt", "-n", "-o", "SOURCE", "--target", mountPath).Output()
if verifyErr != nil || strings.TrimSpace(string(verifyOut)) == "" {
dbg("mount verification failed: findmnt returned %q err=%v", string(verifyOut), verifyErr)
// H19: Also roll back fstab if mount verify fails.
_ = RemoveFstabEntry(FstabPath, uuid)
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))
}
dbg("mount verified: findmnt source=%q", strings.TrimSpace(string(verifyOut)))
send("mounting", "Csatlakoztatva: "+mountPath, 80)
// --- Step 5: Permissions + subdirs ---
send("permissions", "Mappák létrehozása és jogosultságok beállítása...", 85)
_ = exec.Command("chown", "1000:1000", mountPath).Run()
for _, subdir := range []string{"felhom-data", "Dokumentumok"} {
dir := filepath.Join(mountPath, subdir)
if err := os.MkdirAll(dir, 0755); err == nil {
_ = exec.Command("chown", "1000:1000", dir).Run()
}
}
dbg("format and mount completed successfully: %s", mountPath)
send("done", "Meghajtó sikeresen inicializálva: "+mountPath, 100)
return mountPath, nil
}
// GetDeviceUUID returns the UUID of a block device/partition.
func GetDeviceUUID(devicePath string) (string, error) {
out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(devicePath)).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// ReadFstab reads the current fstab content.
func ReadFstab() (string, error) {
data, err := os.ReadFile(FstabPath)
if err != nil {
data, err = os.ReadFile("/etc/fstab")
if err != nil {
return "", err
}
}
return string(data), nil
}