Skip to content

Commit

Permalink
✨ find device by trailing letters (#4732)
Browse files Browse the repository at this point in the history
* naive

* "optimised"

* comments

* one more test

* lint: returned values placeholder

* feat: move lms to a private func and add test

* rm benchmark

* feat: naming and doc

* debug log

* feat: update devcontainers go version

* feat: BlockDevices symlink aliases

* fix: parse stringer size

* cleanup debug log

* optimize: remove unnecessary sort

* remove lmsCache

* Update providers/os/connection/snapshot/blockdevices_test.go

Co-authored-by: Preslav Gerchev <[email protected]>

* feat: remove loop tag

* test: alias matching

* debug log

* simplify search (start with 0)

* de-nit: loop definition

* cleanup

---------

Co-authored-by: Preslav Gerchev <[email protected]>
  • Loading branch information
slntopp and preslavgerchev authored Oct 24, 2024
1 parent 6175900 commit 1fb2db8
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 24 deletions.
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
150 changes: 130 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,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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
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)
}
}

0 comments on commit 1fb2db8

Please sign in to comment.