diff --git a/fakes/fake_executor.go b/fakes/fake_executor.go index 931a575c..968b662d 100644 --- a/fakes/fake_executor.go +++ b/fakes/fake_executor.go @@ -36,6 +36,21 @@ type FakeExecutor struct { result1 []byte result2 error } + ExecuteInteractiveStub func(string, []string, []string) ([]byte, error) + executeInteractiveMutex sync.RWMutex + executeInteractiveArgsForCall []struct { + arg1 string + arg2 []string + arg3 []string + } + executeInteractiveReturns struct { + result1 []byte + result2 error + } + executeInteractiveReturnsOnCall map[int]struct { + result1 []byte + result2 error + } ExecuteWithTimeoutStub func(int, string, []string) ([]byte, error) executeWithTimeoutMutex sync.RWMutex executeWithTimeoutArgsForCall []struct { @@ -376,6 +391,81 @@ func (fake *FakeExecutor) ExecuteReturnsOnCall(i int, result1 []byte, result2 er }{result1, result2} } +func (fake *FakeExecutor) ExecuteInteractive(arg1 string, arg2 []string, arg3 []string) ([]byte, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + var arg3Copy []string + if arg3 != nil { + arg3Copy = make([]string, len(arg3)) + copy(arg3Copy, arg3) + } + fake.executeInteractiveMutex.Lock() + ret, specificReturn := fake.executeInteractiveReturnsOnCall[len(fake.executeInteractiveArgsForCall)] + fake.executeInteractiveArgsForCall = append(fake.executeInteractiveArgsForCall, struct { + arg1 string + arg2 []string + arg3 []string + }{arg1, arg2Copy, arg3Copy}) + fake.recordInvocation("ExecuteInteractive", []interface{}{arg1, arg2Copy, arg3Copy}) + fake.executeInteractiveMutex.Unlock() + if fake.ExecuteInteractiveStub != nil { + return fake.ExecuteInteractiveStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.executeInteractiveReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeExecutor) ExecuteInteractiveCallCount() int { + fake.executeInteractiveMutex.RLock() + defer fake.executeInteractiveMutex.RUnlock() + return len(fake.executeInteractiveArgsForCall) +} + +func (fake *FakeExecutor) ExecuteInteractiveCalls(stub func(string, []string, []string) ([]byte, error)) { + fake.executeInteractiveMutex.Lock() + defer fake.executeInteractiveMutex.Unlock() + fake.ExecuteInteractiveStub = stub +} + +func (fake *FakeExecutor) ExecuteInteractiveArgsForCall(i int) (string, []string, []string) { + fake.executeInteractiveMutex.RLock() + defer fake.executeInteractiveMutex.RUnlock() + argsForCall := fake.executeInteractiveArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeExecutor) ExecuteInteractiveReturns(result1 []byte, result2 error) { + fake.executeInteractiveMutex.Lock() + defer fake.executeInteractiveMutex.Unlock() + fake.ExecuteInteractiveStub = nil + fake.executeInteractiveReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeExecutor) ExecuteInteractiveReturnsOnCall(i int, result1 []byte, result2 error) { + fake.executeInteractiveMutex.Lock() + defer fake.executeInteractiveMutex.Unlock() + fake.ExecuteInteractiveStub = nil + if fake.executeInteractiveReturnsOnCall == nil { + fake.executeInteractiveReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.executeInteractiveReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + func (fake *FakeExecutor) ExecuteWithTimeout(arg1 int, arg2 string, arg3 []string) ([]byte, error) { var arg3Copy []string if arg3 != nil { @@ -1424,6 +1514,8 @@ func (fake *FakeExecutor) Invocations() map[string][][]interface{} { defer fake.evalSymlinksMutex.RUnlock() fake.executeMutex.RLock() defer fake.executeMutex.RUnlock() + fake.executeInteractiveMutex.RLock() + defer fake.executeInteractiveMutex.RUnlock() fake.executeWithTimeoutMutex.RLock() defer fake.executeWithTimeoutMutex.RUnlock() fake.getDeviceForFileStatMutex.RLock() diff --git a/utils/executor.go b/utils/executor.go index 9319d0ce..8a8ea571 100644 --- a/utils/executor.go +++ b/utils/executor.go @@ -17,6 +17,7 @@ package utils import ( + "bufio" "bytes" "context" "fmt" @@ -24,8 +25,9 @@ import ( "os" "os/exec" "path/filepath" - "time" + "strings" "syscall" + "time" "github.com/IBM/ubiquity/utils/logs" ) @@ -33,6 +35,7 @@ import ( //go:generate counterfeiter -o ../fakes/fake_executor.go . Executor type Executor interface { // basic host dependent functions Execute(command string, args []string) ([]byte, error) + ExecuteInteractive(command string, args []string, interactiveCmds []string) ([]byte, error) Stat(string) (os.FileInfo, error) Mkdir(string, os.FileMode) error MkdirAll(string, os.FileMode) error @@ -51,7 +54,6 @@ type Executor interface { // basic host dependent functions IsSameFile(file1 os.FileInfo, file2 os.FileInfo) bool IsDirEmpty(dir string) (bool, error) GetDeviceForFileStat(os.FileInfo) uint64 - } type executor struct { @@ -84,6 +86,35 @@ func (e *executor) Execute(command string, args []string) ([]byte, error) { return stdOut, err } +// non-interactive mode of interactive command(s) executor +// It will enter the interactive mode, run given command(s) and exit immediately. +// interactiveCmds is a set of commands run in interactive mode, an exit command is mandatory. +// For example: [command1, exit], [command1, command2, q] +func (e *executor) ExecuteInteractive(command string, args []string, interactiveCmds []string) ([]byte, error) { + var stdout, stderr bytes.Buffer + interactiveCmd := strings.Join(interactiveCmds, "\n") + "\n" + outWriter := bufio.NewWriter(&stdout) + errWriter := bufio.NewWriter(&stderr) + cmdStdin := strings.NewReader(interactiveCmd) + + execInteractive := exec.Command(command, args...) + execInteractive.Stdout = outWriter + execInteractive.Stdin = cmdStdin + execInteractive.Stderr = errWriter + + err := execInteractive.Run() + if err != nil { + return nil, err + } + + errOutput := strings.TrimSpace(stderr.String()) + if errOutput != "" { + return nil, fmt.Errorf(errOutput) + } + + return stdout.Bytes(), nil +} + func (e *executor) ExecuteWithTimeout(mSeconds int, command string, args []string) ([]byte, error) { // Create a new context and add a timeout to it @@ -184,6 +215,6 @@ func (e *executor) IsDirEmpty(dir string) (bool, error) { return len(files) == 0, nil } -func (e *executor) GetDeviceForFileStat(fileStat os.FileInfo) uint64{ +func (e *executor) GetDeviceForFileStat(fileStat os.FileInfo) uint64 { return fileStat.Sys().(*syscall.Stat_t).Dev } diff --git a/utils/mpath.go b/utils/mpath.go index 4bf78c23..ac76a5e7 100644 --- a/utils/mpath.go +++ b/utils/mpath.go @@ -2,6 +2,7 @@ package utils import ( "bufio" + "encoding/json" "fmt" "regexp" "strings" @@ -53,7 +54,7 @@ func GetMultipathOutputAndDeviceMapperAndDevice(volumeWwn string, exec Executor) if err != nil { return []byte{}, "", []string{}, err } - bodyPattern := "[0-9]+:[0-9]+:[0-9]+:[0-9]+ " + bodyPattern := "(?:[0-9]+:[0-9]+:[0-9]+:[0-9]+ )[\\s\\S]+" bodyRegex, err := regexp.Compile(bodyPattern) if err != nil { return []byte{}, "", []string{}, err @@ -74,11 +75,9 @@ func GetMultipathOutputAndDeviceMapperAndDevice(volumeWwn string, exec Executor) skipped := false for scanner.Scan() { text := scanner.Text() - text = strings.TrimSpace(text) if bodyRegex.MatchString(text) { - index := bodyRegex.FindStringIndex(text) - trimedText := text[index[0]:] - deviceName := strings.Fields(trimedText)[1] + res := bodyRegex.FindString(text) + deviceName := strings.Fields(res)[1] deviceNames = append(deviceNames, deviceName) } else if !skipped { skipped = true @@ -112,3 +111,60 @@ func ExcludeNoTargetPortGroupMessagesFromMultipathOutput(mpathOutput string, log regex, _ := regexp.Compile(WarningNoTargetPortGroup) return excludeWarningMessageLines(mpathOutput, regex, logger) } + +// GetMultipathNameUuidpair returns all the multipath devices in the following format: +// ["mpatha,360050768029b8168e000000000006247", "mpathb,360050768029b8168e000000000006247", ...] +func GetMultipathNameUuidpair(exec Executor) ([]string, error) { + cmd := `show maps raw format "%n,%w"` + output, err := Multipathd(cmd, exec) + if err != nil { + return []string{}, err + } else { + pairs := strings.Split(output, "\n") + return pairs, nil + } +} + +func GetMultipathOutputAll(exec Executor) (*MultipathOutputAll, error) { + cmd := "list maps json" + output, err := Multipathd(cmd, exec) + if err != nil { + return nil, err + } else { + var mpathAll MultipathOutputAll + err := json.Unmarshal([]byte(output), &mpathAll) + if err != nil { + return nil, err + } + return &mpathAll, nil + } +} + +func GetMultipathOutput(name string, exec Executor) (*MultipathOutput, error) { + cmd := fmt.Sprintf("list map %s json", name) + output, err := Multipathd(cmd, exec) + if err != nil { + return nil, err + } else { + var mpath MultipathOutput + err := json.Unmarshal([]byte(output), &mpath) + if err != nil { + return nil, err + } + return &mpath, nil + } +} + +// Multipathd is a non-interactive mode of "multipathd -k" +// It will enter the interactive mode, run given command and exit immediately. +func Multipathd(cmd string, exec Executor) (string, error) { + + output, err := exec.ExecuteInteractive("multipathd", []string{"-k"}, []string{cmd, "exit"}) + if err != nil { + return "", err + } + outputString := strings.TrimSpace(string(output)) + outputString = strings.SplitN(outputString, "\n", 2)[1] + outputString = strings.TrimSpace(outputString) + return strings.Split(outputString, "\nmultipathd>")[0], nil +} diff --git a/utils/mpath_test.go b/utils/mpath_test.go index 0572ef3b..650cb403 100644 --- a/utils/mpath_test.go +++ b/utils/mpath_test.go @@ -92,6 +92,112 @@ size=20G features='1 queue_if_no_path' hwhandler='0' wp=rw '-+- policy='service-time 0' prio=10 status=enabled '- 39:0:0:0 sdc 8:32 active ready running` +var fakeMultipathOutputAllJson = ` +multipathd> list maps json +{ + "major_version": 0, + "minor_version": 1, + "maps": [{ + "name" : "mpathp", + "uuid" : "360050768029b8168e000000000006247", + "sysfs" : "dm-3", + "failback" : "immediate", + "queueing" : "5 chk", + "paths" : 0, + "write_prot" : "rw", + "dm_st" : "active", + "features" : "0", + "hwhandler" : "0", + "action" : "create", + "path_faults" : 1, + "vend" : "IBM ", + "prod" : "2145 ", + "rev" : "0000", + "switch_grp" : 0, + "map_loads" : 1, + "total_q_time" : 26, + "q_timeouts" : 1, + "path_groups": [{ + "selector" : "round-robin 0", + "pri" : 0, + "dm_st" : "active", + "group" : 1, + "paths": [{ + "dev" : "sdb", + "dev_t" : "8:16", + "dm_st" : "failed", + "dev_st" : "running", + "chk_st" : "faulty", + "checker" : "tur", + "pri" : 50, + "host_wwnn" : "[undef]", + "target_wwnn" : "iqn.1986-03.com.ibm:2145.v7k60.node1", + "host_wwpn" : "[undef]", + "target_wwpn" : "[undef]", + "host_adapter" : "9.115.240.253" + }] + }] + }] +} +multipathd> exit +` + +var fakeMultipathOutputJson = ` +multipathd> list map mpathp json +{ + "major_version": 0, + "minor_version": 1, + "map": { + "name" : "mpathp", + "uuid" : "360050768029b8168e000000000006247", + "sysfs" : "dm-3", + "failback" : "immediate", + "queueing" : "5 chk", + "paths" : 0, + "write_prot" : "rw", + "dm_st" : "active", + "features" : "0", + "hwhandler" : "0", + "action" : "create", + "path_faults" : 1, + "vend" : "IBM ", + "prod" : "2145 ", + "rev" : "0000", + "switch_grp" : 0, + "map_loads" : 1, + "total_q_time" : 26, + "q_timeouts" : 1, + "path_groups": [{ + "selector" : "round-robin 0", + "pri" : 0, + "dm_st" : "active", + "group" : 1, + "paths": [{ + "dev" : "sdb", + "dev_t" : "8:16", + "dm_st" : "failed", + "dev_st" : "running", + "chk_st" : "faulty", + "checker" : "tur", + "pri" : 50, + "host_wwnn" : "[undef]", + "target_wwnn" : "iqn.1986-03.com.ibm:2145.v7k60.node1", + "host_wwpn" : "[undef]", + "target_wwpn" : "[undef]", + "host_adapter" : "9.115.240.253" + }] + }] + } +} +multipathd> exit +` + +var fakeMultipathNameUuidpair = ` +multipathd> list maps json +mpathp,360050768029b8168e000000000006247 +multipathd> exit +` + var _ = Describe("multipath_utils_test", func() { var ( fakeExec *fakes.FakeExecutor @@ -144,4 +250,47 @@ var _ = Describe("multipath_utils_test", func() { Expect(out).To(Equal(fakeMultipathOutputWithWarningsExcluded)) }) }) + + Context("GetMultipathOutputAll", func() { + + It("should get correct json response and unmarshal to MultipathOutputAll", func() { + fakeExec.ExecuteInteractiveReturns([]byte(fakeMultipathOutputAllJson), nil) + out, err := utils.GetMultipathOutputAll(fakeExec) + Ω(err).ShouldNot(HaveOccurred()) + Expect(out.Maps).To(HaveLen(1)) + mpath := out.Maps[0] + Expect(mpath.PathGroups).To(HaveLen(1)) + pg := mpath.PathGroups[0] + Expect(pg.Paths).To(HaveLen(1)) + path := pg.Paths[0] + Expect(path.Dev).To(Equal("sdb")) + }) + }) + + Context("GetMultipathOutput", func() { + + It("should get correct json response and unmarshal to MultipathOutput", func() { + fakeExec.ExecuteInteractiveReturns([]byte(fakeMultipathOutputJson), nil) + out, err := utils.GetMultipathOutput("mpathp", fakeExec) + Ω(err).ShouldNot(HaveOccurred()) + Expect(out.Map).NotTo(BeNil()) + mpath := out.Map + Expect(mpath.PathGroups).To(HaveLen(1)) + pg := mpath.PathGroups[0] + Expect(pg.Paths).To(HaveLen(1)) + path := pg.Paths[0] + Expect(path.Dev).To(Equal("sdb")) + }) + }) + + Context("GetMultipathNameUuidpair", func() { + + It("should return correct name uuid pair", func() { + fakeExec.ExecuteInteractiveReturns([]byte(fakeMultipathNameUuidpair), nil) + pairs, err := utils.GetMultipathNameUuidpair(fakeExec) + Ω(err).ShouldNot(HaveOccurred()) + Expect(pairs).To(HaveLen(1)) + Expect(pairs[0]).To(Equal("mpathp,360050768029b8168e000000000006247")) + }) + }) }) diff --git a/utils/resources.go b/utils/resources.go new file mode 100644 index 00000000..e9691391 --- /dev/null +++ b/utils/resources.go @@ -0,0 +1,84 @@ +package utils + +/* +Example multipath output: +{ + "major_version": 0, + "minor_version": 1, + "maps": [{ + "name" : "mpathp", + "uuid" : "360050768029b8168e000000000006247", + "sysfs" : "dm-3", + "failback" : "immediate", + "queueing" : "5 chk", + "paths" : 0, + "write_prot" : "rw", + "dm_st" : "active", + "features" : "0", + "hwhandler" : "0", + "action" : "create", + "path_faults" : 1, + "vend" : "IBM ", + "prod" : "2145 ", + "rev" : "0000", + "switch_grp" : 0, + "map_loads" : 1, + "total_q_time" : 26, + "q_timeouts" : 1, + "path_groups": [{ + "selector" : "round-robin 0", + "pri" : 0, + "dm_st" : "active", + "group" : 1, + "paths": [{ + "dev" : "sdb", + "dev_t" : "8:16", + "dm_st" : "failed", + "dev_st" : "running", + "chk_st" : "faulty", + "checker" : "tur", + "pri" : 50, + "host_wwnn" : "[undef]", + "target_wwnn" : "iqn.1986-03.com.ibm:2145.v7k60.node1", + "host_wwpn" : "[undef]", + "target_wwpn" : "[undef]", + "host_adapter" : "9.115.240.253" + }] + }] + }] +} +*/ + +type MultipathOutputAll struct { + Maps []*MultipathDevice `json:"maps"` +} + +type MultipathOutput struct { + Map *MultipathDevice `json:"map"` +} + +type MultipathDevice struct { + Name string `json:"name"` + Uuid string `json:"uuid"` + Sysfs string `json:"sysfs"` + PathFaults int `json:"path_faults"` + Vend string `json:"vend"` + Prod string `json:"prod"` + PathGroups []*MultipathDevicePathGroup `json:"path_groups"` +} + +type MultipathDevicePathGroup struct { + Selector string `json:"selector"` + Pri int `json:"pri"` + DmSt string `json:"dm_st"` + Group int `json:"group"` + Paths []*MultipathDevicePath `json:"paths"` +} + +type MultipathDevicePath struct { + Dev string `json:"dev"` + DevT string `json:"dev_t"` + DmSt string `json:"dm_st"` + DevSt string `json:"dev_st"` + ChkSt string `json:"chk_st"` +}