Files
deploy-felhom-compose/TASK.md
T

8.4 KiB

BUGFIX: Storage Scan — System Disk Detection & FSType in Container

Affects: v0.11.0, internal/storage/scan_linux.go Root cause: Controller runs in a Docker container. Even with --privileged, lsblk reports mount points from the container's mount namespace (not host), and often can't probe filesystem types due to missing udev/blkid cache.

Bug 1: System disk (sda) shows as available

Current broken logic

if part.MountPoint == "/" || part.MountPoint == "/boot" || part.MountPoint == "/boot/efi" {
    isSystem = true
}

Inside the container, sda2 (host's /) shows mounted at /opt/docker/felhom-controller/data (bind mount), not /. So isSystem stays false → sda appears in AvailableDisks.

Fix: Parse host's fstab + blkid to detect system disk

The host's fstab is mounted at /host-fstab inside the container. Parse it to find which devices/UUIDs are used for /, /boot, /boot/efi, and swap. Then resolve UUIDs to device paths via blkid, and mark their parent disks as system disks.

// getSystemDiskNames returns the set of parent disk names (e.g., "sda") 
// that contain system partitions (/, /boot, /boot/efi, swap).
func getSystemDiskNames() map[string]bool {
    systemDisks := map[string]bool{}
    
    // Step 1: Parse /host-fstab for system mount points
    fstabPath := "/host-fstab"
    if _, err := os.Stat(fstabPath); err != nil {
        // Fallback: try /etc/fstab (if not containerized or different mount)
        fstabPath = "/etc/fstab"
    }
    
    data, err := os.ReadFile(fstabPath)
    if err != nil {
        return systemDisks // Can't read fstab, return empty (safe default: nothing excluded)
    }
    
    // System mount points we care about
    systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true}
    
    var systemUUIDs []string
    var systemDevices []string
    
    for _, line := range strings.Split(string(data), "\n") {
        line = strings.TrimSpace(line)
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }
        fields := strings.Fields(line)
        if len(fields) < 3 {
            continue
        }
        source := fields[0]
        mountPoint := fields[1]
        fsType := fields[2]
        
        isSystemEntry := systemMounts[mountPoint] || fsType == "swap"
        if !isSystemEntry {
            continue
        }
        
        if strings.HasPrefix(source, "UUID=") {
            systemUUIDs = append(systemUUIDs, strings.TrimPrefix(source, "UUID="))
        } else if strings.HasPrefix(source, "/dev/") {
            systemDevices = append(systemDevices, source)
        }
    }
    
    // Step 2: Resolve UUIDs to device paths via blkid
    for _, uuid := range systemUUIDs {
        out, err := exec.Command("blkid", "-U", uuid).Output()
        if err == nil {
            devPath := strings.TrimSpace(string(out)) // e.g., "/dev/sda2"
            systemDevices = append(systemDevices, devPath)
        }
    }
    
    // Step 3: Extract parent disk names from device paths
    for _, devPath := range systemDevices {
        diskName := partitionToParentDisk(devPath)
        if diskName != "" {
            systemDisks[diskName] = true
        }
    }
    
    return systemDisks
}

// partitionToParentDisk extracts parent disk name from a partition device path.
// "/dev/sda2" → "sda", "/dev/nvme0n1p2" → "nvme0n1"
func partitionToParentDisk(devPath string) string {
    name := filepath.Base(devPath) // "sda2"
    
    // NVMe: nvme0n1p2 → nvme0n1
    if strings.Contains(name, "nvme") {
        if idx := strings.LastIndex(name, "p"); idx > 0 {
            candidate := name[:idx]
            // Verify it's actually a partition number after 'p'
            if _, err := strconv.Atoi(name[idx+1:]); err == nil {
                return candidate
            }
        }
        return name
    }
    
    // Standard: sda2 → sda, sdb1 → sdb
    return strings.TrimRight(name, "0123456789")
}

Then in ScanDisks(), replace the mount-point-based detection:

func ScanDisks() (*ScanResult, error) {
    // ... lsblk parsing as before ...
    
    // Get system disk names from host fstab
    systemDiskNames := getSystemDiskNames()
    
    for _, dev := range parsed.BlockDevices {
        if dev.Type != "disk" { continue }
        
        // ... build BlockDevice as before ...
        
        // Check if this is a system disk (from fstab analysis)
        isSystem := systemDiskNames[dev.Name]
        
        // Also check if any partition is currently mounted (fallback safety)
        anyMounted := false
        for _, child := range dev.Children {
            // ... as before ...
            if part.MountPoint != "" {
                anyMounted = true
            }
        }
        
        bd.Mounted = anyMounted || isSystem
        
        if isSystem || anyMounted {
            result.SystemDisks = append(result.SystemDisks, bd)
        } else {
            result.AvailableDisks = append(result.AvailableDisks, bd)
        }
    }
    return result, nil
}

Bug 2: "nincs fájlrendszer" for all partitions

Current broken logic

lsblk inside a container often returns null for fstype because it relies on udev/blkid cache that's incomplete in the container's environment.

Fix: Enrich with blkid

After lsblk parsing, run blkid to get filesystem types for all partitions. blkid directly probes the device (works in privileged containers):

// enrichWithBlkid fills in missing FSType, UUID, and Label from blkid.
func enrichWithBlkid(disks []BlockDevice) {
    // Run blkid once for all devices
    out, err := exec.Command("blkid", "-o", "export").Output()
    if err != nil {
        return // Best-effort; lsblk data still usable
    }
    
    // Parse blkid output — blocks separated by blank lines:
    // DEVNAME=/dev/sda1
    // UUID=XXXX-YYYY
    // TYPE=vfat
    // ...
    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
                }
                if p.UUID == "" {
                    p.UUID = info.UUID
                }
                if p.Label == "" {
                    p.Label = info.Label
                }
            }
        }
    }
}

type blkidInfo struct {
    FSType string
    UUID   string
    Label  string
}

func parseBlkidExport(data []byte) map[string]blkidInfo {
    result := map[string]blkidInfo{}
    
    blocks := strings.Split(string(data), "\n\n")
    for _, block := range blocks {
        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
}

Call enrichWithBlkid() at the end of ScanDisks() on both AvailableDisks and SystemDisks.

UI impact

After these fixes:

  • sda will appear in SystemDisks (shown grayed out with "Rendszermeghajtó" label, or hidden entirely)
  • sdb will be the only entry in AvailableDisks
  • sda partitions will show: sda1 (vfat, /boot/efi), sda2 (ext4, /), sda3 (swap)
  • sdb1 will correctly show "(nincs fájlrendszer)" since it genuinely has none

Template update

If SystemDisks are currently shown alongside AvailableDisks (both selectable), the template should either:

  • Option A: Hide system disks entirely — simpler, less confusion
  • Option B: Show them grayed out with a "Rendszermeghajtó — nem választható" badge

Recommended: Option A — only show AvailableDisks. The user doesn't need to see sda at all.

Files modified

  • controller/internal/storage/scan_linux.gogetSystemDiskNames(), partitionToParentDisk(), enrichWithBlkid(), parseBlkidExport(), updated ScanDisks()

Testing

  1. After fix: scan page shows only sdb (931.5 GB, HD710 PRO, sdb1 no filesystem)
  2. sda is no longer listed
  3. If you temporarily disconnect the USB HDD: scan shows "Nem található inicializálható meghajtó"