diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ef94ff9c57..a01ecd8dcd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,7 +5,7 @@ # [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster ARG VARIANT="1.19-bullseye" -FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} +FROM mcr.microsoft.com/vscode/devcontainers/go:1-${VARIANT} # [Choice] Node.js version: none, lts/*, 18, 16, 14 ARG NODE_VERSION="none" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4688a5d01e..1c70236b2e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ // Update the VARIANT arg to pick a version of Go: 1, 1.19, 1.18 // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local arm64/Apple Silicon. - "VARIANT": "1.19", + "VARIANT": "1.23-bullseye", // Options "NODE_VERSION": "none" } diff --git a/providers/os/connection/snapshot/blockdevices.go b/providers/os/connection/snapshot/blockdevices.go index f72bbe879c..a00fbb1110 100644 --- a/providers/os/connection/snapshot/blockdevices.go +++ b/providers/os/connection/snapshot/blockdevices.go @@ -8,7 +8,10 @@ import ( "errors" "fmt" "io" + "math" + "os" "sort" + "strconv" "strings" "github.com/rs/zerolog/log" @@ -25,7 +28,27 @@ type BlockDevice struct { Uuid string `json:"uuid,omitempty"` MountPoint string `json:"mountpoint,omitempty"` Children []BlockDevice `json:"children,omitempty"` - Size int `json:"size,omitempty"` + Size Size `json:"size,omitempty"` + + Aliases []string `json:"-"` +} + +type Size int64 + +func (s *Size) UnmarshalJSON(data []byte) error { + var size any + if err := json.Unmarshal(data, &size); err != nil { + return err + } + switch size := size.(type) { + case string: + isize, err := strconv.Atoi(size) + *s = Size(isize) + return err + case float64: + *s = Size(size) + } + return nil } type PartitionInfo struct { @@ -53,9 +76,51 @@ func (cmdRunner *LocalCommandRunner) GetBlockDevices() (*BlockDevices, error) { if err := json.Unmarshal(data, blockEntries); err != nil { return nil, err } + blockEntries.FindAliases() + return blockEntries, nil } +func (blockEntries *BlockDevices) FindAliases() { + entries, err := os.ReadDir("/dev") + if err != nil { + log.Warn().Err(err).Msg("Can't read /dev directory") + return + } + + for _, entry := range entries { + if entry.Type().Type() != os.ModeSymlink { + continue + } + + path := fmt.Sprintf("/dev/%s", entry.Name()) + target, err := os.Readlink(path) + if err != nil { + log.Warn().Err(err).Str("path", path).Msg("Can't read link target") + continue + } + + targetName := strings.TrimPrefix(target, "/dev/") + blockEntries.findAlias(targetName, path) + } +} + +func (blockEntries *BlockDevices) findAlias(alias, path string) { + for i := range blockEntries.BlockDevices { + device := blockEntries.BlockDevices[i] + if alias == device.Name { + log.Debug(). + Str("alias", alias). + Str("path", path). + Str("name", device.Name). + Msg("found alias") + device.Aliases = append(device.Aliases, path) + blockEntries.BlockDevices[i] = device + return + } + } +} + func (blockEntries BlockDevices) GetRootBlockEntry() (*PartitionInfo, error) { log.Debug().Msg("get root block entry") for i := range blockEntries.BlockDevices { @@ -106,7 +171,7 @@ func (blockEntries BlockDevices) GetMountablePartitionByDevice(device string) (* } for _, partition := range block.Children { - log.Debug().Str("name", partition.Name).Int("size", partition.Size).Msg("checking partition") + log.Debug().Str("name", partition.Name).Int64("size", int64(partition.Size)).Msg("checking partition") if partition.IsNotBootOrRootVolumeAndUnmounted() { log.Debug().Str("name", partition.Name).Msg("found suitable partition") partitions = append(partitions, partition) @@ -125,29 +190,74 @@ func (blockEntries BlockDevices) GetMountablePartitionByDevice(device string) (* return &PartitionInfo{Name: devFsName, FsType: partitions[0].FsType}, nil } +// LongestMatchingSuffix returns the length of the longest common suffix of two strings +// and caches the result (lengths of the matching suffix) for future calls with the same string +func LongestMatchingSuffix(s1, s2 string) int { + n1 := len(s1) + n2 := len(s2) + + // Start from the end of both strings + i := 0 + for i < int(math.Min(float64(n1), float64(n2))) && s1[n1-i-1] == s2[n2-i-1] { + i++ + } + + return i +} + // Searches for a device by name -func (blockEntries BlockDevices) FindDevice(name string) (BlockDevice, error) { - log.Debug().Str("device", name).Msg("searching for device") - var secondName string - if strings.HasPrefix(name, "/dev/sd") { - // sdh and xvdh are interchangeable - end := strings.TrimPrefix(name, "/dev/sd") - secondName = "/dev/xvd" + end +func (blockEntries BlockDevices) FindDevice(requested string) (BlockDevice, error) { + log.Debug().Str("device", requested).Msg("searching for device") + + devices := blockEntries.BlockDevices + if len(devices) == 0 { + return BlockDevice{}, fmt.Errorf("no block devices found") } - for i := range blockEntries.BlockDevices { - d := blockEntries.BlockDevices[i] - log.Debug().Str("name", d.Name).Interface("children", d.Children).Interface("mountpoint", d.MountPoint).Msg("found block device") - fullDeviceName := "/dev/" + d.Name - if name != fullDeviceName { // check if the device name matches - if secondName == "" || secondName != fullDeviceName { - continue + + requestedName := strings.TrimPrefix(requested, "/dev/") + lmsCache := map[string]int{} + bestMatch := struct { + Device BlockDevice + Lms int + }{ + Device: BlockDevice{}, + Lms: 0, + } + + for _, d := range devices { + log.Debug(). + Str("name", d.Name). + Strs("aliases", d.Aliases). + Msg("checking device") + if d.Name == requestedName { + return d, nil + } + + lms := LongestMatchingSuffix(requested, d.Name) + for _, alias := range d.Aliases { + aliasLms := LongestMatchingSuffix(requested, alias) + if aliasLms > lms { + lms = aliasLms + lmsCache[d.Name] = aliasLms } } - log.Debug().Str("name", d.Name).Msg("found matching device") - return d, nil + + if lms > bestMatch.Lms { + bestMatch.Device = d + bestMatch.Lms = lms + } } - return BlockDevice{}, fmt.Errorf("no block device found with name %s", name) + if bestMatch.Lms > 0 { + return bestMatch.Device, nil + } + + log.Debug(). + Str("device", requested). + Any("checked_names", lmsCache). + Msg("no device found") + + return BlockDevice{}, fmt.Errorf("no block device found with name %s", requested) } // Searches all the partitions in the device and finds one that can be mounted. It must be unmounted, non-boot partition @@ -169,7 +279,7 @@ func (device BlockDevice) GetMountablePartitions(includeAll bool) ([]*PartitionI partitions := []*PartitionInfo{} for _, partition := range blockDevices { - log.Debug().Str("name", partition.Name).Int("size", partition.Size).Msg("checking partition") + log.Debug().Str("name", partition.Name).Int64("size", int64(partition.Size)).Msg("checking partition") if partition.FsType == "" { log.Debug().Str("name", partition.Name).Msg("skipping partition without filesystem type") continue diff --git a/providers/os/connection/snapshot/blockdevices_test.go b/providers/os/connection/snapshot/blockdevices_test.go index a37be3b0eb..3dda92847d 100644 --- a/providers/os/connection/snapshot/blockdevices_test.go +++ b/providers/os/connection/snapshot/blockdevices_test.go @@ -12,6 +12,54 @@ import ( "github.com/stretchr/testify/require" ) +func TestBlockDevicesUnmarshal(t *testing.T) { + common := `{ + "blockdevices": [ + {"name": "nvme1n1", "size": 8589934592, "fstype": null, "mountpoint": null, "label": null, "uuid": null, + "children": [ + {"name": "nvme1n1p1", "size": 7515127296, "fstype": "ext4", "mountpoint": null, "label": "cloudimg-rootfs", "uuid": "d84ccd9b-0384-4314-88be-5bd38eb59f30"}, + {"name": "nvme1n1p14", "size": 4194304, "fstype": null, "mountpoint": null, "label": null, "uuid": null}, + {"name": "nvme1n1p15", "size": 111149056, "fstype": "vfat", "mountpoint": null, "label": "UEFI", "uuid": "9601-9938"}, + {"name": "nvme1n1p16", "size": 957350400, "fstype": "ext4", "mountpoint": null, "label": "BOOT", "uuid": "c2032e48-1c8e-4f92-87c6-9db270bf4274"} + ] + }, + {"name": "nvme0n1", "size": "8589934592", "fstype": null, "mountpoint": null, "label": null, "uuid": null, + "children": [ + {"name": "nvme0n1p1", "size": 8578383360, "fstype": "xfs", "mountpoint": "/", "label": "/", "uuid": "804f6603-f3df-4054-8161-50bd9cbd9cf9"}, + {"name": "nvme0n1p128", "size": 10485760, "fstype": "vfat", "mountpoint": "/boot/efi", "label": null, "uuid": "BCB5-3E0E"} + ] + } + ] +}` + + blockEntries := &BlockDevices{} + err := json.Unmarshal([]byte(common), blockEntries) + require.NoError(t, err) + + stringer := `{ + "blockdevices": [ + {"name": "nvme1n1", "size": "8589934592", "fstype": null, "mountpoint": null, "label": null, "uuid": null, + "children": [ + {"name": "nvme1n1p1", "size": "7515127296", "fstype": "ext4", "mountpoint": null, "label": "cloudimg-rootfs", "uuid": "d84ccd9b-0384-4314-88be-5bd38eb59f30"}, + {"name": "nvme1n1p14", "size": "4194304", "fstype": null, "mountpoint": null, "label": null, "uuid": null}, + {"name": "nvme1n1p15", "size": "111149056", "fstype": "vfat", "mountpoint": null, "label": "UEFI", "uuid": "9601-9938"}, + {"name": "nvme1n1p16", "size": "957350400", "fstype": "ext4", "mountpoint": null, "label": "BOOT", "uuid": "c2032e48-1c8e-4f92-87c6-9db270bf4274"} + ] + }, + {"name": "nvme0n1", "size": "8589934592", "fstype": null, "mountpoint": null, "label": null, "uuid": null, + "children": [ + {"name": "nvme0n1p1", "size": "8578383360", "fstype": "xfs", "mountpoint": "/", "label": "/", "uuid": "804f6603-f3df-4054-8161-50bd9cbd9cf9"}, + {"name": "nvme0n1p128", "size": "10485760", "fstype": "vfat", "mountpoint": "/boot/efi", "label": null, "uuid": "BCB5-3E0E"} + ] + } + ] +}` + + blockEntries = &BlockDevices{} + err = json.Unmarshal([]byte(stringer), blockEntries) + require.NoError(t, err) +} + func TestGetMountablePartitionByDevice(t *testing.T) { t.Run("match by exact name", func(t *testing.T) { blockEntries := BlockDevices{ @@ -128,9 +176,25 @@ func TestFindDevice(t *testing.T) { }, } + expected := blockEntries.BlockDevices[2] res, err := blockEntries.FindDevice("/dev/sdx") require.Nil(t, err) - require.Equal(t, res, blockEntries.BlockDevices[2]) + require.Equal(t, expected, res) + }) + + t.Run("match by alias name", func(t *testing.T) { + blockEntries := BlockDevices{ + BlockDevices: []BlockDevice{ + {Name: "sda", Children: []BlockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}}, + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "sdx", Aliases: []string{"xvdx"}, Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + }, + } + + expected := blockEntries.BlockDevices[2] + res, err := blockEntries.FindDevice("/dev/xvdx") + require.Nil(t, err) + require.Equal(t, expected, res) }) t.Run("match by interchangeable name", func(t *testing.T) { @@ -142,9 +206,10 @@ func TestFindDevice(t *testing.T) { }, } + expected := blockEntries.BlockDevices[2] res, err := blockEntries.FindDevice("/dev/sdc") require.Nil(t, err) - require.Equal(t, res, blockEntries.BlockDevices[2]) + require.Equal(t, expected, res) }) t.Run("no match", func(t *testing.T) { @@ -160,6 +225,54 @@ func TestFindDevice(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "no block device found with name") }) + + t.Run("multiple matches by trailing letter", func(t *testing.T) { + blockEntries := BlockDevices{ + BlockDevices: []BlockDevice{ + {Name: "sda", Children: []BlockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}}, + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "stc", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "xvdc", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + }, + } + + expected := blockEntries.BlockDevices[3] + res, err := blockEntries.FindDevice("/dev/sdc") + require.Nil(t, err) + require.Equal(t, expected, res) + }) + + t.Run("perfect match and trailing letter matches", func(t *testing.T) { + blockEntries := BlockDevices{ + BlockDevices: []BlockDevice{ + {Name: "sda", Children: []BlockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}}, + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "sta", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "xvda", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + }, + } + + expected := blockEntries.BlockDevices[0] + res, err := blockEntries.FindDevice("/dev/sda") + require.Nil(t, err) + require.Equal(t, expected, res) + }) + + t.Run("perfect match and trailing letter matches (scrambled)", func(t *testing.T) { + blockEntries := BlockDevices{ + BlockDevices: []BlockDevice{ + {Name: "xvda", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "sta", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "sda", Children: []BlockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}}, + }, + } + + expected := blockEntries.BlockDevices[3] + res, err := blockEntries.FindDevice("/dev/sda") + require.Nil(t, err) + require.Equal(t, expected, res) + }) } func TestGetMountablePartition(t *testing.T) { @@ -411,3 +524,13 @@ func TestAttachedBlockEntryFedora(t *testing.T) { require.Equal(t, "xfs", info.FsType) require.True(t, strings.Contains(info.Name, "xvdh4")) } + +func TestLongestMatchingSuffix(t *testing.T) { + requested := "abcde" + entries := []string{"a", "e", "de"} + + for i, entry := range entries { + r := LongestMatchingSuffix(requested, entry) + require.Equal(t, i, r) + } +}