diff --git a/frontend/csi/utils_test.go b/frontend/csi/utils_test.go index bb7d5837b..59aa26a65 100644 --- a/frontend/csi/utils_test.go +++ b/frontend/csi/utils_test.go @@ -12,8 +12,8 @@ import ( "github.com/netapp/trident/config" mockControllerAPI "github.com/netapp/trident/mocks/mock_frontend/mock_csi/mock_controller_api" + "github.com/netapp/trident/mocks/mock_utils/mock_iscsi" "github.com/netapp/trident/mocks/mock_utils/mock_models/mock_luks" - mock_iscsi "github.com/netapp/trident/mocks/mock_utils/mock_reconcile_utils" "github.com/netapp/trident/utils" "github.com/netapp/trident/utils/errors" "github.com/netapp/trident/utils/models" diff --git a/frontend/csi/volume_publish_manager_test.go b/frontend/csi/volume_publish_manager_test.go index 597e7042b..a93fac277 100644 --- a/frontend/csi/volume_publish_manager_test.go +++ b/frontend/csi/volume_publish_manager_test.go @@ -15,8 +15,8 @@ import ( "go.uber.org/mock/gomock" "github.com/netapp/trident/config" + "github.com/netapp/trident/mocks/mock_utils/mock_iscsi" "github.com/netapp/trident/mocks/mock_utils/mock_models" - mock_iscsi "github.com/netapp/trident/mocks/mock_utils/mock_reconcile_utils" "github.com/netapp/trident/utils" "github.com/netapp/trident/utils/errors" "github.com/netapp/trident/utils/models" diff --git a/mocks/mock_utils/mock_iscsi/mock_iscsi_devices_client.go b/mocks/mock_utils/mock_iscsi/mock_iscsi_devices_client.go new file mode 100644 index 000000000..fc7017710 --- /dev/null +++ b/mocks/mock_utils/mock_iscsi/mock_iscsi_devices_client.go @@ -0,0 +1,144 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/utils/iscsi (interfaces: Devices) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_devices_client.go github.com/netapp/trident/utils/iscsi Devices +// + +// Package mock_iscsi is a generated GoMock package. +package mock_iscsi + +import ( + context "context" + reflect "reflect" + + models "github.com/netapp/trident/utils/models" + gomock "go.uber.org/mock/gomock" +) + +// MockDevices is a mock of Devices interface. +type MockDevices struct { + ctrl *gomock.Controller + recorder *MockDevicesMockRecorder +} + +// MockDevicesMockRecorder is the mock recorder for MockDevices. +type MockDevicesMockRecorder struct { + mock *MockDevices +} + +// NewMockDevices creates a new mock instance. +func NewMockDevices(ctrl *gomock.Controller) *MockDevices { + mock := &MockDevices{ctrl: ctrl} + mock.recorder = &MockDevicesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDevices) EXPECT() *MockDevicesMockRecorder { + return m.recorder +} + +// EnsureDeviceReadable mocks base method. +func (m *MockDevices) EnsureDeviceReadable(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureDeviceReadable", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureDeviceReadable indicates an expected call of EnsureDeviceReadable. +func (mr *MockDevicesMockRecorder) EnsureDeviceReadable(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureDeviceReadable", reflect.TypeOf((*MockDevices)(nil).EnsureDeviceReadable), arg0, arg1) +} + +// EnsureLUKSDeviceMappedOnHost mocks base method. +func (m *MockDevices) EnsureLUKSDeviceMappedOnHost(arg0 context.Context, arg1 models.LUKSDeviceInterface, arg2 string, arg3 map[string]string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureLUKSDeviceMappedOnHost", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureLUKSDeviceMappedOnHost indicates an expected call of EnsureLUKSDeviceMappedOnHost. +func (mr *MockDevicesMockRecorder) EnsureLUKSDeviceMappedOnHost(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureLUKSDeviceMappedOnHost", reflect.TypeOf((*MockDevices)(nil).EnsureLUKSDeviceMappedOnHost), arg0, arg1, arg2, arg3) +} + +// GetDeviceFSType mocks base method. +func (m *MockDevices) GetDeviceFSType(arg0 context.Context, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceFSType", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeviceFSType indicates an expected call of GetDeviceFSType. +func (mr *MockDevicesMockRecorder) GetDeviceFSType(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceFSType", reflect.TypeOf((*MockDevices)(nil).GetDeviceFSType), arg0, arg1) +} + +// GetISCSIDiskSize mocks base method. +func (m *MockDevices) GetISCSIDiskSize(arg0 context.Context, arg1 string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetISCSIDiskSize", arg0, arg1) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetISCSIDiskSize indicates an expected call of GetISCSIDiskSize. +func (mr *MockDevicesMockRecorder) GetISCSIDiskSize(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetISCSIDiskSize", reflect.TypeOf((*MockDevices)(nil).GetISCSIDiskSize), arg0, arg1) +} + +// IsDeviceUnformatted mocks base method. +func (m *MockDevices) IsDeviceUnformatted(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDeviceUnformatted", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDeviceUnformatted indicates an expected call of IsDeviceUnformatted. +func (mr *MockDevicesMockRecorder) IsDeviceUnformatted(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDeviceUnformatted", reflect.TypeOf((*MockDevices)(nil).IsDeviceUnformatted), arg0, arg1) +} + +// NewLUKSDevice mocks base method. +func (m *MockDevices) NewLUKSDevice(arg0, arg1 string) (models.LUKSDeviceInterface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewLUKSDevice", arg0, arg1) + ret0, _ := ret[0].(models.LUKSDeviceInterface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewLUKSDevice indicates an expected call of NewLUKSDevice. +func (mr *MockDevicesMockRecorder) NewLUKSDevice(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewLUKSDevice", reflect.TypeOf((*MockDevices)(nil).NewLUKSDevice), arg0, arg1) +} + +// WaitForDevice mocks base method. +func (m *MockDevices) WaitForDevice(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitForDevice", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitForDevice indicates an expected call of WaitForDevice. +func (mr *MockDevicesMockRecorder) WaitForDevice(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDevice", reflect.TypeOf((*MockDevices)(nil).WaitForDevice), arg0, arg1) +} diff --git a/mocks/mock_utils/mock_iscsi/mock_iscsi_filesystem_client.go b/mocks/mock_utils/mock_iscsi/mock_iscsi_filesystem_client.go new file mode 100644 index 000000000..f0bc3d590 --- /dev/null +++ b/mocks/mock_utils/mock_iscsi/mock_iscsi_filesystem_client.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/utils/iscsi (interfaces: FileSystem) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_filesystem_client.go github.com/netapp/trident/utils/iscsi FileSystem +// + +// Package mock_iscsi is a generated GoMock package. +package mock_iscsi + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockFileSystem is a mock of FileSystem interface. +type MockFileSystem struct { + ctrl *gomock.Controller + recorder *MockFileSystemMockRecorder +} + +// MockFileSystemMockRecorder is the mock recorder for MockFileSystem. +type MockFileSystemMockRecorder struct { + mock *MockFileSystem +} + +// NewMockFileSystem creates a new mock instance. +func NewMockFileSystem(ctrl *gomock.Controller) *MockFileSystem { + mock := &MockFileSystem{ctrl: ctrl} + mock.recorder = &MockFileSystemMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFileSystem) EXPECT() *MockFileSystemMockRecorder { + return m.recorder +} + +// FormatVolume mocks base method. +func (m *MockFileSystem) FormatVolume(arg0 context.Context, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FormatVolume", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// FormatVolume indicates an expected call of FormatVolume. +func (mr *MockFileSystemMockRecorder) FormatVolume(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatVolume", reflect.TypeOf((*MockFileSystem)(nil).FormatVolume), arg0, arg1, arg2) +} + +// RepairVolume mocks base method. +func (m *MockFileSystem) RepairVolume(arg0 context.Context, arg1, arg2 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RepairVolume", arg0, arg1, arg2) +} + +// RepairVolume indicates an expected call of RepairVolume. +func (mr *MockFileSystemMockRecorder) RepairVolume(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepairVolume", reflect.TypeOf((*MockFileSystem)(nil).RepairVolume), arg0, arg1, arg2) +} diff --git a/mocks/mock_utils/mock_iscsi/mock_iscsi_mount_client.go b/mocks/mock_utils/mock_iscsi/mock_iscsi_mount_client.go new file mode 100644 index 000000000..edc4c3bc4 --- /dev/null +++ b/mocks/mock_utils/mock_iscsi/mock_iscsi_mount_client.go @@ -0,0 +1,69 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/utils/iscsi (interfaces: Mount) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_mount_client.go github.com/netapp/trident/utils/iscsi Mount +// + +// Package mock_iscsi is a generated GoMock package. +package mock_iscsi + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockMount is a mock of Mount interface. +type MockMount struct { + ctrl *gomock.Controller + recorder *MockMountMockRecorder +} + +// MockMountMockRecorder is the mock recorder for MockMount. +type MockMountMockRecorder struct { + mock *MockMount +} + +// NewMockMount creates a new mock instance. +func NewMockMount(ctrl *gomock.Controller) *MockMount { + mock := &MockMount{ctrl: ctrl} + mock.recorder = &MockMountMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMount) EXPECT() *MockMountMockRecorder { + return m.recorder +} + +// IsMounted mocks base method. +func (m *MockMount) IsMounted(arg0 context.Context, arg1, arg2, arg3 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsMounted", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsMounted indicates an expected call of IsMounted. +func (mr *MockMountMockRecorder) IsMounted(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMounted", reflect.TypeOf((*MockMount)(nil).IsMounted), arg0, arg1, arg2, arg3) +} + +// MountDevice mocks base method. +func (m *MockMount) MountDevice(arg0 context.Context, arg1, arg2, arg3 string, arg4 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MountDevice", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// MountDevice indicates an expected call of MountDevice. +func (mr *MockMountMockRecorder) MountDevice(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MountDevice", reflect.TypeOf((*MockMount)(nil).MountDevice), arg0, arg1, arg2, arg3, arg4) +} diff --git a/mocks/mock_utils/mock_iscsi/mock_iscsi_os_client.go b/mocks/mock_utils/mock_iscsi/mock_iscsi_os_client.go new file mode 100644 index 000000000..6edc22dcc --- /dev/null +++ b/mocks/mock_utils/mock_iscsi/mock_iscsi_os_client.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netapp/trident/utils/iscsi (interfaces: OS) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_os_client.go github.com/netapp/trident/utils/iscsi OS +// + +// Package mock_iscsi is a generated GoMock package. +package mock_iscsi + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockOS is a mock of OS interface. +type MockOS struct { + ctrl *gomock.Controller + recorder *MockOSMockRecorder +} + +// MockOSMockRecorder is the mock recorder for MockOS. +type MockOSMockRecorder struct { + mock *MockOS +} + +// NewMockOS creates a new mock instance. +func NewMockOS(ctrl *gomock.Controller) *MockOS { + mock := &MockOS{ctrl: ctrl} + mock.recorder = &MockOSMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOS) EXPECT() *MockOSMockRecorder { + return m.recorder +} + +// PathExists mocks base method. +func (m *MockOS) PathExists(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PathExists", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PathExists indicates an expected call of PathExists. +func (mr *MockOSMockRecorder) PathExists(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PathExists", reflect.TypeOf((*MockOS)(nil).PathExists), arg0) +} diff --git a/mocks/mock_utils/mock_reconcile_utils/reconcile_utils.go b/mocks/mock_utils/mock_iscsi/mock_reconcile_utils.go similarity index 97% rename from mocks/mock_utils/mock_reconcile_utils/reconcile_utils.go rename to mocks/mock_utils/mock_iscsi/mock_reconcile_utils.go index f9cbb5165..9975d068a 100644 --- a/mocks/mock_utils/mock_reconcile_utils/reconcile_utils.go +++ b/mocks/mock_utils/mock_iscsi/mock_reconcile_utils.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=../../mocks/mock_utils/mock_reconcile_utils/reconcile_utils.go github.com/netapp/trident/utils/iscsi IscsiReconcileUtils +// mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_reconcile_utils.go github.com/netapp/trident/utils/iscsi IscsiReconcileUtils // // Package mock_iscsi is a generated GoMock package. diff --git a/utils/devices.go b/utils/devices.go index 69be7aa9f..cfabdc048 100644 --- a/utils/devices.go +++ b/utils/devices.go @@ -1193,5 +1193,5 @@ func findMultipathDeviceForDevice(ctx context.Context, device string) string { } func getLunSerial(ctx context.Context, path string) (string, error) { - return iscsi.GetLunSerial(ctx, path) + return iscsiClient.GetLunSerial(ctx, path) } diff --git a/utils/iscsi.go b/utils/iscsi.go index fd6f6b51a..ad28b4fab 100644 --- a/utils/iscsi.go +++ b/utils/iscsi.go @@ -14,6 +14,8 @@ import ( "strings" "time" + "github.com/spf13/afero" + "github.com/netapp/trident/config" "github.com/netapp/trident/internal/fiji" . "github.com/netapp/trident/logging" @@ -36,7 +38,7 @@ const ( var ( IscsiUtils = iscsi.NewReconcileUtils(chrootPathPrefix, NewOSClient()) iscsiClient = iscsi.NewDetailed(chrootPathPrefix, command, iscsi.DefaultSelfHealingExclusion, NewOSClient(), - NewDevicesClient(), NewFilesystemClient(), NewMountClient(), IscsiUtils) + NewDevicesClient(), NewFilesystemClient(), NewMountClient(), IscsiUtils, afero.Afero{Fs: afero.NewOsFs()}) // Non-persistent map to maintain flush delays/errors if any, for device path(s). iSCSIVolumeFlushExceptions = make(map[string]time.Time) @@ -996,7 +998,7 @@ func execIscsiadmCommand(ctx context.Context, args ...string) ([]byte, error) { } func listAllISCSIDevices(ctx context.Context) { - iscsi.ListAllDevices(ctx) + iscsiClient.ListAllDevices(ctx) } func getISCSISessionInfo(ctx context.Context) ([]iscsi.SessionInfo, error) { diff --git a/utils/iscsi/expose.go b/utils/iscsi/expose.go index fe2b84084..6a959f573 100644 --- a/utils/iscsi/expose.go +++ b/utils/iscsi/expose.go @@ -12,8 +12,8 @@ func (client *Client) ExecIscsiadmCommand(ctx context.Context, args ...string) ( return client.execIscsiadmCommand(ctx, args...) } -func ListAllDevices(ctx context.Context) { - listAllDevices(ctx) +func (client *Client) ListAllDevices(ctx context.Context) { + client.listAllDevices(ctx) } func (client *Client) GetSessionInfo(ctx context.Context) ([]SessionInfo, error) { @@ -46,10 +46,10 @@ func (client *Client) FindMultipathDeviceForDevice(ctx context.Context, device s return client.findMultipathDeviceForDevice(ctx, device) } -func GetLunSerial(ctx context.Context, path string) (string, error) { - return getLunSerial(ctx, path) +func (client *Client) GetLunSerial(ctx context.Context, path string) (string, error) { + return client.getLunSerial(ctx, path) } -func FormatPortal(portal string) string { - return formatPortal(portal) +func (client *Client) IsSessionStale(ctx context.Context, sessionID string) bool { + return client.isSessionStale(ctx, sessionID) } diff --git a/utils/iscsi/iscsi.go b/utils/iscsi/iscsi.go index 8c0cd92d5..768617898 100644 --- a/utils/iscsi/iscsi.go +++ b/utils/iscsi/iscsi.go @@ -3,6 +3,10 @@ package iscsi //go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_client.go github.com/netapp/trident/utils/iscsi ISCSI +//go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_os_client.go github.com/netapp/trident/utils/iscsi OS +//go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_devices_client.go github.com/netapp/trident/utils/iscsi Devices +//go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_filesystem_client.go github.com/netapp/trident/utils/iscsi FileSystem +//go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_iscsi_mount_client.go github.com/netapp/trident/utils/iscsi Mount import ( "context" @@ -18,6 +22,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/spf13/afero" "github.com/netapp/trident/config" "github.com/netapp/trident/internal/fiji" @@ -112,6 +117,7 @@ type Client struct { fileSystemClient FileSystem mountClient Mount iscsiUtils IscsiReconcileUtils + os afero.Afero } func New(osClient OS, deviceClient Devices, fileSystemClient FileSystem, mountClient Mount) *Client { @@ -121,14 +127,15 @@ func New(osClient OS, deviceClient Devices, fileSystemClient FileSystem, mountCl } reconcileutils := NewReconcileUtils(chrootPathPrefix, osClient) + osUtils := afero.Afero{Fs: afero.NewOsFs()} return NewDetailed(chrootPathPrefix, tridentexec.NewCommand(), DefaultSelfHealingExclusion, osClient, - deviceClient, fileSystemClient, mountClient, reconcileutils) + deviceClient, fileSystemClient, mountClient, reconcileutils, osUtils) } -func NewDetailed(chrootPathPrefix string, command tridentexec.Command, selfHealingExclusion []string, - osClient OS, deviceClient Devices, fileSystemClient FileSystem, mountClient Mount, - iscsiUtils IscsiReconcileUtils, +func NewDetailed(chrootPathPrefix string, command tridentexec.Command, selfHealingExclusion []string, osClient OS, + deviceClient Devices, fileSystemClient FileSystem, mountClient Mount, iscsiUtils IscsiReconcileUtils, + os afero.Afero, ) *Client { return &Client{ chrootPathPrefix: chrootPathPrefix, @@ -139,6 +146,7 @@ func NewDetailed(chrootPathPrefix string, command tridentexec.Command, selfHeali mountClient: mountClient, iscsiUtils: iscsiUtils, selfHealingExclusion: selfHealingExclusion, + os: os, } } @@ -178,6 +186,260 @@ func (client *Client) AttachVolumeRetry( return mpathSize, err } +// AttachVolume attaches the volume to the local host. +// This method must be able to accomplish its task using only the publish information passed in. +// It may be assumed that this method always runs on the host to which the volume will be attached. +// If the mountpoint parameter is specified, the volume will be mounted to it. +// The device path is set on the in-out publishInfo parameter so that it may be mounted later instead. +// If multipath device size is found to be inconsistent with device size, then the correct size is returned. +func (client *Client) AttachVolume( + ctx context.Context, name, mountPoint string, publishInfo *models.VolumePublishInfo, + secrets map[string]string, +) (int64, error) { + Logc(ctx).Debug(">>>> iscsi.AttachVolume") + defer Logc(ctx).Debug("<<<< iscsi.AttachVolume") + + var err error + var mpathSize int64 + lunID := int(publishInfo.IscsiLunNumber) + + var portals []string + + // IscsiTargetPortal is one of the ports on the target and IscsiPortals + // are rest of the target ports for establishing iSCSI session. + // If the target has multiple portals, then there will be multiple iSCSI sessions. + portals = append(portals, ensureHostportFormatted(publishInfo.IscsiTargetPortal)) + + for _, p := range publishInfo.IscsiPortals { + portals = append(portals, ensureHostportFormatted(p)) + } + + if publishInfo.IscsiInterface == "" { + publishInfo.IscsiInterface = "default" + } + + Logc(ctx).WithFields(LogFields{ + "volume": name, + "mountPoint": mountPoint, + "lunID": lunID, + "portals": portals, + "targetIQN": publishInfo.IscsiTargetIQN, + "iscsiInterface": publishInfo.IscsiInterface, + "fstype": publishInfo.FilesystemType, + }).Debug("Attaching iSCSI volume.") + + if err = client.PreChecks(ctx); err != nil { + return mpathSize, err + } + // Ensure we are logged into correct portals + pendingPortalsToLogin, loggedIn, err := client.portalsToLogin(ctx, publishInfo.IscsiTargetIQN, portals) + if err != nil { + return mpathSize, err + } + + newLogin, err := client.EnsureSessions(ctx, publishInfo, pendingPortalsToLogin) + + if !loggedIn && !newLogin { + return mpathSize, err + } + + // First attempt to fix invalid serials by rescanning them + err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, + client.rescanOneLun) + if err != nil { + return mpathSize, err + } + + // Then attempt to fix invalid serials by purging them (to be scanned + // again later) + err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, + client.purgeOneLun) + if err != nil { + return mpathSize, err + } + + // Scan the target and wait for the device(s) to appear + err = client.waitForDeviceScan(ctx, lunID, publishInfo.IscsiTargetIQN) + if err != nil { + Logc(ctx).Errorf("Could not find iSCSI device: %+v", err) + return mpathSize, err + } + + // At this point if the serials are still invalid, give up so the + // caller can retry (invoking the remediation steps above in the + // process, if they haven't already been run). + failHandler := func(ctx context.Context, path string) error { + Logc(ctx).Error("Detected LUN serial number mismatch, attaching volume would risk data corruption, giving up") + return fmt.Errorf("LUN serial number mismatch, kernel has stale cached data") + } + err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, failHandler) + if err != nil { + return mpathSize, err + } + + // Wait for multipath device i.e. /dev/dm-* for the given LUN + err = client.waitForMultipathDeviceForLUN(ctx, lunID, publishInfo.IscsiTargetIQN) + if err != nil { + return mpathSize, err + } + + // Lookup all the SCSI device information + deviceInfo, err := client.getDeviceInfoForLUN(ctx, lunID, publishInfo.IscsiTargetIQN, false, false) + if err != nil { + return mpathSize, fmt.Errorf("error getting iSCSI device information: %v", err) + } + + Logc(ctx).WithFields(LogFields{ + "scsiLun": deviceInfo.LUN, + "multipathDevice": deviceInfo.MultipathDevice, + "devices": deviceInfo.Devices, + "iqn": deviceInfo.IQN, + }).Debug("Found device.") + + // Make sure we use the proper device + deviceToUse := deviceInfo.MultipathDevice + + // To avoid LUN ID conflict with a ghost device below checks + // are necessary: + // Conflict 1: Due to race conditions, it is possible a ghost + // DM device is discovered instead of the actual + // DM device. + // Conflict 2: Some OS like RHEL displays the ghost device size + // instead of the actual LUN size. + // + // Below check ensures that the correct device with the correct + // size is being discovered. + + // If LUN Serial Number exists, then compare it with DM + // device's UUID in sysfs + if err = client.verifyMultipathDeviceSerial(ctx, deviceToUse, publishInfo.IscsiLunSerial); err != nil { + return mpathSize, err + } + + // Once the multipath device has been found, compare its size with + // the size of one of the devices, if it differs then mark it for + // resize after the staging. + correctMpathSize, mpathSizeCorrect, err := client.verifyMultipathDeviceSize(ctx, deviceToUse, deviceInfo.Devices[0]) + if err != nil { + Logc(ctx).WithFields(LogFields{ + "scsiLun": deviceInfo.LUN, + "multipathDevice": deviceInfo.MultipathDevice, + "device": deviceInfo.Devices[0], + "iqn": deviceInfo.IQN, + "err": err, + }).Error("Failed to verify multipath device size.") + + return mpathSize, fmt.Errorf("failed to verify multipath device %s size", deviceInfo.MultipathDevice) + } + + if !mpathSizeCorrect { + mpathSize = correctMpathSize + + Logc(ctx).WithFields(LogFields{ + "scsiLun": deviceInfo.LUN, + "multipathDevice": deviceInfo.MultipathDevice, + "device": deviceInfo.Devices[0], + "iqn": deviceInfo.IQN, + "mpathSize": mpathSize, + }).Error("Multipath device size does not match device size.") + } + + devicePath := "/dev/" + deviceToUse + if err := client.deviceClient.WaitForDevice(ctx, devicePath); err != nil { + return mpathSize, fmt.Errorf("could not find device %v; %s", devicePath, err) + } + + var isLUKSDevice, luksFormatted bool + if publishInfo.LUKSEncryption != "" { + isLUKSDevice, err = strconv.ParseBool(publishInfo.LUKSEncryption) + if err != nil { + return mpathSize, fmt.Errorf("could not parse LUKSEncryption into a bool, got %v", + publishInfo.LUKSEncryption) + } + } + + if isLUKSDevice { + luksDevice, _ := client.deviceClient.NewLUKSDevice(devicePath, name) + luksFormatted, err = client.deviceClient.EnsureLUKSDeviceMappedOnHost(ctx, luksDevice, name, secrets) + if err != nil { + return mpathSize, err + } + devicePath = luksDevice.MappedDevicePath() + } + + // Return the device in the publish info in case the mount will be done later + publishInfo.DevicePath = devicePath + + if publishInfo.FilesystemType == config.FsRaw { + return mpathSize, nil + } + + existingFstype, err := client.deviceClient.GetDeviceFSType(ctx, devicePath) + if err != nil { + return mpathSize, err + } + if existingFstype == "" { + if !isLUKSDevice { + if unformatted, err := client.deviceClient.IsDeviceUnformatted(ctx, devicePath); err != nil { + Logc(ctx).WithField("device", + devicePath).Errorf("Unable to identify if the device is formatted; err: %v", err) + return mpathSize, err + } else if !unformatted { + Logc(ctx).WithField("device", devicePath).Errorf("Device is already formatted; err: %v", err) + return mpathSize, fmt.Errorf("device %v is already formatted", devicePath) + } + } else { + // We can safely assume if we just luksFormatted the device, we can also add a filesystem without dataloss + if !luksFormatted { + Logc(ctx).WithField("device", + devicePath).Errorf("Unable to identify if the luks device is empty; err: %v", err) + return mpathSize, nil + } + } + + Logc(ctx).WithFields(LogFields{"volume": name, "fstype": publishInfo.FilesystemType}).Debug("Formatting LUN.") + if err = client.fileSystemClient.FormatVolume(ctx, devicePath, publishInfo.FilesystemType); err != nil { + return mpathSize, fmt.Errorf("error formatting LUN %s, device %s: %v", name, deviceToUse, err) + } + } else if existingFstype != unknownFstype && existingFstype != publishInfo.FilesystemType { + Logc(ctx).WithFields(LogFields{ + "volume": name, + "existingFstype": existingFstype, + "requestedFstype": publishInfo.FilesystemType, + }).Error("LUN already formatted with a different file system type.") + return mpathSize, fmt.Errorf("LUN %s, device %s already formatted with other filesystem: %s", + name, deviceToUse, existingFstype) + } else { + Logc(ctx).WithFields(LogFields{ + "volume": name, + "fstype": deviceInfo.Filesystem, + }).Debug("LUN already formatted.") + } + + // Attempt to resolve any filesystem inconsistencies that might be due to dirty node shutdowns, cloning + // in-use volumes, or creating volumes from snapshots taken from in-use volumes. This is only safe to do + // if a device is not mounted. The fsck command returns a non-zero exit code if filesystem errors are found, + // even if they are completely and automatically fixed, so we don't return any error here. + mounted, err := client.mountClient.IsMounted(ctx, devicePath, "", "") + if err != nil { + return mpathSize, err + } + if !mounted { + client.fileSystemClient.RepairVolume(ctx, devicePath, publishInfo.FilesystemType) + } + + // Optionally mount the device + if mountPoint != "" { + if err := client.mountClient.MountDevice(ctx, devicePath, mountPoint, publishInfo.MountOptions, + false); err != nil { + return mpathSize, fmt.Errorf("error mounting LUN %v, device %v, mountpoint %v; %s", + name, deviceToUse, mountPoint, err) + } + } + + return mpathSize, nil +} + // AddSession adds a portal and LUN data to the session map. Extracts the // required iSCSI Target IQN, CHAP Credentials if any from the provided VolumePublishInfo and // populates the map against the portal. @@ -273,8 +535,6 @@ func (client *Client) RescanDevices(ctx context.Context, targetIQN string, lunID deviceInfo, err := client.getDeviceInfoForLUN(ctx, int(lunID), targetIQN, false, false) if err != nil { return fmt.Errorf("error getting iSCSI device information: %s", err) - } else if deviceInfo == nil { - return fmt.Errorf("could not get iSCSI device information for LUN: %d", lunID) } allLargeEnough := true @@ -348,16 +608,19 @@ func (client *Client) rescanDisk(ctx context.Context, deviceName string) error { Logc(ctx).WithFields(fields).Debug(">>>> iscsi.rescanDisk") defer Logc(ctx).WithFields(fields).Debug("<<<< iscsi.rescanDisk") - listAllDevices(ctx) + client.listAllDevices(ctx) filename := fmt.Sprintf(client.chrootPathPrefix+"/sys/block/%s/device/rescan", deviceName) Logc(ctx).WithField("filename", filename).Debug("Opening file for writing.") - f, err := os.OpenFile(filename, os.O_WRONLY, 0) + f, err := client.os.OpenFile(filename, os.O_WRONLY, 0) if err != nil { Logc(ctx).WithField("file", filename).Warning("Could not open file for writing.") return err } - defer f.Close() + + defer func() { + _ = f.Close() + }() written, err := f.WriteString("1") if err != nil { @@ -371,7 +634,7 @@ func (client *Client) rescanDisk(ctx context.Context, deviceName string) error { return fmt.Errorf("no data written to %s", filename) } - listAllDevices(ctx) + client.listAllDevices(ctx) return nil } @@ -416,269 +679,6 @@ func (client *Client) IsAlreadyAttached(ctx context.Context, lunID int, targetIq return 0 < len(devices) } -// AttachVolume attaches the volume to the local host. -// This method must be able to accomplish its task using only the publish information passed in. -// It may be assumed that this method always runs on the host to which the volume will be attached. -// If the mountpoint parameter is specified, the volume will be mounted to it. -// The device path is set on the in-out publishInfo parameter so that it may be mounted later instead. -// If multipath device size is found to be inconsistent with device size, then the correct size is returned. -func (client *Client) AttachVolume( - ctx context.Context, name, mountPoint string, publishInfo *models.VolumePublishInfo, - secrets map[string]string, -) (int64, error) { - Logc(ctx).Debug(">>>> iscsi.AttachVolume") - defer Logc(ctx).Debug("<<<< iscsi.AttachVolume") - - var err error - var mpathSize int64 - lunID := int(publishInfo.IscsiLunNumber) - - var portals []string - - // IscsiTargetPortal is one of the ports on the target and IscsiPortals - // are rest of the target ports for establishing iSCSI session. - // If the target has multiple portals, then there will be multiple iSCSI sessions. - portals = append(portals, ensureHostportFormatted(publishInfo.IscsiTargetPortal)) - - for _, p := range publishInfo.IscsiPortals { - portals = append(portals, ensureHostportFormatted(p)) - } - - if publishInfo.IscsiInterface == "" { - publishInfo.IscsiInterface = "default" - } - - Logc(ctx).WithFields(LogFields{ - "volume": name, - "mountPoint": mountPoint, - "lunID": lunID, - "portals": portals, - "targetIQN": publishInfo.IscsiTargetIQN, - "iscsiInterface": publishInfo.IscsiInterface, - "fstype": publishInfo.FilesystemType, - }).Debug("Attaching iSCSI volume.") - - if err = client.PreChecks(ctx); err != nil { - return mpathSize, err - } - // Ensure we are logged into correct portals - pendingPortalsToLogin, loggedIn, err := client.portalsToLogin(ctx, publishInfo.IscsiTargetIQN, portals) - if err != nil { - return mpathSize, err - } - - newLogin, err := client.EnsureSessions(ctx, publishInfo, pendingPortalsToLogin) - - if !loggedIn && !newLogin { - return mpathSize, err - } - - // First attempt to fix invalid serials by rescanning them - err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, rescanOneLun) - if err != nil { - return mpathSize, err - } - - // Then attempt to fix invalid serials by purging them (to be scanned - // again later) - err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, purgeOneLun) - if err != nil { - return mpathSize, err - } - - // Scan the target and wait for the device(s) to appear - err = client.waitForDeviceScan(ctx, lunID, publishInfo.IscsiTargetIQN) - if err != nil { - Logc(ctx).Errorf("Could not find iSCSI device: %+v", err) - return mpathSize, err - } - - // At this point if the serials are still invalid, give up so the - // caller can retry (invoking the remediation steps above in the - // process, if they haven't already been run). - failHandler := func(ctx context.Context, path string) error { - Logc(ctx).Error("Detected LUN serial number mismatch, attaching volume would risk data corruption, giving up") - return fmt.Errorf("LUN serial number mismatch, kernel has stale cached data") - } - err = client.handleInvalidSerials(ctx, lunID, publishInfo.IscsiTargetIQN, publishInfo.IscsiLunSerial, failHandler) - if err != nil { - return mpathSize, err - } - - // Wait for multipath device i.e. /dev/dm-* for the given LUN - err = client.waitForMultipathDeviceForLUN(ctx, lunID, publishInfo.IscsiTargetIQN) - if err != nil { - return mpathSize, err - } - - // Lookup all the SCSI device information - deviceInfo, err := client.getDeviceInfoForLUN(ctx, lunID, publishInfo.IscsiTargetIQN, false, false) - if err != nil { - return mpathSize, fmt.Errorf("error getting iSCSI device information: %v", err) - } else if deviceInfo == nil { - return mpathSize, fmt.Errorf("could not get iSCSI device information for LUN %d", lunID) - } - - Logc(ctx).WithFields(LogFields{ - "scsiLun": deviceInfo.LUN, - "multipathDevice": deviceInfo.MultipathDevice, - "devices": deviceInfo.Devices, - "iqn": deviceInfo.IQN, - }).Debug("Found device.") - - // Make sure we use the proper device - deviceToUse := deviceInfo.Devices[0] - if deviceInfo.MultipathDevice != "" { - deviceToUse = deviceInfo.MultipathDevice - - // To avoid LUN ID conflict with a ghost device below checks - // are necessary: - // Conflict 1: Due to race conditions, it is possible a ghost - // DM device is discovered instead of the actual - // DM device. - // Conflict 2: Some OS like RHEL displays the ghost device size - // instead of the actual LUN size. - // - // Below check ensures that the correct device with the correct - // size is being discovered. - - // If LUN Serial Number exists, then compare it with DM - // device's UUID in sysfs - if err = client.verifyMultipathDeviceSerial(ctx, deviceToUse, publishInfo.IscsiLunSerial); err != nil { - return mpathSize, err - } - - // Once the multipath device has been found, compare its size with - // the size of one of the devices, if it differs then mark it for - // resize after the staging. - correctMpathSize, mpathSizeCorrect, err := client.verifyMultipathDeviceSize(ctx, deviceToUse, deviceInfo.Devices[0]) - if err != nil { - Logc(ctx).WithFields(LogFields{ - "scsiLun": deviceInfo.LUN, - "multipathDevice": deviceInfo.MultipathDevice, - "device": deviceInfo.Devices[0], - "iqn": deviceInfo.IQN, - "err": err, - }).Error("Failed to verify multipath device size.") - - return mpathSize, fmt.Errorf("failed to verify multipath device %s size", deviceInfo.MultipathDevice) - } - - if !mpathSizeCorrect { - mpathSize = correctMpathSize - - Logc(ctx).WithFields(LogFields{ - "scsiLun": deviceInfo.LUN, - "multipathDevice": deviceInfo.MultipathDevice, - "device": deviceInfo.Devices[0], - "iqn": deviceInfo.IQN, - "mpathSize": mpathSize, - }).Error("Multipath device size does not match device size.") - } - } else { - return mpathSize, fmt.Errorf("could not find multipath device for LUN %d", lunID) - } - - if deviceToUse == "" { - return mpathSize, fmt.Errorf("could not determine device to use for %v", name) - } - devicePath := "/dev/" + deviceToUse - if err := client.deviceClient.WaitForDevice(ctx, devicePath); err != nil { - return mpathSize, fmt.Errorf("could not find device %v; %s", devicePath, err) - } - - var isLUKSDevice, luksFormatted bool - if publishInfo.LUKSEncryption != "" { - isLUKSDevice, err = strconv.ParseBool(publishInfo.LUKSEncryption) - if err != nil { - return mpathSize, fmt.Errorf("could not parse LUKSEncryption into a bool, got %v", - publishInfo.LUKSEncryption) - } - } - - if isLUKSDevice { - luksDevice, _ := client.deviceClient.NewLUKSDevice(devicePath, name) - luksFormatted, err = client.deviceClient.EnsureLUKSDeviceMappedOnHost(ctx, luksDevice, name, secrets) - if err != nil { - return mpathSize, err - } - devicePath = luksDevice.MappedDevicePath() - } - - // Return the device in the publish info in case the mount will be done later - publishInfo.DevicePath = devicePath - - if publishInfo.FilesystemType == config.FsRaw { - return mpathSize, nil - } - - existingFstype, err := client.deviceClient.GetDeviceFSType(ctx, devicePath) - if err != nil { - return mpathSize, err - } - if existingFstype == "" { - if !isLUKSDevice { - if unformatted, err := client.deviceClient.IsDeviceUnformatted(ctx, devicePath); err != nil { - Logc(ctx).WithField("device", - devicePath).Errorf("Unable to identify if the device is unformatted; err: %v", err) - return mpathSize, err - } else if !unformatted { - Logc(ctx).WithField("device", devicePath).Errorf("Device is not unformatted; err: %v", err) - return mpathSize, fmt.Errorf("device %v is not unformatted", devicePath) - } - } else { - // We can safely assume if we just luksFormatted the device, we can also add a filesystem without dataloss - if !luksFormatted { - Logc(ctx).WithField("device", - devicePath).Errorf("Unable to identify if the luks device is empty; err: %v", err) - return mpathSize, err - } - } - - Logc(ctx).WithFields(LogFields{"volume": name, "fstype": publishInfo.FilesystemType}).Debug("Formatting LUN.") - err := client.fileSystemClient.FormatVolume(ctx, devicePath, publishInfo.FilesystemType) - if err != nil { - return mpathSize, fmt.Errorf("error formatting LUN %s, device %s: %v", name, deviceToUse, err) - } - } else if existingFstype != unknownFstype && existingFstype != publishInfo.FilesystemType { - Logc(ctx).WithFields(LogFields{ - "volume": name, - "existingFstype": existingFstype, - "requestedFstype": publishInfo.FilesystemType, - }).Error("LUN already formatted with a different file system type.") - return mpathSize, fmt.Errorf("LUN %s, device %s already formatted with other filesystem: %s", - name, deviceToUse, existingFstype) - } else { - Logc(ctx).WithFields(LogFields{ - "volume": name, - "fstype": deviceInfo.Filesystem, - }).Debug("LUN already formatted.") - } - - // Attempt to resolve any filesystem inconsistencies that might be due to dirty node shutdowns, cloning - // in-use volumes, or creating volumes from snapshots taken from in-use volumes. This is only safe to do - // if a device is not mounted. The fsck command returns a non-zero exit code if filesystem errors are found, - // even if they are completely and automatically fixed, so we don't return any error here. - mounted, err := client.mountClient.IsMounted(ctx, devicePath, "", "") - if err != nil { - return mpathSize, err - } - if !mounted { - client.fileSystemClient.RepairVolume(ctx, devicePath, publishInfo.FilesystemType) - } - - // Optionally mount the device - if mountPoint != "" { - if err := client.mountClient.MountDevice(ctx, devicePath, mountPoint, publishInfo.MountOptions, - false); err != nil { - return mpathSize, fmt.Errorf("error mounting LUN %v, device %v, mountpoint %v; %s", - name, deviceToUse, mountPoint, err) - } - } - - return mpathSize, nil -} - // ScsiDeviceInfo contains information about SCSI devices type ScsiDeviceInfo struct { Host string @@ -746,8 +746,7 @@ func (client *Client) getDeviceInfoForLUN( fsType := "" if needFSType { - err = client.deviceClient.EnsureDeviceReadable(ctx, devicePath) - if err != nil { + if err = client.deviceClient.EnsureDeviceReadable(ctx, devicePath); err != nil { return nil, err } @@ -779,11 +778,11 @@ func (client *Client) getDeviceInfoForLUN( } // purgeOneLun issues a delete for one LUN, based on the sysfs path -func purgeOneLun(ctx context.Context, path string) error { +func (client *Client) purgeOneLun(ctx context.Context, path string) error { Logc(ctx).WithField("path", path).Debug("Purging one LUN") filename := path + "/delete" - f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200) + f, err := client.os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200) if err != nil { Logc(ctx).WithField("file", filename).Warning("Could not open file for writing.") return err @@ -809,11 +808,11 @@ func purgeOneLun(ctx context.Context, path string) error { } // rescanOneLun issues a rescan for one LUN, based on the sysfs path -func rescanOneLun(ctx context.Context, path string) error { +func (client *Client) rescanOneLun(ctx context.Context, path string) error { Logc(ctx).WithField("path", path).Debug("Rescaning one LUN") filename := path + "/rescan" - f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200) + f, err := client.os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200) if err != nil { Logc(ctx).WithField("file", filename).Warning("Could not open file for writing.") return err @@ -897,7 +896,7 @@ func (client *Client) findMultipathDeviceForDevice(ctx context.Context, device s defer Logc(ctx).WithField("device", device).Debug("<<<< iscsi.findMultipathDeviceForDevice") holdersDir := client.chrootPathPrefix + "/sys/block/" + device + "/holders" - if dirs, err := os.ReadDir(holdersDir); err == nil { + if dirs, err := client.os.ReadDir(holdersDir); err == nil { for _, f := range dirs { name := f.Name() if strings.HasPrefix(name, "dm-") { @@ -931,7 +930,7 @@ func (client *Client) waitForDeviceScan(ctx context.Context, lunID int, iSCSINod hosts = append(hosts, hostNumber) } - if err := client.ScanTargetLUN(ctx, lunID, hosts); err != nil { + if err := client.scanTargetLUN(ctx, lunID, hosts); err != nil { Logc(ctx).WithField("scanError", err).Error("Could not scan for new LUN.") } @@ -1000,7 +999,7 @@ func (client *Client) scanTargetLUN(ctx context.Context, lunID int, hosts []int) defer Logc(ctx).WithFields(fields).Debug("<<<< iscsi.scanTargetLUN") var ( - f *os.File + f afero.File err error ) @@ -1010,11 +1009,11 @@ func (client *Client) scanTargetLUN(ctx context.Context, lunID int, hosts []int) scanCmd = fmt.Sprintf("0 0 %d", lunID) } - listAllDevices(ctx) + client.listAllDevices(ctx) for _, hostNumber := range hosts { filename := fmt.Sprintf(client.chrootPathPrefix+"/sys/class/scsi_host/host%d/scan", hostNumber) - if f, err = os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200); err != nil { + if f, err = client.os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o200); err != nil { Logc(ctx).WithField("file", filename).Warning("Could not open file for writing.") return err } @@ -1025,17 +1024,17 @@ func (client *Client) scanTargetLUN(ctx context.Context, lunID int, hosts []int) if written, err := f.WriteString(scanCmd); err != nil { Logc(ctx).WithFields(LogFields{"file": filename, "error": err}).Warning("Could not write to file.") - f.Close() + _ = f.Close() return err } else if written == 0 { Logc(ctx).WithField("file", filename).Warning("No data written to file.") - f.Close() + _ = f.Close() return fmt.Errorf("no data written to %s", filename) } - f.Close() + _ = f.Close() - listAllDevices(ctx) + client.listAllDevices(ctx) Logc(ctx).WithFields(LogFields{ "scanCmd": scanCmd, "scanFile": filename, @@ -1060,7 +1059,7 @@ func (client *Client) handleInvalidSerials( hostSessionMap := client.iscsiUtils.GetISCSIHostSessionMapForTarget(ctx, targetIqn) paths := client.iscsiUtils.GetSysfsBlockDirsForLUN(lunID, hostSessionMap) for _, path := range paths { - serial, err := getLunSerial(ctx, path) + serial, err := client.getLunSerial(ctx, path) if err != nil { if os.IsNotExist(err) { // LUN either isn't scanned yet, or this kernel @@ -1102,14 +1101,14 @@ func (client *Client) handleInvalidSerials( } // getLunSerial get Linux's idea of what the LUN serial number is -func getLunSerial(ctx context.Context, path string) (string, error) { +func (client *Client) getLunSerial(ctx context.Context, path string) (string, error) { Logc(ctx).WithField("path", path).Debug("Get LUN Serial") // We're going to read the SCSI VPD page 80 serial number // information. Linux helpfully provides this through sysfs // so we don't need to open the device and send the ioctl // ourselves. filename := path + "/vpd_pg80" - b, err := os.ReadFile(filename) + b, err := client.os.ReadFile(filename) if err != nil { return "", err } @@ -1153,25 +1152,28 @@ func (client *Client) portalsToLogin(ctx context.Context, targetIQN string, port } for _, e := range sessionInfo { - if e.TargetName == targetIQN { - // Portals (portalsNotLoggedIn) may/may not contain anything after ":", so instead of matching complete - // portal value (with e.Portal), check if e.Portal's IP address matches portal's IP address - matchFunc := func(main, val string) bool { - mainIpAddress := models.ParseHostportIP(main) - valIpAddress := models.ParseHostportIP(val) - - return mainIpAddress == valIpAddress - } - lenBeforeCheck := len(portalsNotLoggedIn) - portalsNotLoggedIn = RemoveStringFromSliceConditionally(portalsNotLoggedIn, e.Portal, matchFunc) - lenAfterCheck := len(portalsNotLoggedIn) + if e.TargetName != targetIQN { + continue + } - // If the portal is logged in ensure it is not stale - if lenBeforeCheck != lenAfterCheck { - if client.IsSessionStale(ctx, e.SID) { - portalsInStaleState = append(portalsInStaleState, e.Portal) - } + // Portals (portalsNotLoggedIn) may/may not contain anything after ":", so instead of matching complete + // portal value (with e.Portal), check if e.Portal's IP address matches portal's IP address + matchFunc := func(main, val string) bool { + mainIpAddress := models.ParseHostportIP(main) + valIpAddress := models.ParseHostportIP(val) + + return mainIpAddress == valIpAddress + } + + lenBeforeCheck := len(portalsNotLoggedIn) + portalsNotLoggedIn = RemoveStringFromSliceConditionally(portalsNotLoggedIn, e.Portal, matchFunc) + lenAfterCheck := len(portalsNotLoggedIn) + + // If the portal is logged in ensure it is not stale + if lenBeforeCheck != lenAfterCheck { + if client.isSessionStale(ctx, e.SID) { + portalsInStaleState = append(portalsInStaleState, e.Portal) } } } @@ -1188,13 +1190,13 @@ func (client *Client) portalsToLogin(ctx context.Context, targetIQN string, port // Looks that the state of an already established session to identify if it is // logged in or not, if it is not logged in then it could be a stale session. // For now, we are relying on the sysfs files -func (client *Client) IsSessionStale(ctx context.Context, sessionID string) bool { +func (client *Client) isSessionStale(ctx context.Context, sessionID string) bool { Logc(ctx).WithField("sessionID", sessionID).Debug(">>>> iscsi.IsSessionStale") defer Logc(ctx).Debug("<<<< iscsi.IsSessionStale") // Find the session state from the session at /sys/class/iscsi_session/sessionXXX/state filename := fmt.Sprintf(client.chrootPathPrefix+"/sys/class/iscsi_session/session%s/state", sessionID) - sessionStateBytes, err := os.ReadFile(filename) + sessionStateBytes, err := client.os.ReadFile(filename) if err != nil { Logc(ctx).WithFields(LogFields{ "path": filename, @@ -1401,7 +1403,7 @@ func (client *Client) EnsureSessions(ctx context.Context, publishInfo *models.Vo loginFailedDueToChap := false for _, portal := range portals { - listAllDevices(ctx) + client.listAllDevices(ctx) formattedPortal := formatPortal(portal) if err := client.ensureTarget(ctx, formattedPortal, publishInfo.IscsiTargetIQN, publishInfo.IscsiUsername, @@ -1426,16 +1428,17 @@ func (client *Client) EnsureSessions(ctx context.Context, publishInfo *models.Vo // Set scanning to manual // Swallow this error, someone is running an old version of Debian/Ubuntu - _ = client.configureTarget(ctx, publishInfo.IscsiTargetIQN, portal, "node.session.scan", "manual") + const sessionScanParam = "node.session.scan" + _ = client.configureTarget(ctx, publishInfo.IscsiTargetIQN, portal, sessionScanParam, "manual") // replacement_timeout controls how long iSCSI layer should wait for a timed-out path/session to reestablish // itself before failing any commands on it. - timeout_param := "node.session.timeo.replacement_timeout" - if err := client.configureTarget(ctx, publishInfo.IscsiTargetIQN, portal, timeout_param, "5"); err != nil { + const timeoutParam = "node.session.timeo.replacement_timeout" + if err := client.configureTarget(ctx, publishInfo.IscsiTargetIQN, portal, timeoutParam, "5"); err != nil { Logc(ctx).WithFields(LogFields{ "iqn": publishInfo.IscsiTargetIQN, "portal": portal, - "name": timeout_param, + "name": timeoutParam, "value": "5", "err": err, }).Errorf("set replacement timeout failed: %v", err) @@ -1531,7 +1534,7 @@ func (client *Client) LoginTarget(ctx context.Context, publishInfo *models.Volum defer Logc(ctx).Debug("<<<< iscsi.LoginTarget") args := []string{"-m", "node", "-T", publishInfo.IscsiTargetIQN, "-p", formatPortal(portal)} - listAllDevices(ctx) + client.listAllDevices(ctx) if publishInfo.UseCHAP { secretsToRedact := map[string]string{ "--value=" + publishInfo.IscsiUsername: "--value=" + REDACTED, @@ -1626,7 +1629,7 @@ func (client *Client) LoginTarget(ctx context.Context, publishInfo *models.Volum return err } - listAllDevices(ctx) + client.listAllDevices(ctx) return nil } @@ -1663,14 +1666,14 @@ func formatPortal(portal string) string { } // In the case of iscsi trace debug, log info about session and what devices are present -func listAllDevices(ctx context.Context) { +func (client *Client) listAllDevices(ctx context.Context) { Logc(ctx).Trace(">>>> iscsi.listAllDevices") defer Logc(ctx).Trace("<<<< iscsi.listAllDevices") // Log information about all the devices dmLog := make([]string, 0) sdLog := make([]string, 0) sysLog := make([]string, 0) - entries, _ := os.ReadDir(DevPrefix) + entries, _ := client.os.ReadDir(DevPrefix) for _, entry := range entries { if strings.HasPrefix(entry.Name(), "dm-") { dmLog = append(dmLog, entry.Name()) @@ -1680,7 +1683,7 @@ func listAllDevices(ctx context.Context) { } } - entries, _ = os.ReadDir("/sys/block/") + entries, _ = client.os.ReadDir("/sys/block/") for _, entry := range entries { sysLog = append(sysLog, entry.Name()) } @@ -1763,7 +1766,7 @@ func (client *Client) identifyFindMultipathsValue(ctx context.Context) (string, return "", fmt.Errorf("could not read multipathd configuration: %v", err) } - findMultipathsValue := GetFindMultipathValue(string(output)) + findMultipathsValue := getFindMultipathValue(string(output)) Logc(ctx).WithField("findMultipathsValue", findMultipathsValue).Debug("Multipath find_multipaths value found.") return findMultipathsValue, nil } @@ -1773,7 +1776,7 @@ func (client *Client) identifyFindMultipathsValue(ctx context.Context) (string, // no (or off): Create a multipath device for every path that is not explicitly disabled // yes (or on): Create a device if one of some conditions are met // other possible values: smart, greedy, strict -func GetFindMultipathValue(text string) string { +func getFindMultipathValue(text string) string { // This matches pattern in a multiline string of type " find_multipaths: yes" tagsWithIndentationRegex := regexp.MustCompile(`(?m)^[\t ]*find_multipaths[\t ]*["|']?(?P[\w-_]+)["|']?[\t ]*$`) tag := tagsWithIndentationRegex.FindStringSubmatch(text) @@ -1814,16 +1817,17 @@ func (client *Client) supported(ctx context.Context) bool { // Note: Adding iSCSI targets using sendtargets rather than static discover // ensures that targets are added with the correct target group portal tags. func (client *Client) ensureTarget( - ctx context.Context, tp, targetIqn, username, password, targetUsername, targetInitiatorSecret, iface string, + ctx context.Context, targetPortal, targetIqn, username, password, targetUsername, targetInitiatorSecret, + iface string, ) error { Logc(ctx).WithFields(LogFields{ "IQN": targetIqn, - "Portal": tp, + "Portal": targetPortal, "Interface": iface, }).Debug(">>>> iscsi.ensureTarget") defer Logc(ctx).Debug("<<<< iscsi.ensureTarget") - targets, err := client.getTargets(ctx, tp) + targets, err := client.getTargets(ctx, targetPortal) if err != nil { // Already logged return err @@ -1842,23 +1846,23 @@ func (client *Client) ensureTarget( // Ignore result _, _ = client.execIscsiadmCommandWithTimeout(ctx, iscsiadmLoginTimeout, "-m", "discoverydb", "-t", "st", "-p", - tp, + targetPortal, "-I", iface, "-o", "new") - err = client.updateDiscoveryDb(ctx, tp, iface, "discovery.sendtargets.auth.authmethod", "CHAP") + err = client.updateDiscoveryDb(ctx, targetPortal, iface, "discovery.sendtargets.auth.authmethod", "CHAP") if err != nil { // Already logged return err } - err = client.updateDiscoveryDb(ctx, tp, iface, "discovery.sendtargets.auth.username", username) + err = client.updateDiscoveryDb(ctx, targetPortal, iface, "discovery.sendtargets.auth.username", username) if err != nil { // Already logged return err } - err = client.updateDiscoveryDb(ctx, tp, iface, "discovery.sendtargets.auth.password", password) + err = client.updateDiscoveryDb(ctx, targetPortal, iface, "discovery.sendtargets.auth.password", password) if err != nil { // Already logged return err @@ -1867,13 +1871,13 @@ func (client *Client) ensureTarget( if targetUsername != "" && targetInitiatorSecret != "" { // Bidirectional CHAP case - err = client.updateDiscoveryDb(ctx, tp, iface, "discovery.sendtargets.auth.username_in", targetUsername) + err = client.updateDiscoveryDb(ctx, targetPortal, iface, "discovery.sendtargets.auth.username_in", targetUsername) if err != nil { // Already logged return err } - err = client.updateDiscoveryDb(ctx, tp, iface, "discovery.sendtargets.auth.password_in", + err = client.updateDiscoveryDb(ctx, targetPortal, iface, "discovery.sendtargets.auth.password_in", targetInitiatorSecret) if err != nil { // Already logged @@ -1885,10 +1889,10 @@ func (client *Client) ensureTarget( // Discovery is here. This will populate the iscsiadm database with the // All the nodes known to the given portal. output, err := client.execIscsiadmCommandWithTimeout(ctx, iscsiadmLoginTimeout, "-m", "discoverydb", - "-t", "st", "-p", tp, "-I", iface, "-D") + "-t", "st", "-p", targetPortal, "-I", iface, "-D") if err != nil { Logc(ctx).WithFields(LogFields{ - "portal": tp, + "portal": targetPortal, "error": err, "output": string(output), }).Error("Failed to discover targets") @@ -1901,7 +1905,7 @@ func (client *Client) ensureTarget( return fmt.Errorf("failed to discover targets: %v", err) } - targets = filterTargets(string(output), tp) + targets = filterTargets(string(output), targetPortal) for _, iqn := range targets { if targetIqn == iqn { Logc(ctx).WithField("Target", iqn).Info("Target discovered successfully") @@ -1911,7 +1915,7 @@ func (client *Client) ensureTarget( } Logc(ctx).WithFields(LogFields{ - "portal": tp, + "portal": targetPortal, "iqn": targetIqn, }).Warning("Target not discovered") return fmt.Errorf("target not discovered") diff --git a/utils/iscsi/iscsi_test.go b/utils/iscsi/iscsi_test.go index fe523809c..7a5dcb95e 100644 --- a/utils/iscsi/iscsi_test.go +++ b/utils/iscsi/iscsi_test.go @@ -4,26 +4,4679 @@ package iscsi import ( "context" - "strings" + "encoding/hex" + "fmt" + "os" "testing" + "time" + "github.com/spf13/afero" + "github.com/spf13/afero/mem" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" - . "github.com/netapp/trident/logging" + "github.com/netapp/trident/config" mockexec "github.com/netapp/trident/mocks/mock_utils/mock_exec" + "github.com/netapp/trident/mocks/mock_utils/mock_iscsi" + "github.com/netapp/trident/mocks/mock_utils/mock_models/mock_luks" "github.com/netapp/trident/utils/errors" + tridentexec "github.com/netapp/trident/utils/exec" + "github.com/netapp/trident/utils/models" ) -const multipathConf = ` -defaults { - user_friendly_names yes - find_multipaths no +func TestNew(t *testing.T) { + ctrl := gomock.NewController(t) + osClient := mock_iscsi.NewMockOS(ctrl) + devicesClient := mock_iscsi.NewMockDevices(ctrl) + FileSystemClient := mock_iscsi.NewMockFileSystem(ctrl) + mountClient := mock_iscsi.NewMockMount(ctrl) + + type parameters struct { + setUpEnvironment func() + } + tests := map[string]parameters{ + "docker plugin mode": { + setUpEnvironment: func() { + assert.Nil(t, os.Setenv("DOCKER_PLUGIN_MODE", "true")) + }, + }, + "not docker plugin mode": {}, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + if params.setUpEnvironment != nil { + params.setUpEnvironment() + } + + iscsiClient := New(osClient, devicesClient, FileSystemClient, mountClient) + assert.NotNil(t, iscsiClient) + }) + } +} + +func TestNewDetailed(t *testing.T) { + const chrootPathPrefix = "" + ctrl := gomock.NewController(t) + osClient := mock_iscsi.NewMockOS(ctrl) + devicesClient := mock_iscsi.NewMockDevices(ctrl) + FileSystemClient := mock_iscsi.NewMockFileSystem(ctrl) + mountClient := mock_iscsi.NewMockMount(ctrl) + command := mockexec.NewMockCommand(ctrl) + iscsiClient := NewDetailed(chrootPathPrefix, command, DefaultSelfHealingExclusion, osClient, devicesClient, FileSystemClient, + mountClient, nil, afero.Afero{Fs: afero.NewMemMapFs()}) + assert.NotNil(t, iscsiClient) +} + +func TestClient_AttachVolumeRetry(t *testing.T) { + type parameters struct { + chrootPathPrefix string + getCommand func(controller *gomock.Controller) tridentexec.Command + getOSClient func(controller *gomock.Controller) OS + getDeviceClient func(controller *gomock.Controller) Devices + getFileSystemClient func(controller *gomock.Controller) FileSystem + getMountClient func(controller *gomock.Controller) Mount + getReconcileUtils func(controller *gomock.Controller) IscsiReconcileUtils + getFileSystemUtils func() afero.Fs + + publishInfo models.VolumePublishInfo + + assertError assert.ErrorAssertionFunc + expectedMpathSize int64 + } + // the test timeout is set specifically to 2 seconds to ensure that the backoff retry runs one time. + const testTimeout = 2 * time.Second + const iscsiadmNodeOutput = `127.0.0.1:3260,1042 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25 +127.0.0.1:3260,1043 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25 +` + const iscsiadmSessionOutput = `tcp: [3] 127.0.0.1:3260,1028 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.3 (non-flash) +tcp: [4] 127.0.0.1:3260,1029 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.3 (non-flash)` + + const iscsiadmDiscoveryDBSendTargetsOutput = `127.0.0.1:3260,1042 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.3 +127.0.0.1:3260,1043 iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.3 +` + tests := map[string]parameters{ + "happy path": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil).Times(2) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil).Times(2) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil).Times(2) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session"). + Return([]byte(iscsiadmSessionOutput), nil).Times(2) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node"). + Return([]byte(iscsiadmNodeOutput), nil).Times(2) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", gomock.Any(), "-I", "default", "-D"). + Return([]byte(iscsiadmDiscoveryDBSendTargetsOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", + "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", "-p", "127.0.0.1:3260", "-o", "update", "-n", + "node.session.scan", "-v", "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", + "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", "-p", "127.0.0.1:3260", "-o", "update", "-n", + "node.session.timeo.replacement_timeout", "-v", "5").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", + "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", "-p", "127.0.0.1:3260", "--op=update", "--name", + "node.conn[0].timeo.login_timeout", "--value=10").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", + "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", "-p", "127.0.0.1:3260", "--op=update", "--name", + "node.session.initial_login_retry_max", "--value=1").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", "-p", "127.0.0.1:3260", + "--login").Return(nil, nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOS := mock_iscsi.NewMockOS(controller) + mockOS.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOS + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(config.FsExt4, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + mockMount.EXPECT().IsMounted(context.TODO(), "/dev/dm-0", "", "").Return(true, nil) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25").Return(map[int]int{0: 0}).Times(3) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(3) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + assert.NoError(t, fs.Mkdir("/sys/block/sda/holders", 777)) + _, err := fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{""}, + IscsiTargetIQN: "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.25", + }, + }, + }, + assertError: assert.NoError, + expectedMpathSize: 0, + }, + "pre check failure": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, errors.New("some error")) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{}, + assertError: assert.Error, + }, + "attach volume failure": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(" find_multipaths: yes"), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, + errors.New("some error")).Times(2) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{}, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + iscsiClient := NewDetailed(params.chrootPathPrefix, params.getCommand(ctrl), DefaultSelfHealingExclusion, + params.getOSClient(ctrl), params.getDeviceClient(ctrl), params.getFileSystemClient(ctrl), + params.getMountClient(ctrl), params.getReconcileUtils(ctrl), afero.Afero{Fs: params.getFileSystemUtils()}) + + mpathSize, err := iscsiClient.AttachVolumeRetry(context.TODO(), "", "", ¶ms.publishInfo, nil, + testTimeout) + if params.assertError != nil { + params.assertError(t, err) + } + + assert.Equal(t, params.expectedMpathSize, mpathSize) + }) + } +} + +func TestClient_AttachVolume(t *testing.T) { + type parameters struct { + chrootPathPrefix string + getCommand func(controller *gomock.Controller) tridentexec.Command + getOSClient func(controller *gomock.Controller) OS + getDeviceClient func(controller *gomock.Controller) Devices + getFileSystemClient func(controller *gomock.Controller) FileSystem + getMountClient func(controller *gomock.Controller) Mount + getReconcileUtils func(controller *gomock.Controller) IscsiReconcileUtils + getFileSystemUtils func() afero.Fs + + publishInfo models.VolumePublishInfo + volumeName string + volumeMountPoint string + volumeAuthSecrets map[string]string + + assertError assert.ErrorAssertionFunc + expectedMpathSize int64 + } + + const targetIQN = "iqn.2016-04.com.open-iscsi:ef9f41e2ffa7:vs.3" + const vpdpg80Serial = "SYA5GZFJ8G1M905GVH7H" + + const iscsiadmSessionOutput = `tcp: [3] 127.0.0.1:3260,1028 ` + targetIQN + ` (non-flash) +tcp: [4] 127.0.0.2:3260,1029 ` + targetIQN + ` (non-flash)` + const iscsiadmSessionOutputOneSession = "tcp: [3] 127.0.0.1:3260,1028 " + targetIQN + " (non-flash)" + + const iscsiadmNodeOutput = `127.0.0.1:3260,1042 ` + targetIQN + ` +127.0.0.1:3260,1043 ` + targetIQN + ` +` + const iscsiadmDiscoveryDBSendTargetsOutput = `127.0.0.1:3260,1042 ` + targetIQN + ` +127.0.0.1:3260,1043 ` + targetIQN + ` +` + + tests := map[string]parameters{ + "pre check failure: iscsiadm command not found": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + command := mockexec.NewMockCommand(controller) + command.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, errors.New("iscsiadm not found")) + return command + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "pre check failure: multipathd not running": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd"). + Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "multipathd", "show", "daemon"). + Return(nil, errors.New("some error")) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "pre check failure: find_multipaths value set to yes": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd"). + Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "multipathd", "show", "daemon"). + Return([]byte("pid 2509 idle"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("yes", false)), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "pre check failure: find_multipaths value set to smart": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd"). + Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "multipathd", "show", "daemon"). + Return([]byte("pid 2509 idle"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("smart", false)), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "portals to login: failed to get session info": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd"). + Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "multipathd", "show", "daemon"). + Return([]byte("pid 2509 idle"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session").Return(nil, errors.New("some error")) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "portals to login: stale session state": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd"). + Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "multipathd", "show", "daemon"). + Return([]byte("pid 2509 idle"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session"). + Return([]byte(iscsiadmSessionOutputOneSession), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node"). + Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", gomock.Any(), "-I", "default", "-D"). + Return([]byte(iscsiadmDiscoveryDBSendTargetsOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + + f, err := fs.Create("/sys/class/iscsi_session/session3/state") + assert.NoError(t, err) + _, err = f.Write([]byte("foo")) + assert.NoError(t, err) + + f, err = fs.Create("/sys/class/iscsi_session/session4/state") + assert.NoError(t, err) + _, err = f.Write([]byte("foo")) + assert.NoError(t, err) + + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "handle invalid serial: failure rescanning the LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: "SYA5GZFJ8G1M905GVH7I", + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "handle invalid serial: failure purging the LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(2) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(2) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: "SYA5GZFJ8G1M905GVH7I", + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "wait for device: device not yet present": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "ls", "-al", "/dev").Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "ls", "-al", "/dev/mapper/").Return(nil, + errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "ls", "-al", "/dev/disk/by-path").Return(nil, + errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "lsscsi").Return(nil, + errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "lsscsi", "-t").Return(nil, + errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "free").Return(nil, + errors.New("some error")) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(false, errors.New("some error")) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(3) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(3) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: "SYA5GZFJ8G1M905GVH7I", + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "handle invalid serial: kernel has stale cache data": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(4) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(4) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: "SYA5GZFJ8G1M905GVH7I", + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "wait for device: error getting device for LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(5) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(5) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return(nil, errors.New("some error")) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "error getting device information for LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return(nil, errors.New("some error")) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "failed to verify multipath device serial": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("", errors.New("some error")) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "failure to verify multipath device size": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), + errors.New("some error")) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "verify multipath device size: size mismatch": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(errors.New("some error")) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "invalid LUKS encryption value in publish info": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + LUKSEncryption: "foo", + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "failure ensuring LUKS device mapped on host": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().NewLUKSDevice("/dev/dm-0", "test-volume").Return(nil, nil) + mockDevices.EXPECT().EnsureLUKSDeviceMappedOnHost(context.TODO(), nil, "test-volume", + map[string]string{}).Return(false, errors.New("some error")) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + LUKSEncryption: "true", + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "LUKS volume with file system raw": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + + device := mock_luks.NewMockLUKSDeviceInterface(controller) + device.EXPECT().MappedDevicePath().Return("/dev/mapper/dm-0") + mockDevices.EXPECT().NewLUKSDevice("/dev/dm-0", "test-volume").Return(device, nil) + mockDevices.EXPECT().EnsureLUKSDeviceMappedOnHost(context.TODO(), device, "test-volume", + map[string]string{}).Return(true, nil) + + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + LUKSEncryption: "true", + FilesystemType: config.FsRaw, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.NoError, + }, + "failed to get file system type of device": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + + device := mock_luks.NewMockLUKSDeviceInterface(controller) + device.EXPECT().MappedDevicePath().Return("/dev/mapper/dm-0") + mockDevices.EXPECT().NewLUKSDevice("/dev/dm-0", "test-volume").Return(device, nil) + mockDevices.EXPECT().EnsureLUKSDeviceMappedOnHost(context.TODO(), device, "test-volume", + map[string]string{}).Return(true, nil) + + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/mapper/dm-0").Return("", errors.New("some error")) + + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + LUKSEncryption: "true", + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "LUKS device has no existing file system type and is not LUKS formatted": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + + device := mock_luks.NewMockLUKSDeviceInterface(controller) + device.EXPECT().MappedDevicePath().Return("/dev/mapper/dm-0") + mockDevices.EXPECT().NewLUKSDevice("/dev/dm-0", "test-volume").Return(device, nil) + mockDevices.EXPECT().EnsureLUKSDeviceMappedOnHost(context.TODO(), device, "test-volume", + map[string]string{}).Return(false, nil) + + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/mapper/dm-0").Return("", nil) + + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + LUKSEncryption: "true", + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.NoError, + }, + "failure determining if device is formatted": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return("", nil) + mockDevices.EXPECT().IsDeviceUnformatted(context.TODO(), "/dev/dm-0").Return(false, errors.New("some error")) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "device is already formatted": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return("", nil) + mockDevices.EXPECT().IsDeviceUnformatted(context.TODO(), "/dev/dm-0").Return(false, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "failure formatting LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return("", nil) + mockDevices.EXPECT().IsDeviceUnformatted(context.TODO(), "/dev/dm-0").Return(true, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + mockFileSystem.EXPECT().FormatVolume(context.TODO(), "/dev/dm-0", config.FsExt4).Return(errors.New("some error")) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "existing file system on device does not match the request file system": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(config.FsExt3, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "failure determining if LUN is mounted": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(unknownFstype, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + mockMount.EXPECT().IsMounted(context.TODO(), "/dev/dm-0", "", "").Return(false, errors.New("some error")) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "no mount path provided": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(unknownFstype, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + mockFileSystem.EXPECT().RepairVolume(context.TODO(), "/dev/dm-0", config.FsExt4) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + mockMount.EXPECT().IsMounted(context.TODO(), "/dev/dm-0", "", "").Return(false, nil) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.NoError, + }, + "failure mounting LUN": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(unknownFstype, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + mockFileSystem.EXPECT().RepairVolume(context.TODO(), "/dev/dm-0", config.FsExt4) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + mockMount.EXPECT().IsMounted(context.TODO(), "/dev/dm-0", "", "").Return(false, nil) + mockMount.EXPECT().MountDevice(context.TODO(), "/dev/dm-0", "/mnt/test-volume", "", + false).Return(errors.New("some error")) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.Error, + }, + "happy path": { + chrootPathPrefix: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-V").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "pgrep", "multipathd").Return([]byte("150"), nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipathd", 5*time.Second, false, "show", + "config").Return([]byte(multipathConfig("no", false)), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getOSClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists("/dev/sda/block").Return(true, nil) + return mockOsClient + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().WaitForDevice(context.TODO(), "/dev/dm-0").Return(nil) + mockDevices.EXPECT().GetDeviceFSType(context.TODO(), "/dev/dm-0").Return(unknownFstype, nil) + return mockDevices + }, + getFileSystemClient: func(controller *gomock.Controller) FileSystem { + mockFileSystem := mock_iscsi.NewMockFileSystem(controller) + mockFileSystem.EXPECT().RepairVolume(context.TODO(), "/dev/dm-0", config.FsExt4) + return mockFileSystem + }, + getMountClient: func(controller *gomock.Controller) Mount { + mockMount := mock_iscsi.NewMockMount(controller) + mockMount.EXPECT().IsMounted(context.TODO(), "/dev/dm-0", "", "").Return(false, nil) + mockMount.EXPECT().MountDevice(context.TODO(), "/dev/dm-0", "/mnt/test-volume", "", + false).Return(nil) + return mockMount + }, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN). + Return(map[int]int{0: 0}).Times(6) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}). + Times(6) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil).Times(2) + mockReconcileUtils.EXPECT().GetMultipathDeviceUUID("dm-0").Return("mpath-53594135475a464a3847314d3930354756483748", nil) + return mockReconcileUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/dev/sda/vpd_pg80") + assert.NoError(t, err) + + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/dev/sda/delete") + assert.NoError(t, err) + + err = fs.MkdirAll("/sys/block/sda/holders/dm-0", 777) + assert.NoError(t, err) + return fs + }, + publishInfo: models.VolumePublishInfo{ + FilesystemType: config.FsExt4, + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: "127.0.0.1", + IscsiPortals: []string{"127.0.0.2"}, + IscsiTargetIQN: targetIQN, + IscsiLunSerial: vpdpg80Serial, + }, + }, + }, + volumeName: "test-volume", + volumeMountPoint: "/mnt/test-volume", + volumeAuthSecrets: make(map[string]string, 0), + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + iscsiClient := NewDetailed(params.chrootPathPrefix, params.getCommand(ctrl), DefaultSelfHealingExclusion, + params.getOSClient(ctrl), params.getDeviceClient(ctrl), params.getFileSystemClient(ctrl), + params.getMountClient(ctrl), params.getReconcileUtils(ctrl), afero.Afero{Fs: params.getFileSystemUtils()}) + + mpathSize, err := iscsiClient.AttachVolume(context.TODO(), params.volumeName, params.volumeMountPoint, + ¶ms.publishInfo, params.volumeAuthSecrets) + if params.assertError != nil { + params.assertError(t, err) + } + + assert.Equal(t, params.expectedMpathSize, mpathSize) + }) + } +} + +func TestClient_AddSession(t *testing.T) { + type parameters struct { + sessions *models.ISCSISessions + publishInfo models.VolumePublishInfo + volID string + sessionNumber string + reasonInvalid models.PortalInvalid + } + + const solidfireTargetIQN = "iqn.2010-01.com.solidfire:target-1" + const netappTargetIQN = "iqn.2010-01.com.netapp:target-1" + const targetPortal = "127.0.0.1" + + tests := map[string]parameters{ + "empty sessions": { + sessions: nil, + }, + "solidfire target portal": { + sessions: &models.ISCSISessions{}, + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: solidfireTargetIQN, + }, + }, + }, + }, + "portal already exists in session info, missing target IQN": { + sessions: &models.ISCSISessions{ + Info: map[string]*models.ISCSISessionData{ + targetPortal: {}, + }, + }, + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: targetPortal, + }, + }, + }, + }, + "portal already exists in session info": { + sessions: &models.ISCSISessions{ + Info: map[string]*models.ISCSISessionData{ + targetPortal: {}, + }, + }, + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetPortal: targetPortal, + IscsiTargetIQN: netappTargetIQN, + }, + }, + }, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := New(nil, nil, nil, nil) + ctx := context.WithValue(context.TODO(), SessionInfoSource, "test") + client.AddSession(ctx, params.sessions, ¶ms.publishInfo, params.volID, + params.sessionNumber, params.reasonInvalid) + }) + } +} + +func TestClient_RescanDevices(t *testing.T) { + type parameters struct { + targetIQN string + lunID int32 + minSize int64 + + getReconcileUtils func(controller *gomock.Controller) IscsiReconcileUtils + getDeviceClient func(controller *gomock.Controller) Devices + getCommandClient func(controller *gomock.Controller) tridentexec.Command + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + } + + const targetIQN = "iqn.2010-01.com.netapp:target-1" + + tests := map[string]parameters{ + "error getting device information": { + targetIQN: targetIQN, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + return NewReconcileUtils("", nil) + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "error getting iscsi disk size": { + targetIQN: targetIQN, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), errors.New("some error")) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "failed to rescan disk": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "failure to resize the disk": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil).Times(2) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "error validating if disk is resized": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), errors.New("some error")) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "disk resized successfully": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + "failure getting multipath device size": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), errors.New("some error")) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "multipath device size already greater than min size": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + "failure reloading multipaths map": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + "/dev/dm-0").Return(nil, errors.New("some error")) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "error determining the size of the multipath device after reload": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), errors.New("some error")) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + "/dev/dm-0").Return(nil, nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "multipath device too small even after reloading multipath map": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil).Times(2) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + "/dev/dm-0").Return(nil, nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + }, + "multipath device successfully resized": { + targetIQN: targetIQN, + minSize: 1, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(0), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/dm-0").Return(int64(1), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + "/dev/dm-0").Return(nil, nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create("/sys/block/sda/device/rescan") + assert.NoError(t, err) + + _, err = fs.Create("/sys/block/sda/holders/dm-0") + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + "happy path": { + targetIQN: targetIQN, + getReconcileUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + getDeviceClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), "/dev/sda").Return(int64(0), nil) + return mockDevices + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + controller := gomock.NewController(t) + + client := NewDetailed("", params.getCommandClient(controller), DefaultSelfHealingExclusion, nil, + params.getDeviceClient(controller), nil, nil, params.getReconcileUtils(controller), afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.RescanDevices(context.TODO(), params.targetIQN, params.lunID, params.minSize) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_rescanDisk(t *testing.T) { + type parameters struct { + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + } + const deviceName = "sda" + tests := map[string]parameters{ + "happy path": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName)) + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + "error opening rescan file": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "error writing to file": { + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringError: errors.New("some error"), + File: mem.NewFileHandle(&mem.FileData{}), + } + + memMapFs := afero.NewMemMapFs() + _, err := memMapFs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName)) + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + openResponse: f, + Fs: memMapFs, + } + + return fs + }, + assertError: assert.Error, + }, + "failed writing to file": { + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringCount: 0, + File: mem.NewFileHandle(&mem.FileData{}), + } + + memMapFs := afero.NewMemMapFs() + _, err := memMapFs.Create(fmt.Sprintf("/sys/block/%s/device/rescan", deviceName)) + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + openResponse: f, + Fs: memMapFs, + } + + return fs + }, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := NewDetailed("", nil, nil, nil, nil, nil, nil, nil, afero.Afero{Fs: params.getFileSystemUtils()}) + err := client.rescanDisk(context.TODO(), deviceName) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_reloadMultipathDevice(t *testing.T) { + type parameters struct { + multipathDeviceName string + getCommand func(controller *gomock.Controller) tridentexec.Command + assertError assert.ErrorAssertionFunc + } + + const multipathDeviceName = "dm-0" + const moultipathDevicePath = "/dev/" + multipathDeviceName + + tests := map[string]parameters{ + "no multipath device provided": { + multipathDeviceName: "", + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + assertError: assert.Error, + }, + "error executing multipath map reload": { + multipathDeviceName: multipathDeviceName, + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + moultipathDevicePath).Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "happy path": { + multipathDeviceName: multipathDeviceName, + getCommand: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "multipath", 10*time.Second, true, "-r", + moultipathDevicePath).Return(nil, nil) + return mockCommand + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", params.getCommand(ctrl), nil, nil, nil, nil, nil, nil, afero.Afero{}) + + err := client.reloadMultipathDevice(context.TODO(), params.multipathDeviceName) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_IsAlreadyAttached(t *testing.T) { + type parameters struct { + lunID int + targetIQN string + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + assertResponse assert.BoolAssertionFunc + } + + const targetIQN = "iqn.2010-01.com.netapp:target-1" + + tests := map[string]parameters{ + "no existing host sessions": { + lunID: 0, + targetIQN: targetIQN, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN).Return(nil) + return mockReconcileUtils + }, + assertResponse: assert.False, + }, + "error getting devices for LUN": { + lunID: 0, + targetIQN: targetIQN, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return(nil, errors.New("some error")) + return mockReconcileUtils + }, + assertResponse: assert.False, + }, + "no devices for LUN": { + lunID: 0, + targetIQN: targetIQN, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return(nil, nil) + return mockReconcileUtils + }, + assertResponse: assert.False, + }, + "happy path": { + lunID: 0, + targetIQN: targetIQN, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + targetIQN).Return(map[int]int{0: 0}) + mockReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{"/dev/sda"}) + mockReconcileUtils.EXPECT().GetDevicesForLUN([]string{"/dev/sda"}).Return([]string{"sda"}, nil) + return mockReconcileUtils + }, + assertResponse: assert.True, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + controller := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, nil, nil, nil, params.getIscsiUtils(controller), afero.Afero{}) + + attached := client.IsAlreadyAttached(context.TODO(), params.lunID, params.targetIQN) + if params.assertResponse != nil { + params.assertResponse(t, attached) + } + }) + } +} + +func TestClient_getDeviceInfoForLUN(t *testing.T) { + type parameters struct { + lunID int + iSCSINodeName string + needFS bool + isDetachCall bool + + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + getDevicesClient func(controller *gomock.Controller) Devices + getFileSystemUtils func() afero.Fs + + assertError assert.ErrorAssertionFunc + expectedDeviceInfo *ScsiDeviceInfo + } + + const iscisNodeName = "iqn.2010-01.com.netapp:target-1" + const multipathDeviceName = "dm-0" + const multipathDevicePath = "/dev/" + multipathDeviceName + const deviceName = "sda" + const devicePath = "/dev/" + deviceName + + tests := map[string]parameters{ + "no host session information present": { + lunID: 0, + iSCSINodeName: iscisNodeName, + isDetachCall: false, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscisNodeName).Return(nil) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + expectedDeviceInfo: nil, + }, + "no host session information present for detach call": { + lunID: 0, + iSCSINodeName: iscisNodeName, + isDetachCall: true, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscisNodeName).Return(nil) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + expectedDeviceInfo: nil, + }, + "error getting devices for LUN": { + lunID: 0, + iSCSINodeName: iscisNodeName, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return(nil, errors.New("some error")) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + expectedDeviceInfo: nil, + }, + "no devices for LUN": { + lunID: 0, + iSCSINodeName: iscisNodeName, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return(nil, nil) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + expectedDeviceInfo: nil, + }, + "no multipath device found": { + lunID: 0, + iSCSINodeName: iscisNodeName, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + expectedDeviceInfo: &ScsiDeviceInfo{ + LUN: "0", + Devices: []string{deviceName}, + DevicePaths: []string{devicePath}, + IQN: iscisNodeName, + HostSessionMap: map[int]int{0: 0}, + }, + }, + "error ensuring multipath device is readable": { + lunID: 0, + iSCSINodeName: iscisNodeName, + needFS: true, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiReconcileUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiReconcileUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + mockDeviceClient.EXPECT().EnsureDeviceReadable(context.TODO(), multipathDevicePath).Return(errors.New("some error")) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + err := fs.Mkdir(fmt.Sprintf("/sys/block/sda/holders/%s", multipathDeviceName), 777) + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + expectedDeviceInfo: nil, + }, + "error getting file system type from multipath device": { + lunID: 0, + iSCSINodeName: iscisNodeName, + needFS: true, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiReconcileUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiReconcileUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiReconcileUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiReconcileUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiReconcileUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + mockDeviceClient.EXPECT().EnsureDeviceReadable(context.TODO(), multipathDevicePath).Return(nil) + mockDeviceClient.EXPECT().GetDeviceFSType(context.TODO(), multipathDevicePath).Return("", errors.New("some error")) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + err := fs.Mkdir(fmt.Sprintf("/sys/block/sda/holders/%s", multipathDeviceName), 777) + assert.NoError(t, err) + return fs + }, + assertError: assert.Error, + expectedDeviceInfo: nil, + }, + "happy path": { + lunID: 0, + needFS: true, + iSCSINodeName: iscisNodeName, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), + iscisNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiUtils + }, + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDeviceClient := mock_iscsi.NewMockDevices(controller) + mockDeviceClient.EXPECT().EnsureDeviceReadable(context.TODO(), multipathDevicePath).Return(nil) + mockDeviceClient.EXPECT().GetDeviceFSType(context.TODO(), multipathDevicePath).Return(config.FsExt4, nil) + return mockDeviceClient + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + err := fs.Mkdir(fmt.Sprintf("/sys/block/sda/holders/%s", multipathDeviceName), 777) + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + expectedDeviceInfo: &ScsiDeviceInfo{ + LUN: "0", + Devices: []string{deviceName}, + DevicePaths: []string{devicePath}, + MultipathDevice: multipathDeviceName, + IQN: iscisNodeName, + HostSessionMap: map[int]int{0: 0}, + Filesystem: config.FsExt4, + }, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, params.getDevicesClient(ctrl), nil, nil, + params.getIscsiUtils(ctrl), + afero.Afero{ + Fs: params.getFileSystemUtils(), + }) + deviceInfo, err := client.getDeviceInfoForLUN(context.TODO(), params.lunID, params.iSCSINodeName, + params.needFS, + params.isDetachCall, + ) + if params.assertError != nil { + params.assertError(t, err) + } + assert.Equal(t, params.expectedDeviceInfo, deviceInfo) + }) + } +} + +func TestClient_purgeOneLun(t *testing.T) { + type parameters struct { + path string + getFileSystemUtils func() afero.Fs + + assertError assert.ErrorAssertionFunc + } + const devicePath = "/dev/sda" + tests := map[string]parameters{ + "error opening delete file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "error writing to file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringError: errors.New("some error"), + File: mem.NewFileHandle(&mem.FileData{}), + } + + memFs := afero.NewMemMapFs() + _, err := memFs.Create("/dev/sda/delete") + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + Fs: memFs, + } + + return fs + }, + }, + "unable to write to file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringCount: 0, + File: mem.NewFileHandle(&mem.FileData{}), + } + + memFs := afero.NewMemMapFs() + _, err := memFs.Create("/dev/sda/delete") + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + Fs: memFs, + } + + return fs + }, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := NewDetailed("", nil, nil, nil, nil, nil, nil, nil, afero.Afero{Fs: params.getFileSystemUtils()}) + err := client.purgeOneLun(context.TODO(), params.path) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_rescanOneLun(t *testing.T) { + type parameters struct { + path string + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + } + + const devicePath = "/dev/sda" + + tests := map[string]parameters{ + "error opening rescan file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + assertError: assert.Error, + }, + "error writing to rescan file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringError: errors.New("some error"), + File: mem.NewFileHandle(&mem.FileData{}), + } + + memFs := afero.NewMemMapFs() + _, err := memFs.Create(fmt.Sprintf("%s/rescan", devicePath)) + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + Fs: memFs, + } + + return fs + }, + }, + "failed to write to rescan file": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + f := &aferoFileWrapper{ + WriteStringCount: 0, + File: mem.NewFileHandle(&mem.FileData{}), + } + + memFs := afero.NewMemMapFs() + _, err := memFs.Create(fmt.Sprintf("%s/rescan", devicePath)) + assert.NoError(t, err) + + fs := &aferoWrapper{ + openFileResponse: f, + Fs: memFs, + } + + return fs + }, + }, + "happy path": { + path: devicePath, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create(fmt.Sprintf("%s/rescan", devicePath)) + assert.NoError(t, err) + return fs + }, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := NewDetailed("", nil, nil, nil, nil, nil, nil, nil, afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.rescanOneLun(context.TODO(), params.path) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_waitForMultipathDeviceForLUN(t *testing.T) { + type parameters struct { + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + } + + const lunID = 0 + const iscsiNodeName = "iqn.2010-01.com.netapp:target-1" + const deviceName = "sda" + const devicePath = "/dev/" + deviceName + const multipathDeviceName = "dm-0" + const holdersDirectory = "/sys/block/" + deviceName + "/holders/" + multipathDeviceName + + tests := map[string]parameters{ + "no host session mappings present": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(nil) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + "error getting devices for LUN": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return(nil, errors.New("some error")) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + "error getting multipath device": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + "happy path": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + mockIscsiUtils.EXPECT().GetDevicesForLUN([]string{devicePath}).Return([]string{deviceName}, nil) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create(holdersDirectory) + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, nil, nil, nil, params.getIscsiUtils(ctrl), afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.waitForMultipathDeviceForLUN(context.TODO(), lunID, iscsiNodeName) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_waitForDeviceScan(t *testing.T) { + type parameters struct { + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + getCommandClient func(controller *gomock.Controller) tridentexec.Command + getOsClient func(controller *gomock.Controller) OS + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + } + + const lunID = 0 + const iscsiNodeName = "iqn.2010-01.com.netapp:target-1" + const devicePath1 = "/dev/sda" + const devicePath2 = "/dev/sdb" + const devicePath3 = "/dev/sdc" + + tests := map[string]parameters{ + "no host session mappings present": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(nil) + return mockIscsiUtils + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getOsClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + "some devices present": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath1, devicePath2, devicePath3}) + return mockIscsiUtils + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getOsClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists(devicePath1+"/block").Return(true, nil) + mockOsClient.EXPECT().PathExists(devicePath2+"/block").Return(false, nil) + mockOsClient.EXPECT().PathExists(devicePath3+"/block").Return(false, errors.New("some error")) + return mockOsClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + }, + "all devices present": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath1, devicePath2}) + return mockIscsiUtils + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getOsClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + mockOsClient.EXPECT().PathExists(devicePath1+"/block").Return(true, nil) + mockOsClient.EXPECT().PathExists(devicePath2+"/block").Return(true, nil) + return mockOsClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + }, + "no devices present": { + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), iscsiNodeName).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return(nil) + return mockIscsiUtils + }, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "ls", gomock.Any()).Return(nil, + errors.New("some error")).Times(3) + mockCommand.EXPECT().Execute(context.TODO(), "lsscsi").Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "lsscsi", "-t").Return(nil, errors.New("some error")) + mockCommand.EXPECT().Execute(context.TODO(), "free").Return(nil, errors.New("some error")) + return mockCommand + }, + getOsClient: func(controller *gomock.Controller) OS { + mockOsClient := mock_iscsi.NewMockOS(controller) + return mockOsClient + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", params.getCommandClient(ctrl), nil, params.getOsClient(ctrl), nil, nil, nil, + params.getIscsiUtils(ctrl), + afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.waitForDeviceScan(context.TODO(), lunID, iscsiNodeName) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_scanTargetLUN(t *testing.T) { + type parameters struct { + assertError assert.ErrorAssertionFunc + getFileSystemUtils func() afero.Fs + } + + const lunID = 0 + const host1 = 1 + const host2 = 2 + + tests := map[string]parameters{ + "scan files not present": { + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + }, + "error writing to scan files": { + getFileSystemUtils: func() afero.Fs { + memFs := afero.NewMemMapFs() + _, err := memFs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host1)) + assert.NoError(t, err) + _, err = memFs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host2)) + assert.NoError(t, err) + + f := &aferoFileWrapper{ + WriteStringError: errors.New("some error"), + File: mem.NewFileHandle(&mem.FileData{}), + } + + fs := &aferoWrapper{ + openFileResponse: f, + openResponse: f, + Fs: memFs, + } + + return fs + }, + assertError: assert.Error, + }, + "failed to write to scan files": { + getFileSystemUtils: func() afero.Fs { + memFs := afero.NewMemMapFs() + _, err := memFs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host1)) + assert.NoError(t, err) + _, err = memFs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host2)) + assert.NoError(t, err) + + f := &aferoFileWrapper{ + WriteStringCount: 0, + File: mem.NewFileHandle(&mem.FileData{}), + } + + fs := &aferoWrapper{ + openFileResponse: f, + openResponse: f, + Fs: memFs, + } + + return fs + }, + assertError: assert.Error, + }, + "happy path": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + _, err := fs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host1)) + assert.NoError(t, err) + _, err = fs.Create(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", host2)) + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := NewDetailed("", nil, nil, nil, nil, nil, nil, nil, afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.scanTargetLUN(context.TODO(), lunID, []int{host1, host2}) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_handleInvalidSerials(t *testing.T) { + type parameters struct { + expectedSerial string + handlerError error + + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + getFileSystemUtils func() afero.Fs + + assertError assert.ErrorAssertionFunc + assertHandlerCalled assert.BoolAssertionFunc + } + + const lunID = 0 + const targetIQN = "iqn.2010-01.com.netapp:target-1" + const vpdpg80Serial = "SYA5GZFJ8G1M905GVH7H" + const devicePath = "/dev/sda" + + tests := map[string]parameters{ + "empty serial passed in": { + expectedSerial: "", + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + }, + "serial files do not exist": { + expectedSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + }, + "error reading serial files": { + expectedSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := &aferoWrapper{ + openError: errors.New("some error"), + Fs: afero.NewMemMapFs(), + } + return fs + }, + assertError: assert.Error, + }, + "lun serial does not match expected serial": { + expectedSerial: "SYA5GZFJ8G1M905GVH7I", + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(fmt.Sprintf("%s/vpd_pg80", devicePath)) + assert.NoError(t, err) + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + return fs + }, + assertHandlerCalled: assert.True, + handlerError: errors.New("some error"), + assertError: assert.Error, + }, + "happy path": { + expectedSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetISCSIHostSessionMapForTarget(context.TODO(), targetIQN).Return(map[int]int{0: 0}) + mockIscsiUtils.EXPECT().GetSysfsBlockDirsForLUN(0, gomock.Any()).Return([]string{devicePath}) + return mockIscsiUtils + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(fmt.Sprintf("%s/vpd_pg80", devicePath)) + assert.NoError(t, err) + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + return fs + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + handlerCalled := false + mockHandler := func(ctx context.Context, path string) error { + handlerCalled = true + return params.handlerError + } + + ctrl := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, nil, nil, nil, params.getIscsiUtils(ctrl), afero.Afero{Fs: params.getFileSystemUtils()}) + + err := client.handleInvalidSerials(context.TODO(), lunID, targetIQN, params.expectedSerial, mockHandler) + if params.assertError != nil { + params.assertError(t, err) + } + if params.assertHandlerCalled != nil { + params.assertHandlerCalled(t, handlerCalled) + } + }) + } +} + +func TestClient_getLunSerial(t *testing.T) { + type parameters struct { + getFileSystemUtils func() afero.Fs + expectedResponse string + assertError assert.ErrorAssertionFunc + } + + const devicePath = "/dev/sda" + const vpdpg80Serial = "SYA5GZFJ8G1M905GVH7H" + + tests := map[string]parameters{ + "error reading serial file": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + return fs + }, + expectedResponse: "", + assertError: assert.Error, + }, + "invalid serial in file len < 4 bytes": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(devicePath + "/vpd_pg80") + assert.NoError(t, err) + _, err = f.Write([]byte("123")) + assert.NoError(t, err) + return fs + }, + expectedResponse: "", + assertError: assert.Error, + }, + "invalid serial bytes[1] != 0x80": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(devicePath + "/vpd_pg80") + assert.NoError(t, err) + _, err = f.Write([]byte{0x81, 0x00, 0x00, 0x00, 0x00}) + assert.NoError(t, err) + return fs + }, + expectedResponse: "", + assertError: assert.Error, + }, + "invalid serial bad length": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(devicePath + "/vpd_pg80") + assert.NoError(t, err) + _, err = f.Write([]byte{0x81, 0x80, 0x01, 0x01, 0x02}) + assert.NoError(t, err) + return fs + }, + expectedResponse: "", + assertError: assert.Error, + }, + "happy path": { + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create(devicePath + "/vpd_pg80") + assert.NoError(t, err) + _, err = f.Write(vpdpg80SerialBytes(vpdpg80Serial)) + assert.NoError(t, err) + return fs + }, + expectedResponse: vpdpg80Serial, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + client := NewDetailed("", nil, nil, nil, nil, nil, nil, nil, afero.Afero{Fs: params.getFileSystemUtils()}) + response, err := client.getLunSerial(context.TODO(), devicePath) + if params.assertError != nil { + params.assertError(t, err) + } + assert.Equal(t, params.expectedResponse, response) + }) + } +} + +func TestClient_portalsToLogin(t *testing.T) { + type parameters struct { + getCommandClient func(controller *gomock.Controller) tridentexec.Command + getFileSystemUtils func() afero.Fs + portalsNeedingLogin []string + assertError assert.ErrorAssertionFunc + assertLoggedIn assert.BoolAssertionFunc + } + + const targetIQN = "iqn.2010-01.com.netapp:target-1" + const alternateTargetIQN = "iqn.2010-01.com.netapp:target-2" + const portal1 = "127.0.0.1" + const portal2 = "127.0.0.2" + const iscsiadmSessionOutput = `tcp: [3] 127.0.0.1:3260,1028 ` + targetIQN + ` (non-flash) +tcp: [4] 127.0.0.2:3260,1029 ` + targetIQN + ` (non-flash)` + const iscsiadmSessionOutputAlternateIQN = `tcp: [3] 127.0.0.1:3260,1028 ` + alternateTargetIQN + ` (non-flash) +tcp: [4] 127.0.0.2:3260,1029 ` + alternateTargetIQN + ` (non-flash)` + + tests := map[string]parameters{ + "error getting session info": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session").Return(nil, errors.New("some error")) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertLoggedIn: assert.False, + portalsNeedingLogin: []string{portal1, portal2}, + }, + "targetIQN does not match the session targetIQN": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session").Return([]byte(iscsiadmSessionOutputAlternateIQN), nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + assertLoggedIn: assert.False, + portalsNeedingLogin: []string{portal1, portal2}, + }, + "all sessions stale": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + fs := afero.NewMemMapFs() + f, err := fs.Create("/sys/class/iscsi_session/session3/state") + assert.NoError(t, err) + _, err = f.Write([]byte("foo")) + assert.NoError(t, err) + f, err = fs.Create("/sys/class/iscsi_session/session4/state") + assert.NoError(t, err) + _, err = f.Write([]byte("foo")) + assert.NoError(t, err) + return fs + }, + }, + "happy path": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + assertLoggedIn: assert.True, + portalsNeedingLogin: nil, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", params.getCommandClient(ctrl), nil, nil, nil, nil, nil, nil, + afero.Afero{Fs: params.getFileSystemUtils()}) + + portalsNeedingLogin, loggedIn, err := client.portalsToLogin(context.TODO(), targetIQN, []string{ + portal1, + portal2, + }) + if params.assertError != nil { + params.assertError(t, err) + } + + if params.assertLoggedIn != nil { + params.assertLoggedIn(t, loggedIn) + } + assert.Equal(t, params.portalsNeedingLogin, portalsNeedingLogin) + }) + } +} + +func TestClient_verifyMultipathDeviceSerial(t *testing.T) { + type parameters struct { + lunSerial string + getIscsiUtils func(controller *gomock.Controller) IscsiReconcileUtils + assertError assert.ErrorAssertionFunc + } + + const multipathDeviceName = "dm-0" + const vpdpg80Serial = "SYA5GZFJ8G1M905GVH7H" + lunSerialHex := hex.EncodeToString([]byte(vpdpg80Serial)) + multipathdeviceSerial := fmt.Sprintf("mpath-%s", lunSerialHex) + + tests := map[string]parameters{ + "empty lun serial": { + lunSerial: "", + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + return mockIscsiUtils + }, + assertError: assert.NoError, + }, + "multipath device not present": { + lunSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetMultipathDeviceUUID(multipathDeviceName).Return("", + errors.NotFoundError("not found")) + return mockIscsiUtils + }, + assertError: assert.NoError, + }, + "error getting multipath device UUID": { + lunSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetMultipathDeviceUUID(multipathDeviceName).Return("", + errors.New("some error")) + return mockIscsiUtils + }, + assertError: assert.Error, + }, + "LUN serial not present in multipath device UUID": { + lunSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetMultipathDeviceUUID(multipathDeviceName).Return("foo", nil) + return mockIscsiUtils + }, + assertError: assert.Error, + }, + "happy path": { + lunSerial: vpdpg80Serial, + getIscsiUtils: func(controller *gomock.Controller) IscsiReconcileUtils { + mockIscsiUtils := mock_iscsi.NewMockIscsiReconcileUtils(controller) + mockIscsiUtils.EXPECT().GetMultipathDeviceUUID(multipathDeviceName).Return(multipathdeviceSerial, nil) + return mockIscsiUtils + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, nil, nil, nil, params.getIscsiUtils(ctrl), afero.Afero{}) + err := client.verifyMultipathDeviceSerial(context.TODO(), multipathDeviceName, params.lunSerial) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_verifyMultipathDeviceSize(t *testing.T) { + type parameters struct { + getDevicesClient func(controller *gomock.Controller) Devices + assertError assert.ErrorAssertionFunc + assertValid assert.BoolAssertionFunc + expectedDeviceSize int64 + } + + const deviceName = "sda" + const multipathDeviceName = "dm-0" + + tests := map[string]parameters{ + "error getting device size": { + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(0), + errors.New("some error")) + return mockDevices + }, + assertError: assert.Error, + assertValid: assert.False, + expectedDeviceSize: 0, + }, + "error getting multipath device size": { + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(0), + errors.New("some error")) + return mockDevices + }, + assertError: assert.Error, + assertValid: assert.False, + expectedDeviceSize: 0, + }, + "device size != multipath device size": { + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(0), nil) + return mockDevices + }, + assertError: assert.NoError, + assertValid: assert.False, + expectedDeviceSize: 1, + }, + "happy path": { + getDevicesClient: func(controller *gomock.Controller) Devices { + mockDevices := mock_iscsi.NewMockDevices(controller) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(1), nil) + mockDevices.EXPECT().GetISCSIDiskSize(context.TODO(), gomock.Any()).Return(int64(1), nil) + return mockDevices + }, + assertError: assert.NoError, + assertValid: assert.True, + expectedDeviceSize: 0, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", nil, nil, nil, params.getDevicesClient(ctrl), nil, nil, nil, afero.Afero{}) + deviceSize, valid, err := client.verifyMultipathDeviceSize(context.TODO(), multipathDeviceName, deviceName) + if params.assertError != nil { + params.assertError(t, err) + } + if params.assertValid != nil { + params.assertValid(t, valid) + } + assert.Equal(t, params.expectedDeviceSize, deviceSize) + }) + } +} + +func TestClient_EnsureSessions(t *testing.T) { + type parameters struct { + publishInfo models.VolumePublishInfo + portals []string + getCommandClient func(controller *gomock.Controller) tridentexec.Command + getFileSystemUtils func() afero.Fs + assertError assert.ErrorAssertionFunc + assertResult assert.BoolAssertionFunc + } + + const portal1 = "127.0.0.1" + const portal2 = "127.0.0.2" + const targetIQN = "iqn.2010-01.com.netapp:target-1" + + const iscsiadmNodeOutput = portal1 + `:3260,1042 ` + targetIQN + ` +` + portal2 + `:3260,1043 ` + targetIQN + ` +` + const iscsiadmSessionOutput = `tcp: [3] 127.0.0.1:3260,1028 ` + targetIQN + ` (non-flash) +tcp: [4] 127.0.0.2:3260,1029 ` + targetIQN + ` (non-flash)` + + tests := map[string]parameters{ + "no portals provided to login": { + publishInfo: models.VolumePublishInfo{}, + portals: []string{}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "failed to ensure target": { + publishInfo: models.VolumePublishInfo{}, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, errors.New("some error")) + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "error setting node session replacement timeout": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.scan", "-v", + "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.timeo.replacement_timeout", + "-v", "5").Return(nil, errors.New("some error")) + + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "error logging into target": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.scan", "-v", + "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.timeo.replacement_timeout", + "-v", "5").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.conn[0].timeo.login_timeout", + "--value=10").Return(nil, errors.New("some error")) + + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "error verifying if the session exists": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.scan", "-v", + "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.timeo.replacement_timeout", + "-v", "5").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.conn[0].timeo.login_timeout", + "--value=10").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.session.initial_login_retry_max", + "--value=1").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", targetIQN, "-p", fmt.Sprintf("%s:3260", portal1), "--login").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return(nil, errors.New("some error")) + + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "session does not exist": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.scan", "-v", + "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.timeo.replacement_timeout", + "-v", "5").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.conn[0].timeo.login_timeout", + "--value=10").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.session.initial_login_retry_max", + "--value=1").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", targetIQN, "-p", fmt.Sprintf("%s:3260", portal1), "--login").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(""), nil) + + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.Error, + assertResult: assert.False, + }, + "happy path": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portals: []string{portal1}, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return([]byte(iscsiadmNodeOutput), nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.scan", "-v", + "manual").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "-o", "update", "-n", "node.session.timeo.replacement_timeout", + "-v", "5").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.conn[0].timeo.login_timeout", + "--value=10").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, "-p", + fmt.Sprintf("%s:3260", portal1), "--op=update", "--name", "node.session.initial_login_retry_max", + "--value=1").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", targetIQN, "-p", fmt.Sprintf("%s:3260", portal1), "--login").Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", + "session").Return([]byte(iscsiadmSessionOutput), nil) + + return mockCommand + }, + getFileSystemUtils: func() afero.Fs { + return afero.NewMemMapFs() + }, + assertError: assert.NoError, + assertResult: assert.True, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", params.getCommandClient(ctrl), nil, nil, nil, nil, nil, nil, + afero.Afero{Fs: params.getFileSystemUtils()}) + successfullyLoggedIn, err := client.EnsureSessions(context.TODO(), ¶ms.publishInfo, params.portals) + if params.assertError != nil { + params.assertError(t, err) + } + if params.assertResult != nil { + params.assertResult(t, successfullyLoggedIn) + } + }) + } +} + +func TestClient_LoginTarget(t *testing.T) { + type parameters struct { + publishInfo models.VolumePublishInfo + portal string + + getCommandClient func(controller *gomock.Controller) tridentexec.Command + assertError assert.ErrorAssertionFunc + } + + const targetIQN = "iqn.2010-01.com.netapp:target-1" + const portal = "127.0.0.1" + + tests := map[string]parameters{ + "error setting login timeout": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portal: portal, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.conn[0].timeo.login_timeout", "--value=10"). + Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error setting login max retry": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portal: portal, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.conn[0].timeo.login_timeout", "--value=10"). + Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.session.initial_login_retry_max", "--value=1"). + Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "error logging in": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portal: portal, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.conn[0].timeo.login_timeout", "--value=10"). + Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.session.initial_login_retry_max", "--value=1"). + Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", + targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--login"). + Return(nil, errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "happy path": { + publishInfo: models.VolumePublishInfo{ + VolumeAccessInfo: models.VolumeAccessInfo{ + IscsiAccessInfo: models.IscsiAccessInfo{ + IscsiTargetIQN: targetIQN, + }, + }, + }, + portal: portal, + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.conn[0].timeo.login_timeout", "--value=10"). + Return(nil, nil) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node", "-T", targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--op=update", "--name", + "node.session.initial_login_retry_max", "--value=1"). + Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "node", "-T", + targetIQN, + "-p", fmt.Sprintf("%s:3260", portal), "--login"). + Return(nil, nil) + return mockCommand + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + iscsiClient := NewDetailed("", params.getCommandClient(ctrl), nil, nil, nil, nil, nil, nil, + afero.Afero{Fs: afero.NewMemMapFs()}) + + err := iscsiClient.LoginTarget(context.TODO(), ¶ms.publishInfo, params.portal) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } } +func TestClient_ensureTarget(t *testing.T) { + type parameters struct { + username string + password string + targetUsername string + targetInitiatorSecret string + getCommandClient func(controller *gomock.Controller) tridentexec.Command + assertError assert.ErrorAssertionFunc + } + + const targetPortal = "127.0.0.1:3260" + const portal2 = "127.0.0.2:3260" + const targetIQN = "iqn.2010-01.com.netapp:target-1" + const networkInterface = "default" + const iscsiadmNodeOutput = targetPortal + `,1042 ` + targetIQN + ` +` + portal2 + `,1043 ` + targetIQN + ` +` + const iscsiadmDiscoveryDBSendTargetsOutput = targetPortal + `,1042 ` + targetIQN + ` +` + portal2 + `,1043 ` + targetIQN + ` ` -func TestMultipathdIsRunning(t *testing.T) { + tests := map[string]parameters{ + "error getting targets": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, + errors.New("some error")) + return mockCommand + }, + assertError: assert.Error, + }, + "already logged in": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node"). + Return([]byte(iscsiadmNodeOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + "failure to discover any targets": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-D").Return(nil, nil) + return mockCommand + }, + assertError: assert.Error, + }, + "error setting auth method to chap": { + username: "username", + password: "password", + targetUsername: "targetUsername", + targetInitiatorSecret: "targetInitiatorSecret", + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "new").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.authmethod", "-v", "CHAP").Return(nil, errors.New("some error")) + return mockCommand + }, + }, + "error setting auth username for chap": { + username: "username", + password: "password", + targetUsername: "targetUsername", + targetInitiatorSecret: "targetInitiatorSecret", + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "new").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.authmethod", "-v", "CHAP").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username", "-v", "username").Return(nil, errors.New("some error")) + return mockCommand + }, + }, + "error setting auth password for chap": { + username: "username", + password: "password", + targetUsername: "targetUsername", + targetInitiatorSecret: "targetInitiatorSecret", + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "new").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.authmethod", "-v", "CHAP").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username", "-v", "username").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.password", "-v", "password").Return(nil, errors.New("some error")) + return mockCommand + }, + }, + "error setting target auth username for chap": { + username: "username", + password: "password", + targetUsername: "targetUsername", + targetInitiatorSecret: "targetInitiatorSecret", + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "new").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.authmethod", "-v", "CHAP").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username", "-v", "username").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.password", "-v", "password").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username_in", "-v", "targetUsername").Return(nil, errors.New("some error")) + return mockCommand + }, + }, + "error setting target auth initiatior secret for chap": { + username: "username", + password: "password", + targetUsername: "targetUsername", + targetInitiatorSecret: "targetInitiatorSecret", + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "new").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.authmethod", "-v", "CHAP").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username", "-v", "username").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.password", "-v", "password").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.username_in", "-v", "targetUsername").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-o", "update", "-n", + "discovery.sendtargets.auth.password_in", "-v", "targetInitiatorSecret").Return(nil, errors.New("some error")) + return mockCommand + }, + }, + "error getting portal discovery information": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, + "-m", "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-D"). + Return(nil, nil) + return mockCommand + }, + assertError: assert.Error, + }, + "happy path": { + getCommandClient: func(controller *gomock.Controller) tridentexec.Command { + mockCommand := mockexec.NewMockCommand(controller) + mockCommand.EXPECT().Execute(context.TODO(), "iscsiadm", "-m", "node").Return(nil, nil) + mockCommand.EXPECT().ExecuteWithTimeout(context.TODO(), "iscsiadm", iscsiadmLoginTimeout, true, "-m", + "discoverydb", "-t", "st", "-p", targetPortal, "-I", networkInterface, "-D").Return([]byte(iscsiadmDiscoveryDBSendTargetsOutput), nil) + return mockCommand + }, + assertError: assert.NoError, + }, + } + + for name, params := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + client := NewDetailed("", params.getCommandClient(ctrl), nil, nil, nil, nil, nil, nil, afero.Afero{}) + err := client.ensureTarget(context.TODO(), targetPortal, targetIQN, params.username, + params.password, params.targetUsername, params.targetInitiatorSecret, networkInterface) + if params.assertError != nil { + params.assertError(t, err) + } + }) + } +} + +func TestClient_multipathdIsRunning(t *testing.T) { mockCtrl := gomock.NewController(t) mockExec := mockexec.NewMockCommand(mockCtrl) tests := []struct { @@ -52,7 +4705,7 @@ func TestMultipathdIsRunning(t *testing.T) { ctx, gomock.Any(), gomock.Any(), gomock.Any(), ).Return([]byte(tt.execOut), tt.execErr) } - iscsiClient := NewDetailed("", mockExec, nil, nil, nil, nil, nil, nil) + iscsiClient := NewDetailed("", mockExec, nil, nil, nil, nil, nil, nil, afero.Afero{}) actualValue := iscsiClient.multipathdIsRunning(context.Background()) assert.Equal(t, tt.expectedValue, actualValue) @@ -176,12 +4829,11 @@ func TestFormatPortal(t *testing.T) { } func TestPidRunningOrIdleRegex(t *testing.T) { - Log().Debug("Running TestPidRegexes...") - - tests := map[string]struct { + type parameters struct { input string expectedOutput bool - }{ + } + tests := map[string]parameters{ // Negative tests "Negative input #1": { input: "", @@ -206,79 +4858,97 @@ func TestPidRunningOrIdleRegex(t *testing.T) { expectedOutput: true, }, } - for testName, test := range tests { - t.Run(testName, func(t *testing.T) { - result := pidRunningOrIdleRegex.MatchString(test.input) - assert.True(t, test.expectedOutput == result) + for name, params := range tests { + t.Run(name, func(t *testing.T) { + result := pidRunningOrIdleRegex.MatchString(params.input) + assert.Equal(t, params.expectedOutput, result) }) } } func TestGetFindMultipathValue(t *testing.T) { - Log().Debug("Running TestGetFindMultipathValue...") - - findMultipathsValue := GetFindMultipathValue(multipathConf) - assert.Equal(t, "no", findMultipathsValue) - - inputStringCopy := strings.ReplaceAll(multipathConf, "find_multipaths", "#find_multipaths") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "find_multipaths no", "") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "yes") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "yes", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "'yes'") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "yes", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "'on'") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "yes", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "'off'") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "no", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "on") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "yes", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "off") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "no", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "random") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "random", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "smart") - - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "smart", findMultipathsValue) - - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "greedy") + type parameters struct { + findMultipathsValue string + findMultipathsLineCommented bool + expectedFindMultipathsValue string + } - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "greedy", findMultipathsValue) + tests := map[string]parameters{ + "find_multipaths no": { + findMultipathsValue: "no", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "no", + }, + "find_multipaths line commented": { + findMultipathsValue: "no", + findMultipathsLineCommented: true, + expectedFindMultipathsValue: "", + }, + "empty find_multipaths": { + findMultipathsValue: "", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "", + }, + "find_multipaths yes": { + findMultipathsValue: "yes", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "yes", + }, + "find_multipaths 'yes'": { + findMultipathsValue: "'yes'", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "yes", + }, + "find_multipaths 'on'": { + findMultipathsValue: "'on'", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "yes", + }, + "find_multipaths 'off'": { + findMultipathsValue: "'off'", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "no", + }, + "find_multipaths on": { + findMultipathsValue: "on", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "yes", + }, + "find_multipaths off": { + findMultipathsValue: "off", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "no", + }, + "find_multipaths random": { + findMultipathsValue: "random", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "random", + }, + "find_multipaths smart": { + findMultipathsValue: "smart", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "smart", + }, + "find_multipaths greedy": { + findMultipathsValue: "greedy", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "greedy", + }, + "find_multipaths 'no'": { + findMultipathsValue: "'no'", + findMultipathsLineCommented: false, + expectedFindMultipathsValue: "no", + }, + } - inputStringCopy = strings.ReplaceAll(multipathConf, "no", "'no'") + for name, params := range tests { + t.Run(name, func(t *testing.T) { + mpathConfig := multipathConfig(params.findMultipathsValue, params.findMultipathsLineCommented) - findMultipathsValue = GetFindMultipathValue(inputStringCopy) - assert.Equal(t, "no", findMultipathsValue) + findMultipathsValue := getFindMultipathValue(mpathConfig) + assert.Equal(t, params.expectedFindMultipathsValue, findMultipathsValue) + }) + } } func TestEnsureHostportFormatted(t *testing.T) { @@ -327,3 +4997,54 @@ func TestEnsureHostportFormatted(t *testing.T) { }) } } + +// ---- helpers +func multipathConfig(findMultipathsValue string, ValueCommented bool) string { + const multipathConf = ` +defaults { + user_friendly_names yes +%s} +` + commentPrefix := "" + if ValueCommented { + commentPrefix = "#" + } + + findMultipathLine := "" + if findMultipathsValue != "" { + findMultipathLine = fmt.Sprintf(" %sfind_multipaths %s\n", commentPrefix, findMultipathsValue) + } + + cfg := fmt.Sprintf(multipathConf, findMultipathLine) + return cfg +} + +func vpdpg80SerialBytes(serial string) []byte { + return append([]byte{0, 128, 0, 20}, []byte(serial)...) +} + +type aferoWrapper struct { + openFileError error + openFileResponse afero.File + openResponse afero.File + openError error + afero.Fs +} + +func (a *aferoWrapper) OpenFile(_ string, _ int, _ os.FileMode) (afero.File, error) { + return a.openFileResponse, a.openFileError +} + +func (a *aferoWrapper) Open(_ string) (afero.File, error) { + return a.openResponse, a.openError +} + +type aferoFileWrapper struct { + WriteStringError error + WriteStringCount int + afero.File +} + +func (a *aferoFileWrapper) WriteString(_ string) (ret int, err error) { + return a.WriteStringCount, a.WriteStringError +} diff --git a/utils/iscsi/reconcile_utils.go b/utils/iscsi/reconcile_utils.go index 42cacdc51..f61dfd27c 100644 --- a/utils/iscsi/reconcile_utils.go +++ b/utils/iscsi/reconcile_utils.go @@ -2,7 +2,7 @@ package iscsi -//go:generate mockgen -destination=../../mocks/mock_utils/mock_reconcile_utils/reconcile_utils.go github.com/netapp/trident/utils/iscsi IscsiReconcileUtils +//go:generate mockgen -destination=../../mocks/mock_utils/mock_iscsi/mock_reconcile_utils.go github.com/netapp/trident/utils/iscsi IscsiReconcileUtils import ( "context"