Skip to content

Commit c4eb26c

Browse files
authored
Only initiate block device rescans if every path is available during iSCSI volume expansion
This commit changes CSI NodeExpandVolume such that block device rescans are initiated if and only if all block device paths are available.
1 parent 751dabf commit c4eb26c

File tree

3 files changed

+178
-135
lines changed

3 files changed

+178
-135
lines changed

utils/devices/devices_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 NetApp, Inc. All Rights Reserved.
1+
// Copyright 2025 NetApp, Inc. All Rights Reserved.
22

33
// NOTE: This file should only contain functions for handling devices for linux flavor
44

utils/iscsi/iscsi.go

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,49 @@ func (client *Client) AddSession(
594594
}
595595
}
596596

597+
// filterDevicesBySize builds a map of disk devices to their size, filtered by a minimum size requirement.
598+
// If any errors occur when checking the size of a device, it captures the error and moves onto the next device.
599+
func (client *Client) filterDevicesBySize(
600+
ctx context.Context, deviceInfo *models.ScsiDeviceInfo, minSize int64,
601+
) (map[string]int64, error) {
602+
var errs error
603+
deviceSizeMap := make(map[string]int64, 0)
604+
for _, diskDevice := range deviceInfo.Devices {
605+
size, err := client.devices.GetDiskSize(ctx, devices.DevPrefix+diskDevice)
606+
if err != nil {
607+
errs = errors.Join(errs, err)
608+
// Only consider devices whose size can be gathered.
609+
continue
610+
}
611+
612+
if size < minSize {
613+
// Only consider devices that are undersized.
614+
deviceSizeMap[diskDevice] = size
615+
}
616+
}
617+
618+
if errs != nil {
619+
return nil, errs
620+
}
621+
return deviceSizeMap, nil
622+
}
623+
624+
// rescanDevices accepts a map of disk devices to sizes and initiates a rescan for each device.
625+
// If any rescan fails it captures the error and moves onto the next rescanning the next device.
626+
func (client *Client) rescanDevices(ctx context.Context, deviceSizeMap map[string]int64) error {
627+
var errs error
628+
for diskDevice := range deviceSizeMap {
629+
if err := client.rescanDisk(ctx, diskDevice); err != nil {
630+
errs = errors.Join(errs, fmt.Errorf("failed to rescan disk %s: %s", diskDevice, err))
631+
}
632+
}
633+
634+
if errs != nil {
635+
return errs
636+
}
637+
return nil
638+
}
639+
597640
func (client *Client) RescanDevices(ctx context.Context, targetIQN string, lunID int32, minSize int64) error {
598641
GenerateRequestContextForLayer(ctx, LogLayerUtils)
599642

@@ -610,36 +653,39 @@ func (client *Client) RescanDevices(ctx context.Context, targetIQN string, lunID
610653
return fmt.Errorf("error getting iSCSI device information: %s", err)
611654
}
612655

613-
allLargeEnough := true
614-
for _, diskDevice := range deviceInfo.Devices {
615-
size, err := client.devices.GetDiskSize(ctx, devices.DevPrefix+diskDevice)
616-
if err != nil {
617-
return err
656+
// Get all disk devices that require a rescan.
657+
devicesBySize, err := client.filterDevicesBySize(ctx, deviceInfo, minSize)
658+
if err != nil {
659+
Logc(ctx).WithError(err).Error("Failed to read disk size for devices.")
660+
return err
661+
}
662+
663+
if len(devicesBySize) != 0 {
664+
fields = LogFields{
665+
"lunID": lunID,
666+
"devices": devicesBySize,
667+
"minSize": minSize,
618668
}
619-
if size < minSize {
620-
allLargeEnough = false
621-
} else {
622-
continue
669+
670+
Logc(ctx).WithFields(fields).Debug("Found devices that require a rescan.")
671+
if err := client.rescanDevices(ctx, devicesBySize); err != nil {
672+
Logc(ctx).WithError(err).Error("Failed to initiate rescanning for devices.")
673+
return err
623674
}
624675

625-
err = client.rescanDisk(ctx, diskDevice)
676+
// Sleep for a second to give the SCSI subsystem time to rescan the devices.
677+
time.Sleep(time.Second)
678+
679+
// Reread the devices to check if any are undersized.
680+
devicesBySize, err = client.filterDevicesBySize(ctx, deviceInfo, minSize)
626681
if err != nil {
627-
Logc(ctx).WithField("diskDevice", diskDevice).Error("Failed to rescan disk.")
628-
return fmt.Errorf("failed to rescan disk %s: %s", diskDevice, err)
682+
Logc(ctx).WithError(err).Error("Failed to read disk size for devices after rescan.")
683+
return err
629684
}
630-
}
631685

632-
if !allLargeEnough {
633-
time.Sleep(time.Second)
634-
for _, diskDevice := range deviceInfo.Devices {
635-
size, err := client.devices.GetDiskSize(ctx, devices.DevPrefix+diskDevice)
636-
if err != nil {
637-
return err
638-
}
639-
if size < minSize {
640-
Logc(ctx).Error("Disk size not large enough after resize.")
641-
return fmt.Errorf("disk size not large enough after resize: %d, %d", size, minSize)
642-
}
686+
if len(devicesBySize) != 0 {
687+
Logc(ctx).WithFields(fields).Error("Some devices are still undersized after rescan.")
688+
return fmt.Errorf("devices are still undersized after rescan")
643689
}
644690
}
645691

@@ -653,15 +699,16 @@ func (client *Client) RescanDevices(ctx context.Context, targetIQN string, lunID
653699
fields = LogFields{"size": size, "minSize": minSize}
654700
if size < minSize {
655701
Logc(ctx).WithFields(fields).Debug("Reloading the multipath device.")
656-
err := client.reloadMultipathDevice(ctx, multipathDevice)
657-
if err != nil {
702+
if err := client.reloadMultipathDevice(ctx, multipathDevice); err != nil {
658703
return err
659704
}
660705
time.Sleep(time.Second)
661-
size, err = client.devices.GetDiskSize(ctx, devices.DevPrefix+multipathDevice)
706+
707+
size, err := client.devices.GetDiskSize(ctx, devices.DevPrefix+multipathDevice)
662708
if err != nil {
663709
return err
664710
}
711+
665712
if size < minSize {
666713
Logc(ctx).Error("Multipath device not large enough after resize.")
667714
return fmt.Errorf("multipath device not large enough after resize: %d < %d", size, minSize)

utils/iscsi/iscsi_test.go

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,6 +2283,95 @@ func TestClient_AddSession(t *testing.T) {
22832283
}
22842284
}
22852285

2286+
func TestClient_filterDevicesBySize(t *testing.T) {
2287+
mockCtrl := gomock.NewController(t)
2288+
mockDevices := mock_devices.NewMockDevices(mockCtrl)
2289+
2290+
client := NewDetailed(
2291+
"",
2292+
nil,
2293+
nil,
2294+
nil,
2295+
mockDevices,
2296+
nil,
2297+
nil,
2298+
nil,
2299+
afero.Afero{},
2300+
nil,
2301+
)
2302+
2303+
ctx := context.TODO()
2304+
deviceInfo := &models.ScsiDeviceInfo{
2305+
Devices: []string{"sda", "sdb"},
2306+
}
2307+
minSize := int64(10)
2308+
2309+
// Negative case.
2310+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sda").Return(int64(1), nil)
2311+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sdb").Return(int64(0), errors.New("failed to open disk"))
2312+
deviceSizeMap, err := client.filterDevicesBySize(ctx, deviceInfo, minSize)
2313+
assert.Error(t, err)
2314+
assert.Nil(t, deviceSizeMap)
2315+
assert.NotEqual(t, len(deviceInfo.Devices), len(deviceSizeMap))
2316+
2317+
// Positive case #1: Only one device needs a resize.
2318+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sda").Return(minSize, nil)
2319+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sdb").Return(int64(1), nil)
2320+
deviceSizeMap, err = client.filterDevicesBySize(ctx, deviceInfo, minSize)
2321+
assert.NoError(t, err)
2322+
assert.NotNil(t, deviceSizeMap)
2323+
assert.NotEqual(t, len(deviceInfo.Devices), len(deviceSizeMap))
2324+
2325+
// Positive case #2: All devices need to resize.
2326+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sda").Return(int64(1), nil)
2327+
mockDevices.EXPECT().GetDiskSize(ctx, "/dev/sdb").Return(int64(1), nil)
2328+
deviceSizeMap, err = client.filterDevicesBySize(ctx, deviceInfo, minSize)
2329+
assert.NoError(t, err)
2330+
assert.NotNil(t, deviceSizeMap)
2331+
assert.Equal(t, len(deviceInfo.Devices), len(deviceSizeMap))
2332+
}
2333+
2334+
func TestClient_rescanDevices(t *testing.T) {
2335+
mockCtrl := gomock.NewController(t)
2336+
mockDevices := mock_devices.NewMockDevices(mockCtrl)
2337+
2338+
fs := afero.NewMemMapFs()
2339+
_, err := fs.Create("/sys/block/sda/device/rescan")
2340+
assert.NoError(t, err)
2341+
2342+
client := NewDetailed(
2343+
"",
2344+
nil,
2345+
nil,
2346+
nil,
2347+
mockDevices,
2348+
nil,
2349+
nil,
2350+
nil,
2351+
afero.Afero{Fs: fs},
2352+
nil,
2353+
)
2354+
2355+
ctx := context.TODO()
2356+
deviceSizeMap := map[string]int64{
2357+
"sda": 1,
2358+
"sdb": 1,
2359+
}
2360+
2361+
// Should fail because a device path does not exist.
2362+
mockDevices.EXPECT().ListAllDevices(ctx).AnyTimes()
2363+
err = client.rescanDevices(ctx, deviceSizeMap)
2364+
assert.Error(t, err)
2365+
2366+
// Add the missing device path.
2367+
_, err = fs.Create("/sys/block/sdb/device/rescan")
2368+
assert.NoError(t, err)
2369+
2370+
// Should succeed now that the device path exists.
2371+
err = client.rescanDevices(ctx, deviceSizeMap)
2372+
assert.NoError(t, err)
2373+
}
2374+
22862375
func TestClient_RescanDevices(t *testing.T) {
22872376
type parameters struct {
22882377
targetIQN string
@@ -2447,8 +2536,12 @@ func TestClient_RescanDevices(t *testing.T) {
24472536
getDeviceClient: func(controller *gomock.Controller) devices.Devices {
24482537
mockDevices := mock_devices.NewMockDevices(controller)
24492538
mockDevices.EXPECT().FindMultipathDeviceForDevice(context.TODO(), "sda").Return("dm-0").Times(1)
2539+
2540+
// This will be called twice because we read from each disk twice during an expansion.
2541+
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil).Times(1)
2542+
mockDevices.EXPECT().ListAllDevices(context.TODO()).Times(2)
24502543
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil).Times(1)
2451-
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), nil).Times(1)
2544+
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), nil)
24522545
return mockDevices
24532546
},
24542547
getCommandClient: func(controller *gomock.Controller) tridentexec.Command {
@@ -2684,6 +2777,7 @@ func TestClient_RescanDevices(t *testing.T) {
26842777
assertError: assert.NoError,
26852778
},
26862779
"happy path": {
2780+
minSize: 10,
26872781
targetIQN: targetIQN,
26882782
getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils {
26892783
mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller)
@@ -2696,8 +2790,12 @@ func TestClient_RescanDevices(t *testing.T) {
26962790
getDeviceClient: func(controller *gomock.Controller) devices.Devices {
26972791
mockDevices := mock_devices.NewMockDevices(controller)
26982792
mockDevices.EXPECT().FindMultipathDeviceForDevice(context.TODO(), "sda").Return("dm-0").Times(1)
2699-
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil)
2700-
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil)
2793+
2794+
// This will be called twice because we read from each disk twice during an expansion.
2795+
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil).Times(1)
2796+
mockDevices.EXPECT().ListAllDevices(context.TODO()).Times(2)
2797+
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/sda").Return(int64(10), nil).Times(1)
2798+
mockDevices.EXPECT().GetDiskSize(context.TODO(), "/dev/dm-0").Return(int64(10), nil)
27012799
return mockDevices
27022800
},
27032801
getCommandClient: func(controller *gomock.Controller) tridentexec.Command {
@@ -2706,6 +2804,8 @@ func TestClient_RescanDevices(t *testing.T) {
27062804
},
27072805
getFileSystemUtils: func() afero.Fs {
27082806
fs := afero.NewMemMapFs()
2807+
_, err := fs.Create("/sys/block/sda/device/rescan")
2808+
assert.NoError(t, err)
27092809
return fs
27102810
},
27112811
assertError: assert.NoError,
@@ -2728,110 +2828,6 @@ func TestClient_RescanDevices(t *testing.T) {
27282828
}
27292829
}
27302830

2731-
func TestClient_rescanDisk(t *testing.T) {
2732-
type parameters struct {
2733-
getFileSystemUtils func() afero.Fs
2734-
assertError assert.ErrorAssertionFunc
2735-
getDevices func(controller *gomock.Controller) devices.Devices
2736-
}
2737-
const deviceName = "sda"
2738-
tests := map[string]parameters{
2739-
"happy path": {
2740-
getFileSystemUtils: func() afero.Fs {
2741-
fs := afero.NewMemMapFs()
2742-
_, err := fs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName))
2743-
assert.NoError(t, err)
2744-
return fs
2745-
},
2746-
getDevices: func(controller *gomock.Controller) devices.Devices {
2747-
mockDevices := mock_devices.NewMockDevices(controller)
2748-
mockDevices.EXPECT().ListAllDevices(context.TODO()).Times(2)
2749-
return mockDevices
2750-
},
2751-
assertError: assert.NoError,
2752-
},
2753-
"error opening rescan file": {
2754-
getFileSystemUtils: func() afero.Fs {
2755-
fs := afero.NewMemMapFs()
2756-
return fs
2757-
},
2758-
getDevices: func(controller *gomock.Controller) devices.Devices {
2759-
mockDevices := mock_devices.NewMockDevices(controller)
2760-
mockDevices.EXPECT().ListAllDevices(context.TODO())
2761-
return mockDevices
2762-
},
2763-
assertError: assert.Error,
2764-
},
2765-
"error writing to file": {
2766-
getFileSystemUtils: func() afero.Fs {
2767-
f := &aferoFileWrapper{
2768-
WriteStringError: errors.New("some error"),
2769-
File: mem.NewFileHandle(&mem.FileData{}),
2770-
}
2771-
2772-
memMapFs := afero.NewMemMapFs()
2773-
_, err := memMapFs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName))
2774-
assert.NoError(t, err)
2775-
2776-
fs := &aferoWrapper{
2777-
openFileResponse: f,
2778-
openResponse: f,
2779-
Fs: memMapFs,
2780-
}
2781-
2782-
return fs
2783-
},
2784-
getDevices: func(controller *gomock.Controller) devices.Devices {
2785-
mockDevices := mock_devices.NewMockDevices(controller)
2786-
mockDevices.EXPECT().ListAllDevices(context.TODO())
2787-
return mockDevices
2788-
},
2789-
assertError: assert.Error,
2790-
},
2791-
"failed writing to file": {
2792-
getFileSystemUtils: func() afero.Fs {
2793-
f := &aferoFileWrapper{
2794-
WriteStringCount: 0,
2795-
File: mem.NewFileHandle(&mem.FileData{}),
2796-
}
2797-
2798-
memMapFs := afero.NewMemMapFs()
2799-
_, err := memMapFs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName))
2800-
assert.NoError(t, err)
2801-
2802-
fs := &aferoWrapper{
2803-
openFileResponse: f,
2804-
openResponse: f,
2805-
Fs: memMapFs,
2806-
}
2807-
2808-
return fs
2809-
},
2810-
getDevices: func(controller *gomock.Controller) devices.Devices {
2811-
mockDevices := mock_devices.NewMockDevices(controller)
2812-
mockDevices.EXPECT().ListAllDevices(context.TODO())
2813-
return mockDevices
2814-
},
2815-
assertError: assert.Error,
2816-
},
2817-
}
2818-
2819-
for name, params := range tests {
2820-
t.Run(name, func(t *testing.T) {
2821-
var deviceClient devices.Devices
2822-
if params.getDevices != nil {
2823-
deviceClient = params.getDevices(gomock.NewController(t))
2824-
}
2825-
client := NewDetailed("", nil, nil, nil, deviceClient, nil, nil, nil,
2826-
afero.Afero{Fs: params.getFileSystemUtils()}, nil)
2827-
err := client.rescanDisk(context.TODO(), deviceName)
2828-
if params.assertError != nil {
2829-
params.assertError(t, err)
2830-
}
2831-
})
2832-
}
2833-
}
2834-
28352831
func TestClient_reloadMultipathDevice(t *testing.T) {
28362832
type parameters struct {
28372833
multipathDeviceName string

0 commit comments

Comments
 (0)