diff --git a/ghw/ghw.go b/ghw/ghw.go index 7fb3029..0bf0d31 100644 --- a/ghw/ghw.go +++ b/ghw/ghw.go @@ -17,13 +17,6 @@ const ( UNKNOWN = "unknown" ) -type Disk struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - SizeBytes uint64 `json:"size_bytes,omitempty" yaml:"size_bytes,omitempty"` - UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` - Partitions types.PartitionList `json:"partitions,omitempty" yaml:"partitions,omitempty"` -} - type Paths struct { SysBlock string RunUdevData string @@ -56,12 +49,12 @@ func NewPaths(withOptionalPrefix string) *Paths { return p } -func GetDisks(paths *Paths, logger *types.KairosLogger) []*Disk { +func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk { if logger == nil { newLogger := types.NewKairosLogger("ghw", "info", false) logger = &newLogger } - disks := make([]*Disk, 0) + disks := make([]*types.Disk, 0) logger.Logger.Debug().Str("path", paths.SysBlock).Msg("Scanning for disks") files, err := os.ReadDir(paths.SysBlock) if err != nil { @@ -76,7 +69,7 @@ func GetDisks(paths *Paths, logger *types.KairosLogger) []*Disk { // We don't care about unused loop devices... continue } - d := &Disk{ + d := &types.Disk{ Name: dname, SizeBytes: size, UUID: diskUUID(paths, dname, "", logger), diff --git a/ghw/ghw_test.go b/ghw/ghw_test.go index 7ac1e6b..cfed87e 100644 --- a/ghw/ghw_test.go +++ b/ghw/ghw_test.go @@ -1,15 +1,11 @@ package ghw_test import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" + "testing" "github.com/kairos-io/kairos-sdk/ghw" + "github.com/kairos-io/kairos-sdk/ghw/mocks" "github.com/kairos-io/kairos-sdk/types" - "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -21,16 +17,16 @@ func TestGHW(t *testing.T) { } var _ = Describe("GHW functions tests", func() { - var ghwMock GhwMock + var ghwMock mocks.GhwMock BeforeEach(func() { - ghwMock = GhwMock{} + ghwMock = mocks.GhwMock{} }) AfterEach(func() { ghwMock.Clean() }) Describe("With a disk", func() { BeforeEach(func() { - mainDisk := ghw.Disk{ + mainDisk := types.Disk{ Name: "disk", UUID: "555", SizeBytes: 1 * 1024, @@ -51,7 +47,7 @@ var _ = Describe("GHW functions tests", func() { }) It("Finds the disk and partition", func() { - disks := ghw.GetDisks(ghw.NewPaths(ghwMock.chroot), nil) + disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil) Expect(len(disks)).To(Equal(1), disks) Expect(disks[0].Name).To(Equal("disk"), disks) Expect(disks[0].UUID).To(Equal("555"), disks) @@ -68,159 +64,9 @@ var _ = Describe("GHW functions tests", func() { Describe("With no disks", func() { It("Finds nothing", func() { ghwMock.CreateDevices() - disks := ghw.GetDisks(ghw.NewPaths(ghwMock.chroot), nil) + disks := ghw.GetDisks(ghw.NewPaths(ghwMock.Chroot), nil) Expect(len(disks)).To(Equal(0), disks) }) }) }) - -// GhwMock is used to construct a fake disk to present to ghw when scanning block devices -// The way this works is ghw will use the existing files in the system to determine the different disks, partitions and -// mountpoints. It uses /sys/block, /proc/self/mounts and /run/udev/data to gather everything -// It also has an entrypoint to overwrite the root dir from which the paths are constructed so that allows us to override -// it easily and make it read from a different location. -// This mock is used to construct a fake FS with all its needed files on a different chroot and just add a Disk with its -// partitions and let the struct do its thing creating files and mountpoints and such -// You can even just pass no disks to simulate a system in which there is no disk/no cos partitions -type GhwMock struct { - chroot string - paths *ghw.Paths - disks []ghw.Disk - mounts []string -} - -// AddDisk adds a disk to GhwMock -func (g *GhwMock) AddDisk(disk ghw.Disk) { - g.disks = append(g.disks, disk) -} - -// AddPartitionToDisk will add a partition to the given disk and call Clean+CreateDevices, so we recreate all files -// It makes no effort checking if the disk exists -func (g *GhwMock) AddPartitionToDisk(diskName string, partition *types.Partition) { - for _, disk := range g.disks { - if disk.Name == diskName { - disk.Partitions = append(disk.Partitions, partition) - g.Clean() - g.CreateDevices() - } - } -} - -// CreateDevices will create a new context and paths for ghw using the Chroot value as base, then set the env var GHW_ROOT so the -// ghw library picks that up and then iterate over the disks and partitions and create the necessary files -func (g *GhwMock) CreateDevices() { - d, _ := os.MkdirTemp("", "ghwmock") - g.chroot = d - g.paths = ghw.NewPaths(d) - // Create the /sys/block dir - _ = os.MkdirAll(g.paths.SysBlock, 0755) - // Create the /run/udev/data dir - _ = os.MkdirAll(g.paths.RunUdevData, 0755) - // Create only the /proc/ dir, we add the mounts file afterwards - procDir, _ := filepath.Split(g.paths.ProcMounts) - _ = os.MkdirAll(procDir, 0755) - for indexDisk, disk := range g.disks { - // For each dir we create the /sys/block/DISK_NAME - diskPath := filepath.Join(g.paths.SysBlock, disk.Name) - _ = os.Mkdir(diskPath, 0755) - // We create a dev file to indicate the devicenumber for a given disk - _ = os.WriteFile(filepath.Join(g.paths.SysBlock, disk.Name, "dev"), []byte(fmt.Sprintf("%d:0\n", indexDisk)), 0644) - // Also write the size - _ = os.WriteFile(filepath.Join(g.paths.SysBlock, disk.Name, "size"), []byte(strconv.FormatUint(disk.SizeBytes, 10)), 0644) - // Create the udevdata for this disk - _ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%d:0", indexDisk)), []byte(fmt.Sprintf("E:ID_PART_TABLE_UUID=%s\n", disk.UUID)), 0644) - for indexPart, partition := range disk.Partitions { - // For each partition we create the /sys/block/DISK_NAME/PARTITION_NAME - _ = os.Mkdir(filepath.Join(diskPath, partition.Name), 0755) - // Create the /sys/block/DISK_NAME/PARTITION_NAME/dev file which contains the major:minor of the partition - _ = os.WriteFile(filepath.Join(diskPath, partition.Name, "dev"), []byte(fmt.Sprintf("%d:6%d\n", indexDisk, indexPart)), 0644) - _ = os.WriteFile(filepath.Join(diskPath, partition.Name, "size"), []byte(fmt.Sprintf("%d\n", partition.Size)), 0644) - // Create the /run/udev/data/bMAJOR:MINOR file with the data inside to mimic the udev database - data := []string{fmt.Sprintf("E:ID_FS_LABEL=%s\n", partition.FilesystemLabel)} - if partition.FS != "" { - data = append(data, fmt.Sprintf("E:ID_FS_TYPE=%s\n", partition.FS)) - } - if partition.UUID != "" { - data = append(data, fmt.Sprintf("E:ID_PART_ENTRY_UUID=%s\n", partition.UUID)) - } - _ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%d:6%d", indexDisk, indexPart)), []byte(strings.Join(data, "")), 0644) - // If we got a mountpoint, add it to our fake /proc/self/mounts - if partition.MountPoint != "" { - // Check if the partition has a fs, otherwise default to ext4 - if partition.FS == "" { - partition.FS = "ext4" - } - // Prepare the g.mounts with all the mount lines - g.mounts = append( - g.mounts, - fmt.Sprintf("%s %s %s ro,relatime 0 0\n", filepath.Join("/dev", partition.Name), partition.MountPoint, partition.FS)) - } - } - } - // Finally, write all the mounts - _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) -} - -// RemoveDisk will remove the files for a disk. It makes no effort to check if the disk exists or not -func (g *GhwMock) RemoveDisk(disk string) { - // This could be simpler I think, just removing the /sys/block/DEVICE should make ghw not find anything and not search - // for partitions, but just in case do it properly - var newMounts []string - diskPath := filepath.Join(g.paths.SysBlock, disk) - _ = os.RemoveAll(diskPath) - - // Try to find any mounts that match the disk given and remove them from the mounts - for _, mount := range g.mounts { - fields := strings.Fields(mount) - // If first field does not contain the /dev/DEVICE, add it to the newmounts - if !strings.Contains(fields[0], filepath.Join("/dev", disk)) { - newMounts = append(newMounts, mount) - } - } - g.mounts = newMounts - // Write the mounts again - _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) -} - -// RemovePartitionFromDisk will remove the files for a partition -// It makes no effort checking if the disk/partition/files exist -func (g *GhwMock) RemovePartitionFromDisk(diskName string, partitionName string) { - var newMounts []string - diskPath := filepath.Join(g.paths.SysBlock, diskName) - // Read the dev major:minor - devName, _ := os.ReadFile(filepath.Join(diskPath, partitionName, "dev")) - // Remove the MAJOR:MINOR file from the udev database - _ = os.RemoveAll(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%s", devName))) - // Remove the /sys/block/DISK/PARTITION dir - _ = os.RemoveAll(filepath.Join(diskPath, partitionName)) - - // Try to find any mounts that match the partition given and remove them from the mounts - for _, mount := range g.mounts { - fields := strings.Fields(mount) - // If first field does not contain the /dev/PARTITION, add it to the newmounts - if !strings.Contains(fields[0], filepath.Join("/dev", partitionName)) { - newMounts = append(newMounts, mount) - } - } - g.mounts = newMounts - // Write the mounts again - _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) - // Remove it from the partitions list - for index, disk := range g.disks { - if disk.Name == diskName { - var newPartitions types.PartitionList - for _, partition := range disk.Partitions { - if partition.Name != partitionName { - newPartitions = append(newPartitions, partition) - } - } - g.disks[index].Partitions = newPartitions - } - } -} - -// Clean will remove the chroot dir and unset the env var -func (g *GhwMock) Clean() { - _ = os.RemoveAll(g.chroot) -} diff --git a/ghw/mocks/ghw_mock.go b/ghw/mocks/ghw_mock.go new file mode 100644 index 0000000..2684755 --- /dev/null +++ b/ghw/mocks/ghw_mock.go @@ -0,0 +1,161 @@ +package mocks + +import ( + "fmt" + "github.com/kairos-io/kairos-sdk/ghw" + "github.com/kairos-io/kairos-sdk/types" + "os" + "path/filepath" + "strconv" + "strings" +) + +// GhwMock is used to construct a fake disk to present to ghw when scanning block devices +// The way this works is ghw will use the existing files in the system to determine the different disks, partitions and +// mountpoints. It uses /sys/block, /proc/self/mounts and /run/udev/data to gather everything +// It also has an entrypoint to overwrite the root dir from which the paths are constructed so that allows us to override +// it easily and make it read from a different location. +// This mock is used to construct a fake FS with all its needed files on a different chroot and just add a Disk with its +// partitions and let the struct do its thing creating files and mountpoints and such +// You can even just pass no disks to simulate a system in which there is no disk/no cos partitions +type GhwMock struct { + Chroot string + paths *ghw.Paths + disks []types.Disk + mounts []string +} + +// AddDisk adds a disk to GhwMock +func (g *GhwMock) AddDisk(disk types.Disk) { + g.disks = append(g.disks, disk) +} + +// AddPartitionToDisk will add a partition to the given disk and call Clean+CreateDevices, so we recreate all files +// It makes no effort checking if the disk exists +func (g *GhwMock) AddPartitionToDisk(diskName string, partition *types.Partition) { + for _, disk := range g.disks { + if disk.Name == diskName { + disk.Partitions = append(disk.Partitions, partition) + g.Clean() + g.CreateDevices() + } + } +} + +// CreateDevices will create a new context and paths for ghw using the Chroot value as base, then set the env var GHW_ROOT so the +// ghw library picks that up and then iterate over the disks and partitions and create the necessary files +func (g *GhwMock) CreateDevices() { + d, _ := os.MkdirTemp("", "ghwmock") + g.Chroot = d + g.paths = ghw.NewPaths(d) + // Create the /sys/block dir + _ = os.MkdirAll(g.paths.SysBlock, 0755) + // Create the /run/udev/data dir + _ = os.MkdirAll(g.paths.RunUdevData, 0755) + // Create only the /proc/ dir, we add the mounts file afterwards + procDir, _ := filepath.Split(g.paths.ProcMounts) + _ = os.MkdirAll(procDir, 0755) + for indexDisk, disk := range g.disks { + // For each dir we create the /sys/block/DISK_NAME + diskPath := filepath.Join(g.paths.SysBlock, disk.Name) + _ = os.Mkdir(diskPath, 0755) + // We create a dev file to indicate the devicenumber for a given disk + _ = os.WriteFile(filepath.Join(g.paths.SysBlock, disk.Name, "dev"), []byte(fmt.Sprintf("%d:0\n", indexDisk)), 0644) + // Also write the size + _ = os.WriteFile(filepath.Join(g.paths.SysBlock, disk.Name, "size"), []byte(strconv.FormatUint(disk.SizeBytes, 10)), 0644) + // Create the udevdata for this disk + _ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%d:0", indexDisk)), []byte(fmt.Sprintf("E:ID_PART_TABLE_UUID=%s\n", disk.UUID)), 0644) + for indexPart, partition := range disk.Partitions { + // For each partition we create the /sys/block/DISK_NAME/PARTITION_NAME + _ = os.Mkdir(filepath.Join(diskPath, partition.Name), 0755) + // Create the /sys/block/DISK_NAME/PARTITION_NAME/dev file which contains the major:minor of the partition + _ = os.WriteFile(filepath.Join(diskPath, partition.Name, "dev"), []byte(fmt.Sprintf("%d:6%d\n", indexDisk, indexPart)), 0644) + _ = os.WriteFile(filepath.Join(diskPath, partition.Name, "size"), []byte(fmt.Sprintf("%d\n", partition.Size)), 0644) + // Create the /run/udev/data/bMAJOR:MINOR file with the data inside to mimic the udev database + data := []string{fmt.Sprintf("E:ID_FS_LABEL=%s\n", partition.FilesystemLabel)} + if partition.FS != "" { + data = append(data, fmt.Sprintf("E:ID_FS_TYPE=%s\n", partition.FS)) + } + if partition.UUID != "" { + data = append(data, fmt.Sprintf("E:ID_PART_ENTRY_UUID=%s\n", partition.UUID)) + } + _ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%d:6%d", indexDisk, indexPart)), []byte(strings.Join(data, "")), 0644) + // If we got a mountpoint, add it to our fake /proc/self/mounts + if partition.MountPoint != "" { + // Check if the partition has a fs, otherwise default to ext4 + if partition.FS == "" { + partition.FS = "ext4" + } + // Prepare the g.mounts with all the mount lines + g.mounts = append( + g.mounts, + fmt.Sprintf("%s %s %s ro,relatime 0 0\n", filepath.Join("/dev", partition.Name), partition.MountPoint, partition.FS)) + } + } + } + // Finally, write all the mounts + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) +} + +// RemoveDisk will remove the files for a disk. It makes no effort to check if the disk exists or not +func (g *GhwMock) RemoveDisk(disk string) { + // This could be simpler I think, just removing the /sys/block/DEVICE should make ghw not find anything and not search + // for partitions, but just in case do it properly + var newMounts []string + diskPath := filepath.Join(g.paths.SysBlock, disk) + _ = os.RemoveAll(diskPath) + + // Try to find any mounts that match the disk given and remove them from the mounts + for _, mount := range g.mounts { + fields := strings.Fields(mount) + // If first field does not contain the /dev/DEVICE, add it to the newmounts + if !strings.Contains(fields[0], filepath.Join("/dev", disk)) { + newMounts = append(newMounts, mount) + } + } + g.mounts = newMounts + // Write the mounts again + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) +} + +// RemovePartitionFromDisk will remove the files for a partition +// It makes no effort checking if the disk/partition/files exist +func (g *GhwMock) RemovePartitionFromDisk(diskName string, partitionName string) { + var newMounts []string + diskPath := filepath.Join(g.paths.SysBlock, diskName) + // Read the dev major:minor + devName, _ := os.ReadFile(filepath.Join(diskPath, partitionName, "dev")) + // Remove the MAJOR:MINOR file from the udev database + _ = os.RemoveAll(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%s", devName))) + // Remove the /sys/block/DISK/PARTITION dir + _ = os.RemoveAll(filepath.Join(diskPath, partitionName)) + + // Try to find any mounts that match the partition given and remove them from the mounts + for _, mount := range g.mounts { + fields := strings.Fields(mount) + // If first field does not contain the /dev/PARTITION, add it to the newmounts + if !strings.Contains(fields[0], filepath.Join("/dev", partitionName)) { + newMounts = append(newMounts, mount) + } + } + g.mounts = newMounts + // Write the mounts again + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) + // Remove it from the partitions list + for index, disk := range g.disks { + if disk.Name == diskName { + var newPartitions types.PartitionList + for _, partition := range disk.Partitions { + if partition.Name != partitionName { + newPartitions = append(newPartitions, partition) + } + } + g.disks[index].Partitions = newPartitions + } + } +} + +// Clean will remove the chroot dir and unset the env var +func (g *GhwMock) Clean() { + _ = os.RemoveAll(g.Chroot) +} diff --git a/types/partitions.go b/types/partitions.go index cda7c01..c2edfd0 100644 --- a/types/partitions.go +++ b/types/partitions.go @@ -13,3 +13,10 @@ type Partition struct { } type PartitionList []*Partition + +type Disk struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + SizeBytes uint64 `json:"size_bytes,omitempty" yaml:"size_bytes,omitempty"` + UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` + Partitions PartitionList `json:"partitions,omitempty" yaml:"partitions,omitempty"` +}