mirror of
https://github.com/kairos-io/kairos-sdk.git
synced 2025-09-26 14:47:33 +00:00
feat: support multipath in Kairos SDK
Signed-off-by: Aidan Leuck <aidan_leuck@selinc.com>
This commit is contained in:
committed by
Dimitris Karakasilis
parent
2fcb006782
commit
1cd4261f67
58
ghw/disk_partition_handler.go
Normal file
58
ghw/disk_partition_handler.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ghw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
)
|
||||
|
||||
type DiskPartitionHandler struct {
|
||||
DiskName string
|
||||
}
|
||||
|
||||
// Validate that DiskPartitionHandler implements PartitionHandler interface
|
||||
var _ PartitionHandler = &DiskPartitionHandler{}
|
||||
|
||||
func NewDiskPartitionHandler(diskName string) *DiskPartitionHandler {
|
||||
return &DiskPartitionHandler{DiskName: diskName}
|
||||
}
|
||||
|
||||
func (d *DiskPartitionHandler) GetPartitions(paths *Paths, logger *types.KairosLogger) types.PartitionList {
|
||||
out := make(types.PartitionList, 0)
|
||||
path := filepath.Join(paths.SysBlock, d.DiskName)
|
||||
logger.Logger.Debug().Str("file", path).Msg("Reading disk file")
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to read disk partitions")
|
||||
return out
|
||||
}
|
||||
for _, file := range files {
|
||||
fname := file.Name()
|
||||
if !strings.HasPrefix(fname, d.DiskName) {
|
||||
continue
|
||||
}
|
||||
logger.Logger.Debug().Str("file", fname).Msg("Reading partition file")
|
||||
partitionPath := filepath.Join(d.DiskName, fname)
|
||||
size := partitionSizeBytes(paths, partitionPath, logger)
|
||||
mp, pt := partitionInfo(paths, fname, logger)
|
||||
du := diskPartUUID(paths, partitionPath, logger)
|
||||
if pt == "" {
|
||||
pt = diskPartTypeUdev(paths, partitionPath, logger)
|
||||
}
|
||||
fsLabel := diskFSLabel(paths, partitionPath, logger)
|
||||
p := &types.Partition{
|
||||
Name: fname,
|
||||
Size: uint(size / (1024 * 1024)),
|
||||
MountPoint: mp,
|
||||
UUID: du,
|
||||
FilesystemLabel: fsLabel,
|
||||
FS: pt,
|
||||
Path: filepath.Join("/dev", fname),
|
||||
Disk: filepath.Join("/dev", d.DiskName),
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
270
ghw/ghw.go
270
ghw/ghw.go
@@ -1,9 +1,7 @@
|
||||
package ghw
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -49,6 +47,10 @@ func NewPaths(withOptionalPrefix string) *Paths {
|
||||
return p
|
||||
}
|
||||
|
||||
func isMultipathDevice(entry os.DirEntry) bool {
|
||||
return strings.HasPrefix(entry.Name(), "dm-")
|
||||
}
|
||||
|
||||
func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk {
|
||||
if logger == nil {
|
||||
newLogger := types.NewKairosLogger("ghw", "info", false)
|
||||
@@ -61,10 +63,18 @@ func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk {
|
||||
return nil
|
||||
}
|
||||
for _, file := range files {
|
||||
var partitionHandler PartitionHandler;
|
||||
logger.Logger.Debug().Str("file", file.Name()).Msg("Reading file")
|
||||
dname := file.Name()
|
||||
size := diskSizeBytes(paths, dname, logger)
|
||||
|
||||
// Skip entries that are multipath partitions
|
||||
// we will handle them when we parse this disks partitions
|
||||
if isMultipathPartition(file, paths, logger) {
|
||||
logger.Logger.Debug().Str("file", dname).Msg("Skipping multipath partition")
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(dname, "loop") && size == 0 {
|
||||
// We don't care about unused loop devices...
|
||||
continue
|
||||
@@ -72,10 +82,17 @@ func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk {
|
||||
d := &types.Disk{
|
||||
Name: dname,
|
||||
SizeBytes: size,
|
||||
UUID: diskUUID(paths, dname, "", logger),
|
||||
UUID: diskUUID(paths, dname, logger),
|
||||
}
|
||||
|
||||
parts := diskPartitions(paths, dname, logger)
|
||||
if(isMultipathDevice(file)) {
|
||||
partitionHandler = NewMultipathPartitionHandler(dname)
|
||||
} else {
|
||||
partitionHandler = NewDiskPartitionHandler(dname)
|
||||
}
|
||||
|
||||
|
||||
parts := partitionHandler.GetPartitions(paths, logger)
|
||||
d.Partitions = parts
|
||||
|
||||
disks = append(disks, d)
|
||||
@@ -84,6 +101,25 @@ func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk {
|
||||
return disks
|
||||
}
|
||||
|
||||
func isMultipathPartition(entry os.DirEntry, paths *Paths, logger *types.KairosLogger) bool {
|
||||
// Must be a dm device to be a multipath partition
|
||||
if !isMultipathDevice(entry) {
|
||||
return false
|
||||
}
|
||||
|
||||
deviceName := entry.Name()
|
||||
udevInfo, err := udevInfoPartition(paths, deviceName, logger)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("devNo", deviceName).Msg("Failed to get udev info")
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the udev info contains DM_PART indicating it's a partition
|
||||
// this is the primary check for multipath partitions and should be safe.
|
||||
_, ok := udevInfo["DM_PART"]
|
||||
return ok
|
||||
}
|
||||
|
||||
func diskSizeBytes(paths *Paths, disk string, logger *types.KairosLogger) uint64 {
|
||||
// We can find the number of 512-byte sectors by examining the contents of
|
||||
// /sys/block/$DEVICE/size and calculate the physical bytes accordingly.
|
||||
@@ -102,229 +138,3 @@ func diskSizeBytes(paths *Paths, disk string, logger *types.KairosLogger) uint64
|
||||
logger.Logger.Trace().Uint64("size", size*sectorSize).Msg("Got disk size")
|
||||
return size * sectorSize
|
||||
}
|
||||
|
||||
// diskPartitions takes the name of a disk (note: *not* the path of the disk,
|
||||
// but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not
|
||||
// "/dev/nvme0n1") and returns a slice of pointers to Partition structs
|
||||
// representing the partitions in that disk
|
||||
func diskPartitions(paths *Paths, disk string, logger *types.KairosLogger) types.PartitionList {
|
||||
out := make(types.PartitionList, 0)
|
||||
path := filepath.Join(paths.SysBlock, disk)
|
||||
logger.Logger.Debug().Str("file", path).Msg("Reading disk file")
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to read disk partitions")
|
||||
return out
|
||||
}
|
||||
for _, file := range files {
|
||||
fname := file.Name()
|
||||
if !strings.HasPrefix(fname, disk) {
|
||||
continue
|
||||
}
|
||||
logger.Logger.Debug().Str("file", fname).Msg("Reading partition file")
|
||||
size := partitionSizeBytes(paths, disk, fname, logger)
|
||||
mp, pt := partitionInfo(paths, fname, logger)
|
||||
du := diskPartUUID(paths, disk, fname, logger)
|
||||
if pt == "" {
|
||||
pt = diskPartTypeUdev(paths, disk, fname, logger)
|
||||
}
|
||||
fsLabel := diskFSLabel(paths, disk, fname, logger)
|
||||
p := &types.Partition{
|
||||
Name: fname,
|
||||
Size: uint(size / (1024 * 1024)),
|
||||
MountPoint: mp,
|
||||
UUID: du,
|
||||
FilesystemLabel: fsLabel,
|
||||
FS: pt,
|
||||
Path: filepath.Join("/dev", fname),
|
||||
Disk: filepath.Join("/dev", disk),
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func partitionSizeBytes(paths *Paths, disk string, part string, logger *types.KairosLogger) uint64 {
|
||||
path := filepath.Join(paths.SysBlock, disk, part, "size")
|
||||
logger.Logger.Debug().Str("file", path).Msg("Reading size file")
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("file", path).Err(err).Msg("failed to read disk partition size")
|
||||
return 0
|
||||
}
|
||||
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("contents", string(contents)).Err(err).Msg("failed to parse disk partition size")
|
||||
return 0
|
||||
}
|
||||
logger.Logger.Trace().Str("disk", disk).Str("partition", part).Uint64("size", size*sectorSize).Msg("Got partition size")
|
||||
return size * sectorSize
|
||||
}
|
||||
|
||||
func partitionInfo(paths *Paths, part string, logger *types.KairosLogger) (string, string) {
|
||||
// Allow calling PartitionInfo with either the full partition name
|
||||
// "/dev/sda1" or just "sda1"
|
||||
if !strings.HasPrefix(part, "/dev") {
|
||||
part = "/dev/" + part
|
||||
}
|
||||
|
||||
// mount entries for mounted partitions look like this:
|
||||
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
||||
var r io.ReadCloser
|
||||
logger.Logger.Debug().Str("file", paths.ProcMounts).Msg("Reading mounts file")
|
||||
r, err := os.Open(paths.ProcMounts)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("file", paths.ProcMounts).Err(err).Msg("failed to open mounts")
|
||||
return "", ""
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.Logger.Trace().Str("line", line).Msg("Parsing mount info")
|
||||
entry := parseMountEntry(line, logger)
|
||||
if entry == nil || entry.Partition != part {
|
||||
continue
|
||||
}
|
||||
|
||||
return entry.Mountpoint, entry.FilesystemType
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type mountEntry struct {
|
||||
Partition string
|
||||
Mountpoint string
|
||||
FilesystemType string
|
||||
}
|
||||
|
||||
func parseMountEntry(line string, logger *types.KairosLogger) *mountEntry {
|
||||
// mount entries for mounted partitions look like this:
|
||||
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
||||
if line[0] != '/' {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) < 4 {
|
||||
logger.Logger.Debug().Interface("fields", fields).Msg("Mount line has less than 4 fields")
|
||||
return nil
|
||||
}
|
||||
|
||||
// We do some special parsing of the mountpoint, which may contain space,
|
||||
// tab and newline characters, encoded into the mount entry line using their
|
||||
// octal-to-string representations. From the GNU mtab man pages:
|
||||
//
|
||||
// "Therefore these characters are encoded in the files and the getmntent
|
||||
// function takes care of the decoding while reading the entries back in.
|
||||
// '\040' is used to encode a space character, '\011' to encode a tab
|
||||
// character, '\012' to encode a newline character, and '\\' to encode a
|
||||
// backslash."
|
||||
mp := fields[1]
|
||||
r := strings.NewReplacer(
|
||||
"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
|
||||
)
|
||||
mp = r.Replace(mp)
|
||||
|
||||
res := &mountEntry{
|
||||
Partition: fields[0],
|
||||
Mountpoint: mp,
|
||||
FilesystemType: fields[2],
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func diskUUID(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, disk, partition, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk UUID")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("failed to read disk UUID")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_PART_TABLE_UUID"]; ok {
|
||||
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", pType).Msg("Got disk uuid")
|
||||
return pType
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func diskPartUUID(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, disk, partition, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk Part UUID")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk Part UUID")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_PART_ENTRY_UUID"]; ok {
|
||||
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", pType).Msg("Got partition uuid")
|
||||
return pType
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
// diskPartTypeUdev gets the partition type from the udev database directly and its only used as fallback when
|
||||
// the partition is not mounted, so we cannot get the type from paths.ProcMounts from the partitionInfo function
|
||||
func diskPartTypeUdev(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, disk, partition, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk Part Type")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk Part Type")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_FS_TYPE"]; ok {
|
||||
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("FS", pType).Msg("Got partition fs type")
|
||||
return pType
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func diskFSLabel(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, disk, partition, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk FS label")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk FS label")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if label, ok := info["ID_FS_LABEL"]; ok {
|
||||
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", label).Msg("Got partition label")
|
||||
return label
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func udevInfoPartition(paths *Paths, disk string, partition string, logger *types.KairosLogger) (map[string]string, error) {
|
||||
// Get device major:minor numbers
|
||||
devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, partition, "dev"))
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.SysBlock, disk, partition, "dev")).Msg("failed to read udev info")
|
||||
return nil, err
|
||||
}
|
||||
return UdevInfo(paths, string(devNo), logger)
|
||||
}
|
||||
|
||||
// UdevInfo will return information on udev database about a device number
|
||||
func UdevInfo(paths *Paths, devNo string, logger *types.KairosLogger) (map[string]string, error) {
|
||||
// Look up block device in udev runtime database
|
||||
udevID := "b" + strings.TrimSpace(devNo)
|
||||
udevBytes, err := os.ReadFile(filepath.Join(paths.RunUdevData, udevID))
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.RunUdevData, udevID)).Msg("failed to read udev info for device")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udevInfo := make(map[string]string)
|
||||
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
||||
if strings.HasPrefix(udevLine, "E:") {
|
||||
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
||||
udevInfo[s[0]] = s[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return udevInfo, nil
|
||||
}
|
||||
|
246
ghw/ghw_test.go
246
ghw/ghw_test.go
@@ -69,4 +69,250 @@ var _ = Describe("GHW functions tests", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("With multipath devices", func() {
|
||||
BeforeEach(func() {
|
||||
// Create a multipath device dm-0 with two partitions dm-1 and dm-2
|
||||
multipathDisk := types.Disk{
|
||||
Name: "dm-0",
|
||||
UUID: "mpath-uuid-123",
|
||||
SizeBytes: 10 * 1024 * 1024, // 10MB
|
||||
Partitions: []*types.Partition{
|
||||
{
|
||||
Name: "dm-1",
|
||||
FilesystemLabel: "MPATH_BOOT",
|
||||
FS: "ext4",
|
||||
MountPoint: "/boot",
|
||||
Size: 512,
|
||||
UUID: "part1-uuid-456",
|
||||
},
|
||||
{
|
||||
Name: "dm-2",
|
||||
FilesystemLabel: "MPATH_DATA",
|
||||
FS: "xfs",
|
||||
MountPoint: "/data",
|
||||
Size: 1024,
|
||||
UUID: "part2-uuid-789",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ghwMock.AddDisk(multipathDisk)
|
||||
ghwMock.CreateMultipathDevices()
|
||||
})
|
||||
|
||||
It("Identifies multipath device correctly", func() {
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
// Should find the multipath device but not the partitions as separate disks
|
||||
Expect(len(disks)).To(Equal(1), "Should find only the multipath device")
|
||||
|
||||
disk := disks[0]
|
||||
Expect(disk.Name).To(Equal("dm-0"))
|
||||
Expect(disk.UUID).To(Equal("mpath-uuid-123"))
|
||||
Expect(disk.SizeBytes).To(Equal(uint64(10 * 1024 * 1024 * 512))) // size * sectorSize
|
||||
})
|
||||
|
||||
It("Finds multipath partitions using MultipathPartitionHandler", func() {
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
Expect(len(disks)).To(Equal(1))
|
||||
disk := disks[0]
|
||||
|
||||
// Should find both partitions
|
||||
Expect(len(disk.Partitions)).To(Equal(2))
|
||||
|
||||
// Verify first partition
|
||||
part1 := disk.Partitions[0]
|
||||
Expect(part1.Name).To(Equal("dm-1"))
|
||||
Expect(part1.FilesystemLabel).To(Equal("MPATH_BOOT"))
|
||||
Expect(part1.FS).To(Equal("ext4"))
|
||||
Expect(part1.MountPoint).To(Equal("/boot"))
|
||||
Expect(part1.UUID).To(Equal("part1-uuid-456"))
|
||||
Expect(part1.Path).To(Equal("/dev/dm-1"))
|
||||
Expect(part1.Disk).To(Equal("/dev/dm-0"))
|
||||
|
||||
// Verify second partition
|
||||
part2 := disk.Partitions[1]
|
||||
Expect(part2.Name).To(Equal("dm-2"))
|
||||
Expect(part2.FilesystemLabel).To(Equal("MPATH_DATA"))
|
||||
Expect(part2.FS).To(Equal("xfs"))
|
||||
Expect(part2.MountPoint).To(Equal("/data"))
|
||||
Expect(part2.UUID).To(Equal("part2-uuid-789"))
|
||||
Expect(part2.Path).To(Equal("/dev/dm-2"))
|
||||
Expect(part2.Disk).To(Equal("/dev/dm-0"))
|
||||
})
|
||||
|
||||
It("Skips multipath partitions in main disk enumeration", func() {
|
||||
// This test verifies that multipath partitions (dm-1, dm-2) are not
|
||||
// returned as separate disks in the main GetDisks call
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
// Should only find the parent multipath device, not the partition devices
|
||||
Expect(len(disks)).To(Equal(1))
|
||||
Expect(disks[0].Name).To(Equal("dm-0"))
|
||||
|
||||
// Verify no disk named dm-1 or dm-2 is returned
|
||||
for _, disk := range disks {
|
||||
Expect(disk.Name).ToNot(Equal("dm-1"))
|
||||
Expect(disk.Name).ToNot(Equal("dm-2"))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Describe("With multipath devices using /dev/dm-<n> mount format", func() {
|
||||
BeforeEach(func() {
|
||||
// Create a multipath device dm-3 with partitions mounted as /dev/dm-<n> instead of /dev/mapper/<name>
|
||||
multipathDisk := types.Disk{
|
||||
Name: "dm-3",
|
||||
UUID: "mpath-dm-mount-uuid",
|
||||
SizeBytes: 8 * 1024 * 1024,
|
||||
Partitions: []*types.Partition{
|
||||
{
|
||||
Name: "dm-4",
|
||||
FilesystemLabel: "DM_BOOT",
|
||||
FS: "ext4",
|
||||
MountPoint: "/boot",
|
||||
Size: 256,
|
||||
UUID: "dm-part1-uuid",
|
||||
},
|
||||
{
|
||||
Name: "dm-5",
|
||||
FilesystemLabel: "DM_DATA",
|
||||
FS: "xfs",
|
||||
MountPoint: "/data",
|
||||
Size: 512,
|
||||
UUID: "dm-part2-uuid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ghwMock.AddDisk(multipathDisk)
|
||||
ghwMock.CreateMultipathDevicesWithDmMounts()
|
||||
})
|
||||
|
||||
It("Finds multipath partitions mounted as /dev/dm-<n>", func() {
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
Expect(len(disks)).To(Equal(1))
|
||||
disk := disks[0]
|
||||
|
||||
// Should find both partitions
|
||||
Expect(len(disk.Partitions)).To(Equal(2))
|
||||
|
||||
// Verify partitions can be found regardless of mount format
|
||||
var bootPartition, dataPartition *types.Partition
|
||||
for _, part := range disk.Partitions {
|
||||
if part.MountPoint == "/boot" {
|
||||
bootPartition = part
|
||||
} else if part.MountPoint == "/data" {
|
||||
dataPartition = part
|
||||
}
|
||||
}
|
||||
|
||||
Expect(bootPartition).ToNot(BeNil())
|
||||
Expect(bootPartition.Name).To(Equal("dm-4"))
|
||||
Expect(bootPartition.FilesystemLabel).To(Equal("DM_BOOT"))
|
||||
Expect(bootPartition.MountPoint).To(Equal("/boot"))
|
||||
Expect(bootPartition.FS).To(Equal("ext4"))
|
||||
Expect(bootPartition.Path).To(Equal("/dev/dm-4"))
|
||||
|
||||
Expect(dataPartition).ToNot(BeNil())
|
||||
Expect(dataPartition.Name).To(Equal("dm-5"))
|
||||
Expect(dataPartition.FilesystemLabel).To(Equal("DM_DATA"))
|
||||
Expect(dataPartition.MountPoint).To(Equal("/data"))
|
||||
Expect(dataPartition.FS).To(Equal("xfs"))
|
||||
Expect(dataPartition.Path).To(Equal("/dev/dm-5"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("With standalone multipath device (no partitions)", func() {
|
||||
It("Handles multipath device with no partitions", func() {
|
||||
multipathDisk := types.Disk{
|
||||
Name: "dm-5",
|
||||
UUID: "mpath-empty-uuid",
|
||||
SizeBytes: 5 * 1024 * 1024,
|
||||
}
|
||||
|
||||
ghwMock.AddDisk(multipathDisk)
|
||||
ghwMock.CreateMultipathDevices()
|
||||
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
Expect(len(disks)).To(Equal(1))
|
||||
disk := disks[0]
|
||||
Expect(disk.Name).To(Equal("dm-5"))
|
||||
Expect(len(disk.Partitions)).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("With mixed regular and multipath disks", func() {
|
||||
It("Handles mixed regular and multipath disks", func() {
|
||||
// Create multipath device
|
||||
multipathDeviceDef := types.Disk{
|
||||
Name: "dm-0",
|
||||
UUID: "mpath-uuid-123",
|
||||
SizeBytes: 10 * 1024 * 1024,
|
||||
Partitions: []*types.Partition{
|
||||
{
|
||||
Name: "dm-1",
|
||||
FilesystemLabel: "MPATH_BOOT",
|
||||
FS: "ext4",
|
||||
MountPoint: "/boot",
|
||||
Size: 512,
|
||||
UUID: "part1-uuid-456",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create regular disk
|
||||
regularDisk := types.Disk{
|
||||
Name: "sda",
|
||||
UUID: "regular-uuid-999",
|
||||
SizeBytes: 8 * 1024 * 1024,
|
||||
Partitions: []*types.Partition{
|
||||
{
|
||||
Name: "sda1",
|
||||
FilesystemLabel: "REGULAR_ROOT",
|
||||
FS: "ext4",
|
||||
MountPoint: "/",
|
||||
Size: 2048,
|
||||
UUID: "regular-part-uuid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add both disks
|
||||
ghwMock.AddDisk(multipathDeviceDef)
|
||||
ghwMock.AddDisk(regularDisk)
|
||||
|
||||
// Create multipath structure (this will handle both disks appropriately)
|
||||
ghwMock.CreateMultipathDevices()
|
||||
|
||||
disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil)
|
||||
|
||||
// Should find both the regular disk and multipath device
|
||||
Expect(len(disks)).To(Equal(2))
|
||||
|
||||
var foundMultipathDisk, foundRegularDisk *types.Disk
|
||||
for _, disk := range disks {
|
||||
if disk.Name == "dm-0" {
|
||||
foundMultipathDisk = disk
|
||||
} else if disk.Name == "sda" {
|
||||
foundRegularDisk = disk
|
||||
}
|
||||
}
|
||||
|
||||
Expect(foundMultipathDisk).ToNot(BeNil())
|
||||
Expect(foundRegularDisk).ToNot(BeNil())
|
||||
|
||||
// Verify multipath device has its partition
|
||||
Expect(len(foundMultipathDisk.Partitions)).To(Equal(1))
|
||||
Expect(foundMultipathDisk.Partitions[0].Name).To(Equal("dm-1"))
|
||||
|
||||
// Verify regular device has its partition
|
||||
Expect(len(foundRegularDisk.Partitions)).To(Equal(1))
|
||||
Expect(foundRegularDisk.Partitions[0].Name).To(Equal("sda1"))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
@@ -2,12 +2,13 @@ package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kairos-io/kairos-sdk/ghw"
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kairos-io/kairos-sdk/ghw"
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
)
|
||||
|
||||
// GhwMock is used to construct a fake disk to present to ghw when scanning block devices
|
||||
@@ -163,3 +164,198 @@ func (g *GhwMock) Clean() {
|
||||
_ = os.Unsetenv("GHW_CHROOT")
|
||||
_ = os.RemoveAll(g.Chroot)
|
||||
}
|
||||
|
||||
// CreateMultipathDevicesWithDmMounts creates multipath device structure using /dev/dm-<n> mount format
|
||||
// This is the same as CreateMultipathDevices but mounts partitions as /dev/dm-<n> instead of /dev/mapper/<name>
|
||||
func (g *GhwMock) CreateMultipathDevicesWithDmMounts() {
|
||||
g.createMultipathDevicesWithMountFormat(true)
|
||||
}
|
||||
|
||||
// CreateMultipathDevices creates multipath device structure in the mock filesystem
|
||||
// This sets up the basic dm device structure needed for multipath devices
|
||||
func (g *GhwMock) CreateMultipathDevices() {
|
||||
g.createMultipathDevicesWithMountFormat(false)
|
||||
}
|
||||
|
||||
// createMultipathDevicesWithMountFormat is the common implementation
|
||||
// useDmMount determines whether to use /dev/dm-<n> (true) or /dev/mapper/<name> (false) for mounts
|
||||
func (g *GhwMock) createMultipathDevicesWithMountFormat(useDmMount bool) {
|
||||
// Store multipath partitions before clearing them
|
||||
multipathPartitions := make(map[string][]*types.Partition)
|
||||
|
||||
// Clear partitions from multipath devices before creating basic structure
|
||||
// We'll recreate them as multipath partitions after
|
||||
for i := range g.disks {
|
||||
if strings.HasPrefix(g.disks[i].Name, "dm-") {
|
||||
multipathPartitions[g.disks[i].Name] = g.disks[i].Partitions
|
||||
g.disks[i].Partitions = nil // Clear existing partitions
|
||||
}
|
||||
}
|
||||
|
||||
// First create the basic devices (now without partitions for dm devices)
|
||||
g.CreateDevices()
|
||||
|
||||
// Now add multipath-specific structure for dm- devices
|
||||
for indexDisk, disk := range g.disks {
|
||||
if strings.HasPrefix(disk.Name, "dm-") {
|
||||
diskPath := filepath.Join(g.paths.SysBlock, disk.Name)
|
||||
|
||||
// Create dm/name file
|
||||
dmDir := filepath.Join(diskPath, "dm")
|
||||
_ = os.MkdirAll(dmDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(dmDir, "name"), []byte(fmt.Sprintf("mpath%d", indexDisk)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(dmDir, "uuid"), []byte(fmt.Sprintf("mpath-%s", disk.UUID)), 0644)
|
||||
|
||||
// Create holders directory for partitions
|
||||
holdersDir := filepath.Join(diskPath, "holders")
|
||||
_ = os.MkdirAll(holdersDir, 0755)
|
||||
|
||||
// Create slaves directory to indicate this is a multipath device
|
||||
slavesDir := filepath.Join(diskPath, "slaves")
|
||||
_ = os.MkdirAll(slavesDir, 0755)
|
||||
// Add some fake slave devices
|
||||
_ = os.WriteFile(filepath.Join(slavesDir, "sda"), []byte(""), 0644)
|
||||
_ = os.WriteFile(filepath.Join(slavesDir, "sdb"), []byte(""), 0644)
|
||||
|
||||
// Convert stored partitions to multipath partitions
|
||||
if partitions, exists := multipathPartitions[disk.Name]; exists {
|
||||
for partIndex, partition := range partitions {
|
||||
g.createMultipathPartitionWithMountFormat(disk.Name, partition, partIndex+1, useDmMount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createMultipathPartitionWithMountFormat creates a multipath partition structure
|
||||
// useDmMount determines the mount format: true for /dev/dm-<n>, false for /dev/mapper/<name>
|
||||
func (g *GhwMock) createMultipathPartitionWithMountFormat(parentDiskName string, partition *types.Partition, partNum int, useDmMount bool) {
|
||||
parentDiskPath := filepath.Join(g.paths.SysBlock, parentDiskName)
|
||||
holdersDir := filepath.Join(parentDiskPath, "holders")
|
||||
partitionSuffix := fmt.Sprintf("p%d", partNum)
|
||||
|
||||
// Create the partition as a top-level device in /sys/block/
|
||||
partitionPath := filepath.Join(g.paths.SysBlock, partition.Name)
|
||||
_ = os.MkdirAll(partitionPath, 0755)
|
||||
|
||||
// Create partition dev file (use unique device numbers)
|
||||
partIndex := 100 + partNum // Ensure unique device numbers
|
||||
_ = os.WriteFile(filepath.Join(partitionPath, "dev"), []byte(fmt.Sprintf("253:%d\n", partIndex)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(partitionPath, "size"), []byte(fmt.Sprintf("%d\n", partition.Size)), 0644)
|
||||
|
||||
// Create dm structure for partition
|
||||
partDmDir := filepath.Join(partitionPath, "dm")
|
||||
_ = os.MkdirAll(partDmDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(partDmDir, "name"), []byte(fmt.Sprintf("%s%s", parentDiskName, partitionSuffix)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(partDmDir, "uuid"), []byte(fmt.Sprintf("part-mpath-%s", partition.UUID)), 0644)
|
||||
|
||||
// Create holder symlink from parent to partition
|
||||
_ = os.WriteFile(filepath.Join(holdersDir, partition.Name), []byte(""), 0644)
|
||||
|
||||
// Create udev data for the partition with multipath-specific entries
|
||||
udevData := []string{
|
||||
fmt.Sprintf("E:ID_FS_LABEL=%s\n", partition.FilesystemLabel),
|
||||
fmt.Sprintf("E:DM_NAME=%s%s\n", parentDiskName, partitionSuffix),
|
||||
fmt.Sprintf("E:DM_PART=%d\n", partNum), // This indicates it's a multipath partition
|
||||
}
|
||||
if partition.FS != "" {
|
||||
udevData = append(udevData, fmt.Sprintf("E:ID_FS_TYPE=%s\n", partition.FS))
|
||||
}
|
||||
if partition.UUID != "" {
|
||||
udevData = append(udevData, fmt.Sprintf("E:ID_PART_ENTRY_UUID=%s\n", partition.UUID))
|
||||
}
|
||||
|
||||
_ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b253:%d", partIndex)), []byte(strings.Join(udevData, "")), 0644)
|
||||
|
||||
// Add mount if specified
|
||||
if partition.MountPoint != "" {
|
||||
if partition.FS == "" {
|
||||
partition.FS = "ext4"
|
||||
}
|
||||
|
||||
var mountDevice string
|
||||
if useDmMount {
|
||||
// Use /dev/dm-<n> format for mounting
|
||||
mountDevice = fmt.Sprintf("/dev/%s", partition.Name)
|
||||
} else {
|
||||
// Use /dev/mapper/<name> format for mounting
|
||||
mountDevice = fmt.Sprintf("/dev/mapper/%s%s", parentDiskName, partitionSuffix)
|
||||
}
|
||||
|
||||
g.mounts = append(
|
||||
g.mounts,
|
||||
fmt.Sprintf("%s %s %s ro,relatime 0 0\n", mountDevice, partition.MountPoint, partition.FS))
|
||||
|
||||
// Rewrite mounts file
|
||||
_ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// createMultipathPartition creates a multipath partition structure using /dev/mapper mount format
|
||||
func (g *GhwMock) createMultipathPartition(parentDiskName string, partition *types.Partition, partNum int) {
|
||||
g.createMultipathPartitionWithMountFormat(parentDiskName, partition, partNum, false)
|
||||
}
|
||||
|
||||
// AddMultipathPartition adds a multipath partition to a multipath device
|
||||
// This creates the partition as a holder of the parent device and sets up
|
||||
// the necessary dm structure for the partition
|
||||
func (g *GhwMock) AddMultipathPartition(parentDiskName string, partition *types.Partition) {
|
||||
if g.paths == nil {
|
||||
return // Must call CreateMultipathDevices first
|
||||
}
|
||||
|
||||
parentDiskPath := filepath.Join(g.paths.SysBlock, parentDiskName)
|
||||
holdersDir := filepath.Join(parentDiskPath, "holders")
|
||||
|
||||
// Count existing holders to determine partition number
|
||||
existingHolders, _ := os.ReadDir(holdersDir)
|
||||
partNum := len(existingHolders) + 1
|
||||
partitionSuffix := fmt.Sprintf("p%d", partNum)
|
||||
|
||||
// Create the partition as a top-level device in /sys/block/
|
||||
partitionPath := filepath.Join(g.paths.SysBlock, partition.Name)
|
||||
_ = os.MkdirAll(partitionPath, 0755)
|
||||
|
||||
// Create partition dev file (use unique device numbers)
|
||||
partIndex := len(g.mounts) + 100 + partNum // Ensure unique device numbers
|
||||
_ = os.WriteFile(filepath.Join(partitionPath, "dev"), []byte(fmt.Sprintf("253:%d\n", partIndex)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(partitionPath, "size"), []byte(fmt.Sprintf("%d\n", partition.Size)), 0644)
|
||||
|
||||
// Create dm structure for partition
|
||||
partDmDir := filepath.Join(partitionPath, "dm")
|
||||
_ = os.MkdirAll(partDmDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(partDmDir, "name"), []byte(fmt.Sprintf("%s%s", parentDiskName, partitionSuffix)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(partDmDir, "uuid"), []byte(fmt.Sprintf("part-mpath-%s", partition.UUID)), 0644)
|
||||
|
||||
// Create holder symlink from parent to partition
|
||||
_ = os.WriteFile(filepath.Join(holdersDir, partition.Name), []byte(""), 0644)
|
||||
|
||||
// Create udev data for the partition with multipath-specific entries
|
||||
udevData := []string{
|
||||
fmt.Sprintf("E:ID_FS_LABEL=%s\n", partition.FilesystemLabel),
|
||||
fmt.Sprintf("E:DM_NAME=%s%s\n", parentDiskName, partitionSuffix),
|
||||
fmt.Sprintf("E:DM_PART=%d\n", partNum), // This indicates it's a multipath partition
|
||||
}
|
||||
if partition.FS != "" {
|
||||
udevData = append(udevData, fmt.Sprintf("E:ID_FS_TYPE=%s\n", partition.FS))
|
||||
}
|
||||
if partition.UUID != "" {
|
||||
udevData = append(udevData, fmt.Sprintf("E:ID_PART_ENTRY_UUID=%s\n", partition.UUID))
|
||||
}
|
||||
|
||||
_ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b253:%d", partIndex)), []byte(strings.Join(udevData, "")), 0644)
|
||||
|
||||
// Add mount if specified
|
||||
if partition.MountPoint != "" {
|
||||
if partition.FS == "" {
|
||||
partition.FS = "ext4"
|
||||
}
|
||||
// For multipath partitions, they can be mounted by /dev/mapper/ name or /dev/dm- name
|
||||
g.mounts = append(
|
||||
g.mounts,
|
||||
fmt.Sprintf("/dev/mapper/%s%s %s %s ro,relatime 0 0\n", parentDiskName, partitionSuffix, partition.MountPoint, partition.FS))
|
||||
}
|
||||
|
||||
// Rewrite mounts file
|
||||
_ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644)
|
||||
}
|
||||
|
107
ghw/multipath_partition_hander.go
Normal file
107
ghw/multipath_partition_hander.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package ghw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
)
|
||||
|
||||
type MultipathPartitionHandler struct {
|
||||
DiskName string
|
||||
}
|
||||
|
||||
func NewMultipathPartitionHandler(diskName string) *MultipathPartitionHandler {
|
||||
return &MultipathPartitionHandler{DiskName: diskName}
|
||||
}
|
||||
|
||||
var _ PartitionHandler = &MultipathPartitionHandler{}
|
||||
|
||||
func (m *MultipathPartitionHandler) GetPartitions(paths *Paths, logger *types.KairosLogger) types.PartitionList {
|
||||
out := make(types.PartitionList, 0)
|
||||
|
||||
// For multipath devices, partitions appear as holders of the parent device
|
||||
// in /sys/block/<disk>/holders/<holder>
|
||||
holdersPath := filepath.Join(paths.SysBlock, m.DiskName, "holders")
|
||||
logger.Logger.Debug().Str("path", holdersPath).Msg("Reading multipath holders")
|
||||
|
||||
holders, err := os.ReadDir(holdersPath)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to read holders directory")
|
||||
return out
|
||||
}
|
||||
|
||||
// Find all multipath partitions by checking each holder
|
||||
for _, holder := range holders {
|
||||
partName := holder.Name()
|
||||
|
||||
// Only consider dm- devices as potential multipath partitions
|
||||
if !isMultipathDevice(holder) {
|
||||
logger.Logger.Debug().Str("path", holder.Name()).Msg("Is not a multipath device")
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify this holder is actually a multipath partition
|
||||
// We can use the holder DirEntry directly - no need to search for it!
|
||||
if !isMultipathPartition(holder, paths, logger) {
|
||||
logger.Logger.Debug().Str("partition", partName).Msg("Holder is not a multipath partition")
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logger.Debug().Str("partition", partName).Msg("Found multipath partition")
|
||||
|
||||
udevInfo, err := udevInfoPartition(paths, partName, logger)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("devNo", partName).Msg("Failed to get udev info")
|
||||
return out
|
||||
}
|
||||
|
||||
mapperName, ok := udevInfo["DM_NAME"]
|
||||
if !ok {
|
||||
logger.Logger.Error().Str("devNo", partName).Msg("DM_NAME not found in udev info")
|
||||
continue
|
||||
}
|
||||
|
||||
// For multipath partitions, we need to get size directly from the partition device
|
||||
// since it's a top-level entry in /sys/block, not nested under the parent
|
||||
size := partitionSizeBytes(paths, partName, logger)
|
||||
du := diskPartUUID(paths, partName, logger)
|
||||
|
||||
// The mount point is usually the same as the mapper name
|
||||
// however you can also mount it as /dev/dm-<n> or /dev/mapper/<mapperName>
|
||||
// so we need to check both
|
||||
potentialMountNames := []string{
|
||||
filepath.Join("/dev/mapper", mapperName),
|
||||
filepath.Join("/dev", partName),
|
||||
}
|
||||
|
||||
// Search for the mount point in the system
|
||||
var mp, pt string
|
||||
for _, mountName := range potentialMountNames {
|
||||
mp, pt = partitionInfo(paths, mountName, logger)
|
||||
if mp != "" {
|
||||
logger.Logger.Debug().Str("mountPoint", mp).Msg("Found mount point for partition")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pt == "" {
|
||||
pt = diskPartTypeUdev(paths, partName, logger)
|
||||
}
|
||||
fsLabel := diskFSLabel(paths, partName, logger)
|
||||
|
||||
p := &types.Partition{
|
||||
Name: partName,
|
||||
Size: uint(size / (1024 * 1024)),
|
||||
MountPoint: mp,
|
||||
UUID: du,
|
||||
FilesystemLabel: fsLabel,
|
||||
FS: pt,
|
||||
Path: filepath.Join("/dev", partName),
|
||||
Disk: filepath.Join("/dev", m.DiskName),
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
202
ghw/partitions.go
Normal file
202
ghw/partitions.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package ghw
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
)
|
||||
|
||||
type PartitionHandler interface {
|
||||
// GetPartitions returns a list of partitions on the system.
|
||||
GetPartitions(paths *Paths, logger *types.KairosLogger) types.PartitionList
|
||||
}
|
||||
|
||||
func partitionSizeBytes(paths *Paths, partitionPath string, logger *types.KairosLogger) uint64 {
|
||||
path := filepath.Join(paths.SysBlock, partitionPath, "size")
|
||||
logger.Logger.Debug().Str("file", path).Msg("Reading size file")
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("file", path).Err(err).Msg("failed to read disk partition size")
|
||||
return 0
|
||||
}
|
||||
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("contents", string(contents)).Err(err).Msg("failed to parse disk partition size")
|
||||
return 0
|
||||
}
|
||||
logger.Logger.Trace().Str("partition", partitionPath).Uint64("size", size*sectorSize).Msg("Got partition size")
|
||||
return size * sectorSize
|
||||
}
|
||||
|
||||
func partitionInfo(paths *Paths, part string, logger *types.KairosLogger) (string, string) {
|
||||
// Allow calling PartitionInfo with either the full partition name
|
||||
// "/dev/sda1" or just "sda1"
|
||||
if !strings.HasPrefix(part, "/dev") {
|
||||
part = "/dev/" + part
|
||||
}
|
||||
|
||||
// mount entries for mounted partitions look like this:
|
||||
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
||||
var r io.ReadCloser
|
||||
logger.Logger.Debug().Str("file", paths.ProcMounts).Msg("Reading mounts file")
|
||||
r, err := os.Open(paths.ProcMounts)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("file", paths.ProcMounts).Err(err).Msg("failed to open mounts")
|
||||
return "", ""
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.Logger.Trace().Str("line", line).Msg("Parsing mount info")
|
||||
entry := parseMountEntry(line, logger)
|
||||
if entry == nil || entry.Partition != part {
|
||||
continue
|
||||
}
|
||||
|
||||
return entry.Mountpoint, entry.FilesystemType
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type mountEntry struct {
|
||||
Partition string
|
||||
Mountpoint string
|
||||
FilesystemType string
|
||||
}
|
||||
|
||||
func parseMountEntry(line string, logger *types.KairosLogger) *mountEntry {
|
||||
// mount entries for mounted partitions look like this:
|
||||
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
||||
if line[0] != '/' {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) < 4 {
|
||||
logger.Logger.Debug().Interface("fields", fields).Msg("Mount line has less than 4 fields")
|
||||
return nil
|
||||
}
|
||||
|
||||
// We do some special parsing of the mountpoint, which may contain space,
|
||||
// tab and newline characters, encoded into the mount entry line using their
|
||||
// octal-to-string representations. From the GNU mtab man pages:
|
||||
//
|
||||
// "Therefore these characters are encoded in the files and the getmntent
|
||||
// function takes care of the decoding while reading the entries back in.
|
||||
// '\040' is used to encode a space character, '\011' to encode a tab
|
||||
// character, '\012' to encode a newline character, and '\\' to encode a
|
||||
// backslash."
|
||||
mp := fields[1]
|
||||
r := strings.NewReplacer(
|
||||
"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
|
||||
)
|
||||
mp = r.Replace(mp)
|
||||
|
||||
res := &mountEntry{
|
||||
Partition: fields[0],
|
||||
Mountpoint: mp,
|
||||
FilesystemType: fields[2],
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func diskUUID(paths *Paths, partitionPath string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, partitionPath, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk UUID")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("partition", partitionPath).Interface("info", info).Err(err).Msg("failed to read disk UUID")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_PART_TABLE_UUID"]; ok {
|
||||
logger.Logger.Trace().Str("disk", partitionPath).Str("partition", partitionPath).Str("uuid", pType).Msg("Got disk uuid")
|
||||
return pType
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func diskPartUUID(paths *Paths, partitionPath string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, partitionPath, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk Part UUID")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("partition", partitionPath).Interface("info", info).Err(err).Msg("Disk Part UUID")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_PART_ENTRY_UUID"]; ok {
|
||||
logger.Logger.Trace().Str("partition", partitionPath).Str("uuid", pType).Msg("Got partition uuid")
|
||||
return pType
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
// diskPartTypeUdev gets the partition type from the udev database directly and its only used as fallback when
|
||||
// the partition is not mounted, so we cannot get the type from paths.ProcMounts from the partitionInfo function
|
||||
func diskPartTypeUdev(paths *Paths, partitionPath string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, partitionPath, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk Part Type")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("partition", partitionPath).Interface("info", info).Err(err).Msg("Disk Part Type")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if pType, ok := info["ID_FS_TYPE"]; ok {
|
||||
logger.Logger.Trace().Str("partition", partitionPath).Str("FS", pType).Msg("Got partition fs type")
|
||||
return pType
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func diskFSLabel(paths *Paths, partitionPath string, logger *types.KairosLogger) string {
|
||||
info, err := udevInfoPartition(paths, partitionPath, logger)
|
||||
logger.Logger.Trace().Interface("info", info).Msg("Disk FS label")
|
||||
if err != nil {
|
||||
logger.Logger.Error().Str("partition", partitionPath).Interface("info", info).Err(err).Msg("Disk FS label")
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if label, ok := info["ID_FS_LABEL"]; ok {
|
||||
logger.Logger.Trace().Str("partition", partitionPath).Str("uuid", label).Msg("Got partition label")
|
||||
return label
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func udevInfoPartition(paths *Paths, partitionPath string, logger *types.KairosLogger) (map[string]string, error) {
|
||||
// Get device major:minor numbers
|
||||
devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, partitionPath, "dev"))
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.SysBlock, partitionPath, "dev")).Msg("failed to read udev info")
|
||||
return nil, err
|
||||
}
|
||||
return UdevInfo(paths, string(devNo), logger)
|
||||
}
|
||||
|
||||
// UdevInfo will return information on udev database about a device number
|
||||
func UdevInfo(paths *Paths, devNo string, logger *types.KairosLogger) (map[string]string, error) {
|
||||
// Look up block device in udev runtime database
|
||||
udevID := "b" + strings.TrimSpace(devNo)
|
||||
udevBytes, err := os.ReadFile(filepath.Join(paths.RunUdevData, udevID))
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.RunUdevData, udevID)).Msg("failed to read udev info for device")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udevInfo := make(map[string]string)
|
||||
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
||||
if strings.HasPrefix(udevLine, "E:") {
|
||||
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
||||
udevInfo[s[0]] = s[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return udevInfo, nil
|
||||
}
|
Reference in New Issue
Block a user