-
Notifications
You must be signed in to change notification settings - Fork 124
Implement Runtime NVMe Instance Storage Discovery Using AWS EBS Symlinks #396
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
base: main
Are you sure you want to change the base?
Changes from 8 commits
d5a06d6
3d6949d
dcd857a
bb98869
826c3ed
08b053d
a797780
9491b7e
834ec2a
3c4db0c
c43c6a1
3b63c11
f63ffb3
32f65f1
27f51d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| package devicepathresolver | ||
|
|
||
| import ( | ||
| bosherr "github.com/cloudfoundry/bosh-utils/errors" | ||
| boshlog "github.com/cloudfoundry/bosh-utils/logger" | ||
| boshsys "github.com/cloudfoundry/bosh-utils/system" | ||
|
|
||
| boshudev "github.com/cloudfoundry/bosh-agent/v2/platform/udevdevice" | ||
| ) | ||
|
|
||
| const ( | ||
| // NVMeDevicePattern is a glob pattern matching NVMe namespace devices. | ||
| NVMeDevicePattern = "/dev/nvme*n1" | ||
|
|
||
| // NVMeDevicePathPrefix is the common prefix for NVMe device paths. | ||
| // Used to detect if a device path is an NVMe device. | ||
| NVMeDevicePathPrefix = "/dev/nvme" | ||
| ) | ||
|
|
||
| type SymlinkDeviceResolver struct { | ||
| fs boshsys.FileSystem | ||
| udev boshudev.UdevDevice | ||
| logger boshlog.Logger | ||
| logTag string | ||
| } | ||
|
|
||
| // NewSymlinkDeviceResolver creates a new symlink device resolver. | ||
| func NewSymlinkDeviceResolver( | ||
| fs boshsys.FileSystem, | ||
| udev boshudev.UdevDevice, | ||
| logger boshlog.Logger, | ||
| ) *SymlinkDeviceResolver { | ||
| return &SymlinkDeviceResolver{ | ||
| fs: fs, | ||
| udev: udev, | ||
| logger: logger, | ||
| logTag: "SymlinkDeviceResolver", | ||
| } | ||
| } | ||
|
|
||
| // ResolveSymlinksToDevices resolves all symlinks matching the given pattern | ||
| // and returns a map of resolved device paths -> symlink paths. | ||
| // | ||
| // udevadm trigger and settle are called before globbing to avoid a race condition: | ||
| // NVMe block devices (/dev/nvme*) appear synchronously at boot, but the | ||
| // /dev/disk/by-id/ symlinks are created asynchronously by udev. Without waiting, | ||
| // globbing may return no symlinks, causing all NVMe devices to be misidentified | ||
| // as instance storage (instead of EBS/managed volumes). | ||
| func (r *SymlinkDeviceResolver) ResolveSymlinksToDevices(symlinkPattern string) (map[string]string, error) { | ||
| if err := r.udev.Trigger(); err != nil { | ||
| return nil, bosherr.WrapError(err, "Running udevadm trigger") | ||
| } | ||
| if err := r.udev.Settle(); err != nil { | ||
| return nil, bosherr.WrapError(err, "Running udevadm settle") | ||
| } | ||
|
|
||
| symlinks, err := r.fs.Glob(symlinkPattern) | ||
| if err != nil { | ||
| return nil, bosherr.WrapErrorf(err, "Globbing symlinks with pattern '%s'", symlinkPattern) | ||
| } | ||
|
|
||
| result := make(map[string]string) | ||
| for _, symlink := range symlinks { | ||
| absPath, err := r.fs.ReadAndFollowLink(symlink) | ||
| if err != nil { | ||
| return nil, bosherr.WrapErrorf(err, "Resolving managed volume symlink '%s'", symlink) | ||
| } | ||
|
|
||
| r.logger.Debug(r.logTag, "Resolved symlink: %s -> %s", symlink, absPath) | ||
| result[absPath] = symlink | ||
| } | ||
|
|
||
| return result, nil | ||
| } | ||
|
|
||
| // GetDevicesByPattern returns all devices matching the given pattern. | ||
| func (r *SymlinkDeviceResolver) GetDevicesByPattern(devicePattern string) ([]string, error) { | ||
| devices, err := r.fs.Glob(devicePattern) | ||
| if err != nil { | ||
| return nil, bosherr.WrapErrorf(err, "Globbing devices with pattern '%s'", devicePattern) | ||
| } | ||
|
|
||
| r.logger.Debug(r.logTag, "Found devices matching '%s': %v", devicePattern, devices) | ||
| return devices, nil | ||
| } | ||
|
|
||
| // FilterDevices returns devices that are NOT in the exclusion map. | ||
| // This is used to filter out IaaS-managed volumes (EBS, Azure Managed Disks, etc.) | ||
| // from the list of all NVMe devices, leaving only instance/ephemeral storage. | ||
| func (r *SymlinkDeviceResolver) FilterDevices(allDevices []string, excludeDevices map[string]string) []string { | ||
| var filtered []string | ||
| for _, device := range allDevices { | ||
| if _, excluded := excludeDevices[device]; !excluded { | ||
| filtered = append(filtered, device) | ||
| r.logger.Debug(r.logTag, "Including device: %s", device) | ||
| } else { | ||
| r.logger.Debug(r.logTag, "Excluding device: %s (symlink: %s)", device, excludeDevices[device]) | ||
| } | ||
| } | ||
| return filtered | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| package devicepathresolver_test | ||
|
|
||
| import ( | ||
| "errors" | ||
| "os" | ||
| "runtime" | ||
|
|
||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
|
|
||
| boshlog "github.com/cloudfoundry/bosh-utils/logger" | ||
| fakesys "github.com/cloudfoundry/bosh-utils/system/fakes" | ||
|
|
||
| . "github.com/cloudfoundry/bosh-agent/v2/infrastructure/devicepathresolver" | ||
| fakeudev "github.com/cloudfoundry/bosh-agent/v2/platform/udevdevice/fakes" | ||
| ) | ||
|
|
||
| var _ = Describe("SymlinkDeviceResolver", func() { | ||
| var ( | ||
| fs *fakesys.FakeFileSystem | ||
| udev *fakeudev.FakeUdevDevice | ||
| logger boshlog.Logger | ||
| resolver *SymlinkDeviceResolver | ||
| ) | ||
|
|
||
| BeforeEach(func() { | ||
| if runtime.GOOS == "windows" { | ||
| Skip("Not applicable on Windows") | ||
| } | ||
|
|
||
| fs = fakesys.NewFakeFileSystem() | ||
| udev = fakeudev.NewFakeUdevDevice() | ||
| logger = boshlog.NewLogger(boshlog.LevelNone) | ||
| resolver = NewSymlinkDeviceResolver(fs, udev, logger) | ||
| }) | ||
|
|
||
| Describe("ResolveSymlinksToDevices", func() { | ||
| It("returns empty map when no symlinks match the pattern", func() { | ||
| fs.SetGlob("/dev/disk/by-id/nvme-*", []string{}) | ||
|
|
||
| result, err := resolver.ResolveSymlinksToDevices("/dev/disk/by-id/nvme-*") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result).To(BeEmpty()) | ||
| }) | ||
|
|
||
| It("resolves symlinks to their target device paths", func() { | ||
| err := fs.MkdirAll("/dev/disk/by-id", os.FileMode(0750)) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| // Create target device files | ||
| err = fs.WriteFileString("/dev/nvme1n1", "") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| err = fs.WriteFileString("/dev/nvme2n1", "") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| fs.SetGlob("/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*", []string{ | ||
| "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol123", | ||
| "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol456", | ||
| }) | ||
| err = fs.Symlink("/dev/nvme1n1", "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol123") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| err = fs.Symlink("/dev/nvme2n1", "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol456") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| result, err := resolver.ResolveSymlinksToDevices("/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result).To(HaveLen(2)) | ||
| Expect(result["/dev/nvme1n1"]).To(Equal("/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol123")) | ||
| Expect(result["/dev/nvme2n1"]).To(Equal("/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol456")) | ||
| }) | ||
|
|
||
| It("returns error when a managed volume symlink cannot be resolved", func() { | ||
| err := fs.MkdirAll("/dev/disk/by-id", os.FileMode(0750)) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| // Create target device file for valid symlink | ||
| err = fs.WriteFileString("/dev/nvme1n1", "") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| fs.SetGlob("/dev/disk/by-id/nvme-*", []string{ | ||
| "/dev/disk/by-id/nvme-valid", | ||
| "/dev/disk/by-id/nvme-invalid", | ||
| }) | ||
| err = fs.Symlink("/dev/nvme1n1", "/dev/disk/by-id/nvme-valid") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| // nvme-invalid has no symlink target | ||
|
|
||
| _, err = resolver.ResolveSymlinksToDevices("/dev/disk/by-id/nvme-*") | ||
|
Check failure on line 88 in infrastructure/devicepathresolver/symlink_device_resolver_test.go
|
||
| Expect(err).To(HaveOccurred()) | ||
| Expect(err.Error()).To(ContainSubstring("nvme-invalid")) | ||
| }) | ||
|
|
||
| It("returns error when glob fails", func() { | ||
| fs.GlobErr = errors.New("glob error") | ||
|
|
||
| _, err := resolver.ResolveSymlinksToDevices("/dev/disk/by-id/nvme-*") | ||
| Expect(err).To(HaveOccurred()) | ||
| Expect(err.Error()).To(ContainSubstring("glob error")) | ||
| }) | ||
| }) | ||
|
|
||
| Describe("GetDevicesByPattern", func() { | ||
| It("returns devices matching the pattern", func() { | ||
| fs.SetGlob("/dev/nvme*n1", []string{"/dev/nvme0n1", "/dev/nvme1n1", "/dev/nvme2n1"}) | ||
|
|
||
| devices, err := resolver.GetDevicesByPattern("/dev/nvme*n1") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(devices).To(ConsistOf("/dev/nvme0n1", "/dev/nvme1n1", "/dev/nvme2n1")) | ||
| }) | ||
|
|
||
| It("returns empty slice when no devices match", func() { | ||
| fs.SetGlob("/dev/nvme*n1", []string{}) | ||
|
|
||
| devices, err := resolver.GetDevicesByPattern("/dev/nvme*n1") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(devices).To(BeEmpty()) | ||
| }) | ||
|
|
||
| It("returns error when glob fails", func() { | ||
| fs.GlobErr = errors.New("glob error") | ||
|
|
||
| _, err := resolver.GetDevicesByPattern("/dev/nvme*n1") | ||
| Expect(err).To(HaveOccurred()) | ||
| }) | ||
| }) | ||
|
|
||
| Describe("FilterDevices", func() { | ||
| It("returns devices not in the exclusion map", func() { | ||
| allDevices := []string{"/dev/nvme0n1", "/dev/nvme1n1", "/dev/nvme2n1", "/dev/nvme3n1"} | ||
| excludeDevices := map[string]string{ | ||
| "/dev/nvme1n1": "/dev/disk/by-id/ebs-vol1", | ||
| "/dev/nvme2n1": "/dev/disk/by-id/ebs-vol2", | ||
| } | ||
|
|
||
| filtered := resolver.FilterDevices(allDevices, excludeDevices) | ||
| Expect(filtered).To(ConsistOf("/dev/nvme0n1", "/dev/nvme3n1")) | ||
| }) | ||
|
|
||
| It("returns all devices when exclusion map is empty", func() { | ||
| allDevices := []string{"/dev/nvme0n1", "/dev/nvme1n1"} | ||
| excludeDevices := map[string]string{} | ||
|
|
||
| filtered := resolver.FilterDevices(allDevices, excludeDevices) | ||
| Expect(filtered).To(ConsistOf("/dev/nvme0n1", "/dev/nvme1n1")) | ||
| }) | ||
|
|
||
| It("returns empty slice when all devices are excluded", func() { | ||
| allDevices := []string{"/dev/nvme0n1"} | ||
| excludeDevices := map[string]string{ | ||
| "/dev/nvme0n1": "/dev/disk/by-id/ebs-vol1", | ||
| } | ||
|
|
||
| filtered := resolver.FilterDevices(allDevices, excludeDevices) | ||
| Expect(filtered).To(BeEmpty()) | ||
| }) | ||
| }) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.