diff --git a/controller/docker-compose.yml b/controller/docker-compose.yml index ecff697..82b47d5 100644 --- a/controller/docker-compose.yml +++ b/controller/docker-compose.yml @@ -30,8 +30,8 @@ services: - /etc/os-release:/host/etc/os-release:ro # Host hostname — for monitoring page (os.Hostname() returns container ID) - /etc/hostname:/host/etc/hostname:ro - # Block devices — required for storage init (lsblk, mkfs, sfdisk) - - /dev:/dev + # Block devices — mounted at /host-dev (can't override Docker's /dev tmpfs) + - /dev:/host-dev:rw # Host fstab — UUID-based mount persistence (mounted as /host-fstab inside container) - /etc/fstab:/host-fstab # udev metadata — for blkid/lsblk device model info diff --git a/controller/internal/storage/format_linux.go b/controller/internal/storage/format_linux.go index fb1f7e1..b0fd616 100644 --- a/controller/internal/storage/format_linux.go +++ b/controller/internal/storage/format_linux.go @@ -36,7 +36,7 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, if err := ValidateMountName(req.MountName); err != nil { return "", fail("validating", "Érvénytelen csatlakoztatási név", err) } - if _, err := os.Stat(req.DevicePath); err != nil { + if _, err := os.Stat(HostDevicePath(req.DevicePath)); err != nil { return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err) } @@ -72,20 +72,20 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, send("partitioning", "Partíció létrehozása...", 15) sfdiskInput := "label: gpt\n,,,L\n" - cmd := exec.Command("sfdisk", req.DevicePath) + cmd := exec.Command("sfdisk", 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) } - _ = exec.Command("partprobe", req.DevicePath).Run() + _ = exec.Command("partprobe", HostDevicePath(req.DevicePath)).Run() time.Sleep(2 * time.Second) partDev = req.DevicePath + "1" if strings.Contains(req.DevicePath, "nvme") { partDev = req.DevicePath + "p1" } - if _, err := os.Stat(partDev); err != nil { + if _, err := os.Stat(HostDevicePath(partDev)); err != nil { return "", fail("partitioning", "Partíció nem található a létrehozás után: "+partDev, err) } @@ -103,7 +103,7 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, label = label[:16] } - mkfsCmd := exec.Command("mkfs.ext4", "-L", label, "-F", partDev) + mkfsCmd := exec.Command("mkfs.ext4", "-L", label, "-F", HostDevicePath(partDev)) var mkfsOut bytes.Buffer mkfsCmd.Stdout = &mkfsOut mkfsCmd.Stderr = &mkfsOut @@ -120,7 +120,7 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, return "", fail("mounting", "Csatlakoztatási mappa nem hozható létre: "+mountPath, err) } - uuidOut, err := exec.Command("blkid", "-s", "UUID", "-o", "value", partDev).Output() + uuidOut, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(partDev)).Output() if err != nil { return "", fail("mounting", "UUID lekérése sikertelen", err) } @@ -161,7 +161,7 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, // GetDeviceUUID returns the UUID of a block device/partition. func GetDeviceUUID(devicePath string) (string, error) { - out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", devicePath).Output() + out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(devicePath)).Output() if err != nil { return "", err } diff --git a/controller/internal/storage/safety.go b/controller/internal/storage/safety.go index 32a2eeb..bd8a7fc 100644 --- a/controller/internal/storage/safety.go +++ b/controller/internal/storage/safety.go @@ -3,6 +3,7 @@ package storage import ( "fmt" "regexp" + "strings" ) // mountNameRe validates mount names: only alphanumeric + underscore. @@ -12,6 +13,21 @@ var mountNameRe = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) // The compose file mounts /etc/fstab → /host-fstab. const FstabPath = "/host-fstab" +// HostDevPath is where the host's /dev is mounted inside the container. +// Docker always creates its own tmpfs at /dev (overriding any bind mount), +// so the host's block devices are mounted at a different path instead. +const HostDevPath = "/host-dev" + +// HostDevicePath converts a standard /dev/ device path to the container-accessible path. +// "/dev/sdb" → "/host-dev/sdb", "/dev/sdb1" → "/host-dev/sdb1" +// Paths that don't start with /dev/ are returned unchanged. +func HostDevicePath(devPath string) string { + if strings.HasPrefix(devPath, "/dev/") { + return HostDevPath + "/" + strings.TrimPrefix(devPath, "/dev/") + } + return devPath +} + // ValidateMountName returns an error if the mount name is invalid. func ValidateMountName(name string) error { if name == "" { diff --git a/controller/internal/storage/safety_linux.go b/controller/internal/storage/safety_linux.go index c90b8b4..04022d3 100644 --- a/controller/internal/storage/safety_linux.go +++ b/controller/internal/storage/safety_linux.go @@ -20,9 +20,9 @@ func IsSystemDisk(devicePath string) (bool, error) { return false, fmt.Errorf("cannot stat /: %w", err) } - // Get block device info of the target device + // Get block device info of the target device (via /host-dev — Docker overrides /dev) var devStat syscall.Stat_t - if err := syscall.Stat(devicePath, &devStat); err != nil { + if err := syscall.Stat(HostDevicePath(devicePath), &devStat); err != nil { return false, fmt.Errorf("cannot stat %s: %w", devicePath, err) } diff --git a/controller/internal/storage/scan_linux.go b/controller/internal/storage/scan_linux.go index a57d3a9..9428a0e 100644 --- a/controller/internal/storage/scan_linux.go +++ b/controller/internal/storage/scan_linux.go @@ -175,68 +175,28 @@ func partitionToParentDisk(devPath string) string { return strings.TrimRight(name, "0123456789") } -// blkidInfo holds filesystem metadata from blkid. -type blkidInfo struct { - FSType string - UUID string - Label string -} - -// parseBlkidExport parses the output of `blkid -o export` into a map keyed by device path. -// Output format: blocks separated by blank lines, each line "KEY=VALUE". -func parseBlkidExport(data []byte) map[string]blkidInfo { - result := map[string]blkidInfo{} - - for _, block := range strings.Split(string(data), "\n\n") { - var devName string - info := blkidInfo{} - for _, line := range strings.Split(strings.TrimSpace(block), "\n") { - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key, val := parts[0], parts[1] - switch key { - case "DEVNAME": - devName = val - case "TYPE": - info.FSType = val - case "UUID": - info.UUID = val - case "LABEL": - info.Label = val - } - } - if devName != "" { - result[devName] = info - } - } - return result -} - // enrichWithBlkid fills in missing FSType, UUID, and Label on partitions using blkid. -// blkid directly probes devices, which works in privileged containers where lsblk's -// udev/blkid cache is unavailable. +// Probes each partition individually via /host-dev (Docker overrides /dev with its own +// tmpfs, so the host block devices are accessible at /host-dev instead). func enrichWithBlkid(disks []BlockDevice) { - out, err := exec.Command("blkid", "-o", "export").Output() - if err != nil { - return - } - - blkidMap := parseBlkidExport(out) - for i := range disks { for j := range disks[i].Partitions { p := &disks[i].Partitions[j] - if info, ok := blkidMap[p.Path]; ok { - if p.FSType == "" { - p.FSType = info.FSType + hostPath := HostDevicePath(p.Path) // "/dev/sdb1" → "/host-dev/sdb1" + + if p.FSType == "" { + if out, err := exec.Command("blkid", "-o", "value", "-s", "TYPE", hostPath).Output(); err == nil { + p.FSType = strings.TrimSpace(string(out)) } - if p.UUID == "" { - p.UUID = info.UUID + } + if p.UUID == "" { + if out, err := exec.Command("blkid", "-o", "value", "-s", "UUID", hostPath).Output(); err == nil { + p.UUID = strings.TrimSpace(string(out)) } - if p.Label == "" { - p.Label = info.Label + } + if p.Label == "" { + if out, err := exec.Command("blkid", "-o", "value", "-s", "LABEL", hostPath).Output(); err == nil { + p.Label = strings.TrimSpace(string(out)) } } }