Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ find device by trailing letters #4732

Merged
merged 22 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
151 changes: 131 additions & 20 deletions providers/os/connection/snapshot/blockdevices.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (
"errors"
"fmt"
"io"
"math"
"os"
"sort"
"strconv"
"strings"

"github.com/rs/zerolog/log"
Expand All @@ -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 {
Expand Down Expand Up @@ -53,9 +76,52 @@ 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
}

log.Debug().Str("name", entry.Name()).Msg("found symlink")
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 {
Expand Down Expand Up @@ -106,7 +172,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)
Expand All @@ -125,29 +191,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) {
slntopp marked this conversation as resolved.
Show resolved Hide resolved
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 {
slntopp marked this conversation as resolved.
Show resolved Hide resolved
Device BlockDevice
Lms int
}{
Device: BlockDevice{},
Lms: 0,
}

for i := 0; i < len(devices); i++ {
slntopp marked this conversation as resolved.
Show resolved Hide resolved
log.Debug().
Str("name", devices[i].Name).
Strs("aliases", devices[i].Aliases).
Msg("checking device")
if devices[i].Name == requestedName {
return blockEntries.BlockDevices[i], nil
}

lms := LongestMatchingSuffix(requested, devices[i].Name)
for _, alias := range devices[i].Aliases {
aliasLms := LongestMatchingSuffix(requested, alias)
if aliasLms > lms {
lms = aliasLms
lmsCache[devices[i].Name] = aliasLms
}
}
log.Debug().Str("name", d.Name).Msg("found matching device")
return d, nil

if lms > bestMatch.Lms {
bestMatch.Device = devices[i]
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
Expand All @@ -169,7 +280,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
Expand Down
127 changes: 125 additions & 2 deletions providers/os/connection/snapshot/blockdevices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}
Loading