feat: format empty partitions on system disk (v0.32.6)
Detect and offer to format empty (no filesystem) partitions on the system disk. Adds IsSystemPartition() for granular per-partition safety checks instead of blocking the entire system disk. Init wizard shows formatable partitions with appropriate warnings. Add felhotest demo node to docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,58 @@ func getSystemDiskNames() map[string]bool {
|
||||
return systemDisks
|
||||
}
|
||||
|
||||
// getSystemPartitionPaths returns the set of partition device paths (e.g., "/dev/sda3")
|
||||
// that are system partitions (/, /boot, /boot/efi, swap).
|
||||
// Unlike getSystemDiskNames() which returns parent disk names, this returns the actual
|
||||
// partition paths for granular checks.
|
||||
func getSystemPartitionPaths() map[string]bool {
|
||||
sysPartitions := map[string]bool{}
|
||||
|
||||
fstabPath := "/host-fstab"
|
||||
if _, err := os.Stat(fstabPath); err != nil {
|
||||
fstabPath = "/etc/fstab"
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(fstabPath)
|
||||
if err != nil {
|
||||
return sysPartitions
|
||||
}
|
||||
|
||||
systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true}
|
||||
|
||||
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]
|
||||
|
||||
if !systemMounts[mountPoint] && fsType != "swap" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(source, "UUID=") {
|
||||
uuid := strings.TrimPrefix(source, "UUID=")
|
||||
if out, err := exec.Command("blkid", "-U", uuid).Output(); err == nil {
|
||||
devPath := strings.TrimSpace(string(out))
|
||||
if devPath != "" {
|
||||
sysPartitions[devPath] = true
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(source, "/dev/") {
|
||||
sysPartitions[source] = true
|
||||
}
|
||||
}
|
||||
|
||||
return sysPartitions
|
||||
}
|
||||
|
||||
// partitionToParentDisk extracts the parent disk name from a partition device path.
|
||||
// "/dev/sda2" → "sda", "/dev/nvme0n1p2" → "nvme0n1", "/dev/mmcblk0p1" → "mmcblk0"
|
||||
func partitionToParentDisk(devPath string) string {
|
||||
@@ -344,8 +396,31 @@ func ScanDisks(logger *log.Logger, debug bool) (*ScanResult, error) {
|
||||
enrichWithBlkid(result.AvailableDisks, logger, debug)
|
||||
enrichWithBlkid(result.SystemDisks, logger, debug)
|
||||
|
||||
// Detect formatable partitions on system disks:
|
||||
// empty (no filesystem), unmounted, and NOT a system partition.
|
||||
sysPartitionPaths := getSystemPartitionPaths()
|
||||
for _, sysDisk := range result.SystemDisks {
|
||||
for _, part := range sysDisk.Partitions {
|
||||
if part.FSType != "" || part.MountPoint != "" {
|
||||
continue
|
||||
}
|
||||
if sysPartitionPaths[part.Path] {
|
||||
continue
|
||||
}
|
||||
result.FormatablePartitions = append(result.FormatablePartitions, FormatablePartition{
|
||||
Partition: part,
|
||||
ParentDiskName: sysDisk.Name,
|
||||
ParentDiskPath: sysDisk.Path,
|
||||
ParentDiskModel: sysDisk.Model,
|
||||
ParentDiskSize: sysDisk.Size,
|
||||
})
|
||||
dbg("formatable partition found: %s on system disk %s", part.Path, sysDisk.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Printf("[INFO] [storage] Found %d disks", len(result.AvailableDisks)+len(result.SystemDisks))
|
||||
logger.Printf("[INFO] [storage] Found %d disks, %d formatable partitions",
|
||||
len(result.AvailableDisks)+len(result.SystemDisks), len(result.FormatablePartitions))
|
||||
}
|
||||
dbg("disk scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user