diff --git a/Makefile b/Makefile index 26cf14c..9e3fde2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build clean install +.PHONY: all build clean install test coverage all: clean build install @@ -16,3 +16,6 @@ install: test: go test ./iscsi/ +coverage: + go test ./iscsi -coverprofile=coverage.out + go tool cover -html=coverage.out diff --git a/go.mod b/go.mod index d5c5cdf..c358400 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/kubernetes-csi/csi-lib-iscsi go 1.15 -require github.com/prashantv/gostub v1.0.0 +require ( + github.com/prashantv/gostub v1.0.0 + github.com/stretchr/testify v1.7.0 +) diff --git a/go.sum b/go.sum index 324091f..5bb4efb 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.0.0 h1:wTzvgO04xSS3gHuz6Vhuo0/kvWelyJxwNS0IRBPAwGY= github.com/prashantv/gostub v1.0.0/go.mod h1:dP1v6T1QzyGJJKFocwAU0lSZKpfjstjH8TlhkEU0on0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/iscsi/iscsi.go b/iscsi/iscsi.go index e5f38d5..c688a29 100644 --- a/iscsi/iscsi.go +++ b/iscsi/iscsi.go @@ -27,6 +27,7 @@ var ( osStat = os.Stat filepathGlob = filepath.Glob osOpenFile = os.OpenFile + sleep = time.Sleep ) // iscsiSession contains information avout an iSCSI session @@ -162,14 +163,14 @@ func getCurrentSessions() ([]iscsiSession, error) { // waitForPathToExist wait for a file at a path to exists on disk func waitForPathToExist(devicePath *string, maxRetries, intervalSeconds uint, deviceTransport string) error { - if devicePath == nil { + if devicePath == nil || *devicePath == "" { return fmt.Errorf("unable to check unspecified devicePath") } for i := uint(0); i <= maxRetries; i++ { if i != 0 { debug.Printf("Device path %q doesn't exists yet, retrying in %d seconds (%d/%d)", *devicePath, intervalSeconds, i, maxRetries) - time.Sleep(time.Second * time.Duration(intervalSeconds)) + sleep(time.Second * time.Duration(intervalSeconds)) } if err := pathExists(devicePath, deviceTransport); err == nil { @@ -195,7 +196,11 @@ func pathExists(devicePath *string, deviceTransport string) error { return err } } else { - fpath, _ := filepathGlob(*devicePath) + fpath, err := filepathGlob(*devicePath) + + if err != nil { + return err + } if fpath == nil { return os.ErrNotExist } @@ -210,22 +215,17 @@ func pathExists(devicePath *string, deviceTransport string) error { // getMultipathDevice returns a multipath device for the configured targets if it exists func getMultipathDevice(devices []Device) (*Device, error) { - var deviceInfo deviceInfo var multipathDevice *Device var devicePaths []string for _, device := range devices { devicePaths = append(devicePaths, device.GetPath()) } - out, err := lsblk("-J", devicePaths) + deviceInfo, err := lsblk("", devicePaths) if err != nil { return nil, err } - if err = json.Unmarshal(out, &deviceInfo); err != nil { - return nil, err - } - for _, device := range deviceInfo.BlockDevices { if len(device.Children) != 1 { return nil, fmt.Errorf("device is not mapped to exactly one multipath device: %v", device.Children) @@ -454,18 +454,12 @@ func (c *Connector) IsMultipathEnabled() bool { func GetSCSIDevices(devicePaths []string) ([]Device, error) { debug.Printf("Getting info about SCSI devices %s.\n", devicePaths) - out, err := lsblk("-JS", devicePaths) + deviceInfo, err := lsblk("-S", devicePaths) if err != nil { debug.Printf("An error occured while looking info about SCSI devices: %v", err) return nil, err } - var deviceInfo deviceInfo - err = json.Unmarshal(out, &deviceInfo) - if err != nil { - return nil, err - } - return deviceInfo.BlockDevices, nil } @@ -488,7 +482,7 @@ func GetISCSIDevices(devicePaths []string) (devices []Device, err error) { } // lsblk execute the lsblk commands -func lsblk(flags string, devicePaths []string) ([]byte, error) { +func lsblkRaw(flags string, devicePaths []string) ([]byte, error) { out, err := execCommand("lsblk", append([]string{flags}, devicePaths...)...).CombinedOutput() debug.Printf("lsblk %s %s", flags, strings.Join(devicePaths, " ")) if err != nil { @@ -498,6 +492,21 @@ func lsblk(flags string, devicePaths []string) ([]byte, error) { return out, nil } +func lsblk(flags string, devicePaths []string) (*deviceInfo, error) { + var deviceInfo deviceInfo + + out, err := lsblkRaw("-J "+flags, devicePaths) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(out, &deviceInfo); err != nil { + return nil, err + } + + return &deviceInfo, nil +} + // writeInSCSIDeviceFile write into special devices files to change devices state func writeInSCSIDeviceFile(hctl string, file string, content string) error { filename := filepath.Join("/sys/class/scsi_device", hctl, "device", file) @@ -614,15 +623,15 @@ func (d *Device) WriteDeviceFile(name string, content string) error { // Shutdown turn off an SCSI device by writing offline\n in /sys/class/scsi_device/h:c:t:l/device/state func (d *Device) Shutdown() error { - return writeInSCSIDeviceFile(d.Hctl, "state", "offline\n") + return d.WriteDeviceFile("state", "offline\n") } // Delete detach an SCSI device by writing 1 in /sys/class/scsi_device/h:c:t:l/device/delete func (d *Device) Delete() error { - return writeInSCSIDeviceFile(d.Hctl, "delete", "1") + return d.WriteDeviceFile("delete", "1") } // Rescan rescan an SCSI device by writing 1 in /sys/class/scsi_device/h:c:t:l/device/rescan func (d *Device) Rescan() error { - return writeInSCSIDeviceFile(d.Hctl, "rescan", "1") + return d.WriteDeviceFile("rescan", "1") } diff --git a/iscsi/iscsi_test.go b/iscsi/iscsi_test.go index 61521f3..be117d5 100644 --- a/iscsi/iscsi_test.go +++ b/iscsi/iscsi_test.go @@ -2,6 +2,7 @@ package iscsi import ( "context" + "encoding/json" "fmt" "io/ioutil" "os" @@ -9,12 +10,23 @@ import ( "path/filepath" "reflect" "strconv" + "strings" "testing" "time" "github.com/prashantv/gostub" + "github.com/stretchr/testify/assert" ) +type testWriter struct { + data *[]byte +} + +func (w testWriter) Write(data []byte) (n int, err error) { + *w.data = append(*w.data, data...) + return len(data), nil +} + const nodeDB = ` # BEGIN RECORD 6.2.0.874 node.name = iqn.2010-10.org.openstack:volume-eb393993-73d0-4e39-9ef4-b5841e244ced @@ -99,6 +111,12 @@ func makeFakeExecCommand(exitStatus int, stdout string) func(string, ...string) } } +func makeFakeExecCommandContext(exitStatus int, stdout string) func(context.Context, string, ...string) *exec.Cmd { + return func(ctx context.Context, command string, args ...string) *exec.Cmd { + return makeFakeExecCommand(exitStatus, stdout)(command, args...) + } +} + func makeFakeExecWithTimeout(testCmdTimeout bool, testExecWithTimeoutError error) func(string, []string, time.Duration) ([]byte, error) { return func(command string, args []string, timeout time.Duration) ([]byte, error) { if testCmdTimeout { @@ -374,3 +392,260 @@ func Test_DisconnectMultipathVolume(t *testing.T) { }) } } + +func Test_EnableDebugLogging(t *testing.T) { + assert := assert.New(t) + data := []byte{} + writer := testWriter{data: &data} + EnableDebugLogging(writer) + + assert.Equal("", string(data)) + assert.Len(strings.Split(string(data), "\n"), 1) + + debug.Print("testing debug logs") + assert.Contains(string(data), "testing debug logs") + assert.Len(strings.Split(string(data), "\n"), 2) +} + +func Test_waitForPathToExist(t *testing.T) { + tests := map[string]struct { + attempts int + fileNotFound bool + withErr bool + transport string + }{ + "Basic": { + attempts: 1, + }, + "WithRetry": { + attempts: 2, + }, + "WithRetryFail": { + attempts: 3, + fileNotFound: true, + }, + "WithError": { + withErr: true, + }, + } + + for name, tt := range tests { + tt.transport = "tcp" + tests[name+"OverTCP"] = tt + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + attempts := 0 + maxRetries := tt.attempts - 1 + if tt.fileNotFound { + maxRetries-- + } + if maxRetries < 0 { + maxRetries = 0 + } + doAttempt := func(err error) error { + attempts++ + if tt.withErr { + return err + } + if attempts < tt.attempts { + return os.ErrNotExist + } + return nil + } + defer gostub.Stub(&osStat, func(name string) (os.FileInfo, error) { + if err := doAttempt(os.ErrPermission); err != nil { + return nil, err + } + return nil, nil + }).Reset() + defer gostub.Stub(&filepathGlob, func(name string) ([]string, error) { + if err := doAttempt(filepath.ErrBadPattern); err != nil { + return nil, err + } + return []string{"/somefilewithalongname"}, nil + }).Reset() + defer gostub.Stub(&sleep, func(_ time.Duration) {}).Reset() + path := "/somefile" + err := waitForPathToExist(&path, uint(maxRetries), 1, tt.transport) + + if tt.withErr { + if tt.transport == "tcp" { + assert.Equal(os.ErrPermission, err) + } else { + assert.Equal(filepath.ErrBadPattern, err) + } + return + } + if tt.fileNotFound { + assert.Equal(os.ErrNotExist, err) + assert.Equal(maxRetries, attempts-1) + } else { + assert.Nil(err) + assert.Equal(tt.attempts, attempts) + if tt.transport == "tcp" { + assert.Equal("/somefile", path) + } else { + assert.Equal("/somefilewithalongname", path) + } + } + }) + } + + t.Run("PathEmptyOrNil", func(t *testing.T) { + assert := assert.New(t) + path := "" + + err := waitForPathToExist(&path, 0, 0, "tcp") + assert.NotNil(err) + + err = waitForPathToExist(&path, 0, 0, "") + assert.NotNil(err) + + err = waitForPathToExist(nil, 0, 0, "tcp") + assert.NotNil(err) + + err = waitForPathToExist(nil, 0, 0, "") + assert.NotNil(err) + }) + + t.Run("PathNotFound", func(t *testing.T) { + assert := assert.New(t) + defer gostub.Stub(&filepathGlob, func(name string) ([]string, error) { + return nil, nil + }).Reset() + + path := "/test" + err := waitForPathToExist(&path, 0, 0, "") + assert.NotNil(err) + assert.Equal(os.ErrNotExist, err) + }) +} + +func Test_getMultipathDevice(t *testing.T) { + mpath1 := Device{Name: "3600c0ff0000000000000000000000000", Type: "mpath"} + mpath2 := Device{Name: "3600c0ff1111111111111111111111111", Type: "mpath"} + sda := Device{Name: "sda", Children: []Device{{Name: "sda1"}}} + sdb := Device{Name: "sdb", Children: []Device{mpath1}} + sdc := Device{Name: "sdc", Children: []Device{mpath1}} + sdd := Device{Name: "sdc", Children: []Device{mpath2}} + sde := Device{Name: "sdc", Children: []Device{mpath1, mpath2}} + + tests := map[string]struct { + mockedDevices deviceInfo + mockedStdout string + mockedExitStatus int + multipathDevice *Device + wantErr bool + }{ + "Basic": { + mockedDevices: deviceInfo{BlockDevices: []Device{sdb, sdc}}, + multipathDevice: &mpath1, + }, + "NotABlockDevice": { + mockedStdout: "lsblk: sdzz: not a block device", + mockedExitStatus: 32, + }, + "NotSharingTheSameMultipathDevice": { + mockedDevices: deviceInfo{BlockDevices: []Device{sdb, sdd}}, + wantErr: true, + }, + "MoreThanOneMultipathDevice": { + mockedDevices: deviceInfo{BlockDevices: []Device{sde}}, + wantErr: true, + }, + "NotAMultipathDevice": { + mockedDevices: deviceInfo{BlockDevices: []Device{sda}}, + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + mockedStdout := tt.mockedStdout + if mockedStdout == "" { + out, err := json.Marshal(tt.mockedDevices) + assert.Nil(err, "could not setup test") + mockedStdout = string(out) + } + + gostub.Stub(&execCommand, makeFakeExecCommand(tt.mockedExitStatus, string(mockedStdout))) + multipathDevice, err := getMultipathDevice(tt.mockedDevices.BlockDevices) + + if tt.mockedExitStatus != 0 || tt.wantErr { + assert.Nil(multipathDevice) + assert.NotNil(err) + } else { + assert.Equal(tt.multipathDevice, multipathDevice) + assert.Nil(err) + } + }) + } +} + +func TestConnectorPersistance(t *testing.T) { + assert := assert.New(t) + + secret := Secrets{ + SecretsType: "fake secret type", + UserName: "fake username", + Password: "fake password", + UserNameIn: "fake username in", + PasswordIn: "fake password in", + } + childDevice := Device{ + Name: "child name", + Hctl: "child hctl", + Type: "child type", + Vendor: "child vendor", + Model: "child model", + Revision: "child revision", + Transport: "child transport", + } + device := Device{ + Name: "device name", + Hctl: "device hctl", + Children: []Device{childDevice}, + Type: "device type", + Vendor: "device vendor", + Model: "device model", + Revision: "device revision", + Transport: "device transport", + } + c := Connector{ + VolumeName: "fake volume name", + Targets: []TargetInfo{}, + Lun: 42, + AuthType: "fake auth type", + DiscoverySecrets: secret, + SessionSecrets: secret, + Interface: "fake interface", + MountTargetDevice: &device, + Devices: []Device{device, childDevice}, + RetryCount: 24, + CheckInterval: 13, + DoDiscovery: true, + DoCHAPDiscovery: true, + } + + c.Persist("/tmp/connector.json") + c2, err := GetConnectorFromFile("/tmp/connector.json") + assert.Nil(err) + assert.Equal(c, *c2) + + err = c.Persist("/tmp") + assert.NotNil(err) + + os.Remove("/tmp/shouldNotExists.json") + _, err = GetConnectorFromFile("/tmp/shouldNotExists.json") + assert.NotNil(err) + assert.IsType(&os.PathError{}, err) + + ioutil.WriteFile("/tmp/connector.json", []byte("not a connector"), 0600) + _, err = GetConnectorFromFile("/tmp/connector.json") + assert.NotNil(err) + assert.IsType(&json.SyntaxError{}, err) +} diff --git a/iscsi/iscsiadm.go b/iscsi/iscsiadm.go index f6f096d..a9fd6cf 100644 --- a/iscsi/iscsiadm.go +++ b/iscsi/iscsiadm.go @@ -20,20 +20,6 @@ type Secrets struct { PasswordIn string `json:"passwordIn,omitempty"` } -// CmdError is a custom error to provide details including the command, stderr output and exit code. -// iscsiadm in some cases requires all of this info to determine success or failure -type CmdError struct { - CMD string - StdErr string - ExitCode int -} - -func (e *CmdError) Error() string { - // we don't output the command in the error string to avoid leaking any security info - // the command is still available in the error structure if the caller wants it though - return fmt.Sprintf("iscsiadm returned an error: %s, exit-code: %d", e.StdErr, e.ExitCode) -} - func iscsiCmd(args ...string) (string, error) { cmd := execCommand("iscsiadm", args...) debug.Printf("Run iscsiadm command: %s", strings.Join(append([]string{"iscsiadm"}, args...), " ")) @@ -45,18 +31,12 @@ func iscsiCmd(args ...string) (string, error) { // we're using Start and Wait because we want to grab exit codes err := cmd.Start() + if err == nil { + err = cmd.Wait() + } if err != nil { - // This is usually a cmd not found so we'll set our own error here formattedOutput := strings.Replace(string(stdout.Bytes()), "\n", "", -1) iscsiadmError = fmt.Errorf("iscsiadm error: %s (%s)", formattedOutput, err.Error()) - - } else { - err = cmd.Wait() - if err != nil { - formattedOutput := strings.Replace(string(stdout.Bytes()), "\n", "", -1) - iscsiadmError = fmt.Errorf("iscsiadm error: %s (%s)", formattedOutput, err.Error()) - - } } iscsiadmDebug(string(stdout.Bytes()), iscsiadmError) diff --git a/iscsi/iscsiadm_test.go b/iscsi/iscsiadm_test.go index 9b92495..a281bf5 100644 --- a/iscsi/iscsiadm_test.go +++ b/iscsi/iscsiadm_test.go @@ -4,8 +4,68 @@ import ( "testing" "github.com/prashantv/gostub" + "github.com/stretchr/testify/assert" ) +const defaultInterface = ` +# BEGIN RECORD 2.0-874 +iface.iscsi_ifacename = default +iface.net_ifacename = +iface.ipaddress = +iface.hwaddress = +iface.transport_name = tcp +iface.initiatorname = +iface.state = +iface.vlan_id = 0 +iface.vlan_priority = 0 +iface.vlan_state = +iface.iface_num = 0 +iface.mtu = 0 +iface.port = 0 +iface.bootproto = +iface.subnet_mask = +iface.gateway = +iface.dhcp_alt_client_id_state = +iface.dhcp_alt_client_id = +iface.dhcp_dns = +iface.dhcp_learn_iqn = +iface.dhcp_req_vendor_id_state = +iface.dhcp_vendor_id_state = +iface.dhcp_vendor_id = +iface.dhcp_slp_da = +iface.fragmentation = +iface.gratuitous_arp = +iface.incoming_forwarding = +iface.tos_state = +iface.tos = 0 +iface.ttl = 0 +iface.delayed_ack = +iface.tcp_nagle = +iface.tcp_wsf_state = +iface.tcp_wsf = 0 +iface.tcp_timer_scale = 0 +iface.tcp_timestamp = +iface.redirect = +iface.def_task_mgmt_timeout = 0 +iface.header_digest = +iface.data_digest = +iface.immediate_data = +iface.initial_r2t = +iface.data_seq_inorder = +iface.data_pdu_inorder = +iface.erl = 0 +iface.max_receive_data_len = 0 +iface.first_burst_len = 0 +iface.max_outstanding_r2t = 0 +iface.max_burst_len = 0 +iface.chap_auth = +iface.bidi_chap = +iface.strict_login_compliance = +iface.discovery_auth = +iface.discovery_logout = +# END RECORD +` + func TestDiscovery(t *testing.T) { tests := map[string]struct { tgtPortal string @@ -126,3 +186,89 @@ func TestCreateDBEntry(t *testing.T) { } } + +func TestListInterfaces(t *testing.T) { + tests := map[string]struct { + mockedStdout string + mockedExitStatus int + interfaces []string + wantErr bool + }{ + "EmptyOutput": { + mockedStdout: "", + mockedExitStatus: 0, + interfaces: []string{""}, + wantErr: false, + }, + "DefaultInterface": { + mockedStdout: "default", + mockedExitStatus: 0, + interfaces: []string{"default"}, + wantErr: false, + }, + "TwoInterface": { + mockedStdout: "default\ntest", + mockedExitStatus: 0, + interfaces: []string{"default", "test"}, + wantErr: false, + }, + "HasError": { + mockedStdout: "", + mockedExitStatus: 1, + interfaces: []string{}, + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + defer gostub.Stub(&execCommand, makeFakeExecCommand(tt.mockedExitStatus, tt.mockedStdout)).Reset() + interfaces, err := ListInterfaces() + + if tt.wantErr { + assert.NotNil(err) + } else { + assert.Nil(err) + assert.Equal(interfaces, tt.interfaces) + } + }) + } +} + +func TestShowInterface(t *testing.T) { + tests := map[string]struct { + mockedStdout string + mockedExitStatus int + iFace string + wantErr bool + }{ + "DefaultInterface": { + mockedStdout: defaultInterface, + mockedExitStatus: 0, + iFace: defaultInterface, + wantErr: false, + }, + "HasError": { + mockedStdout: "", + mockedExitStatus: 1, + iFace: "", + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + defer gostub.Stub(&execCommand, makeFakeExecCommand(tt.mockedExitStatus, tt.mockedStdout)).Reset() + interfaces, err := ShowInterface("default") + + if tt.wantErr { + assert.NotNil(err) + } else { + assert.Nil(err) + assert.Equal(interfaces, tt.iFace) + } + }) + } +} diff --git a/iscsi/multipath.go b/iscsi/multipath.go index a7a86c9..347ae33 100644 --- a/iscsi/multipath.go +++ b/iscsi/multipath.go @@ -2,7 +2,6 @@ package iscsi import ( "context" - "encoding/json" "fmt" "os" "os/exec" @@ -10,17 +9,6 @@ import ( "time" ) -type multipathDeviceMap struct { - Map multipathMap `json:"map"` -} - -type multipathMap struct { - Name string `json:"name"` - UUID string `json:"uuid"` - Sysfs string `json:"sysfs"` - PathGroups []pathGroup `json:"path_groups"` -} - type pathGroup struct { Paths []path `json:"paths"` } @@ -42,6 +30,7 @@ func ExecWithTimeout(command string, args []string, timeout time.Duration) ([]by // This time we can simply use Output() to get the result. out, err := cmd.Output() + debug.Println(err) // We want to check the context error to see if the timeout was executed. // The error returned by cmd.Output() will be OS specific based on what @@ -62,37 +51,6 @@ func ExecWithTimeout(command string, args []string, timeout time.Duration) ([]by return out, err } -func getMultipathMap(device string) (*multipathDeviceMap, error) { - debug.Printf("Getting multipath map for device %s.\n", device) - - cmd := execCommand("multipathd", "show", "map", device[1:], "json") - out, err := cmd.CombinedOutput() - // debug.Printf(string(out)) - if err != nil { - debug.Printf("An error occured while looking for multipath device map: %s\n", out) - return nil, err - } - - var deviceMap multipathDeviceMap - err = json.Unmarshal(out, &deviceMap) - if err != nil { - return nil, err - } - return &deviceMap, nil -} - -func (deviceMap *multipathDeviceMap) GetSlaves() []string { - var slaves []string - - for _, pathGroup := range deviceMap.Map.PathGroups { - for _, path := range pathGroup.Paths { - slaves = append(slaves, path.Device) - } - } - - return slaves -} - // FlushMultipathDevice flushes a multipath device dm-x with command multipath -f /dev/dm-x func FlushMultipathDevice(device *Device) error { devicePath := device.GetPath() diff --git a/iscsi/multipath_test.go b/iscsi/multipath_test.go new file mode 100644 index 0000000..abf5249 --- /dev/null +++ b/iscsi/multipath_test.go @@ -0,0 +1,79 @@ +package iscsi + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/prashantv/gostub" + "github.com/stretchr/testify/assert" +) + +func TestExecWithTimeout(t *testing.T) { + tests := map[string]struct { + mockedStdout string + mockedExitStatus int + wantTimeout bool + }{ + "Success": { + mockedStdout: "some output", + mockedExitStatus: 0, + wantTimeout: false, + }, + "WithError": { + mockedStdout: "some\noutput", + mockedExitStatus: 1, + wantTimeout: false, + }, + "WithTimeout": { + mockedStdout: "", + mockedExitStatus: 0, + wantTimeout: true, + }, + "WithTimeoutAndOutput": { + mockedStdout: "should not be returned", + mockedExitStatus: 0, + wantTimeout: true, + }, + "WithTimeoutAndError": { + mockedStdout: "", + mockedExitStatus: 1, + wantTimeout: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + timeout := time.Second + if tt.wantTimeout { + timeout = time.Millisecond * 50 + } + + defer gostub.Stub(&execCommandContext, func(ctx context.Context, command string, args ...string) *exec.Cmd { + if tt.wantTimeout { + time.Sleep(timeout + time.Millisecond*10) + } + return makeFakeExecCommandContext(tt.mockedExitStatus, tt.mockedStdout)(ctx, command, args...) + }).Reset() + + out, err := ExecWithTimeout("dummy", []string{}, timeout) + + if tt.wantTimeout || tt.mockedExitStatus != 0 { + assert.NotNil(err) + if tt.wantTimeout { + assert.Equal(context.DeadlineExceeded, err) + } + } else { + assert.Nil(err) + } + + if tt.wantTimeout { + assert.Equal("", string(out)) + } else { + assert.Equal(tt.mockedStdout, string(out)) + } + }) + } +}