diff --git a/linux/raidcontroller.go b/linux/raidcontroller.go index c26fe3c..476abdf 100644 --- a/linux/raidcontroller.go +++ b/linux/raidcontroller.go @@ -6,6 +6,7 @@ type RAIDControllerType string const ( MegaRAIDControllerType RAIDControllerType = "megaraid" + SmartPqiControllerType RAIDControllerType = "smartpqi" ) type RAIDController interface { diff --git a/smartpqi/arcconf.go b/smartpqi/arcconf.go index a2c0ef0..8ba33c2 100644 --- a/smartpqi/arcconf.go +++ b/smartpqi/arcconf.go @@ -16,17 +16,11 @@ const ( noArcConfRC = 127 ) -func ParsePhysicalDeviceString(rawData string) (PhysicalDevice, error) { - pd := PhysicalDevice{} - - return pd, nil -} - func parseLogicalDevices(rawData string) ([]LogicalDevice, error) { logDevs := []LogicalDevice{} - lines := strings.Split(rawData, "\n\n\n") - for _, device := range lines { + devices := strings.Split(rawData, "\n\n\n") + for _, device := range devices { ldStart := strings.Index(device, "Logical Device number") if ldStart >= 0 { ldRawData := strings.TrimSpace(device[ldStart:]) @@ -66,12 +60,23 @@ func parseLogicalDeviceString(rawData string) (LogicalDevice, error) { } ld := LogicalDevice{} + if len(rawData) < 1 { + return ld, fmt.Errorf("cannot parse an empty string") + } + // break data into first line which has the logical device number and // the remaining data is in mostly key : value pairs lineData := strings.SplitN(rawData, "\n", 2) + if len(lineData) < 2 { + return ld, fmt.Errorf("expected more than 2 lines of data, found %d", len(lineData)) + } // extract the LogicalDrive ID from the first token + // Logical Device number N toks := strings.Split(strings.TrimSpace(lineData[0]), " ") + if len(toks) != 4 { + return ld, fmt.Errorf("expected 4 tokens in %q, found %d", lineData[0], len(toks)) + } ldID, err := strconv.Atoi(toks[len(toks)-1]) if err != nil { @@ -123,7 +128,7 @@ func parseLogicalDeviceString(rawData string) (LogicalDevice, error) { case toks[0] == "Array": // 2] aID, err := strconv.Atoi(toks[1]) if err != nil { - return ld, fmt.Errorf("failed to parse ArrayID from token '%s': %s", toks[1], err) + return ld, fmt.Errorf("failed to parse ArrayID from token '%s': %s", toks[1], err) } ld.ArrayID = aID @@ -135,7 +140,7 @@ func parseLogicalDeviceString(rawData string) (LogicalDevice, error) { case toks[0] == "Size": // 1144609 MB] sizeMB, err := strconv.Atoi(strings.Fields(toks[1])[0]) if err != nil { - return ld, fmt.Errorf("failed to parse Size from token '%s': %s", toks[1], err) + return ld, fmt.Errorf("failed to parse Size from token '%s': %s", toks[1], err) } ld.SizeMB = sizeMB @@ -172,7 +177,14 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { devStartIdx := []int{} devEndIdx := []int{} - for idx, devIdx := range deviceStartRe.FindAllIndex([]byte(output), -1) { + devStart := deviceStartRe.FindAllIndex([]byte(output), -1) + if len(devStart) < 1 { + return []PhysicalDevice{}, fmt.Errorf("error finding start of PhysicalDevice in data") + } + + // construct pairs of start and stop points in the string marking the + // beginning and end of a single PhysicalDevice entry + for idx, devIdx := range devStart { devStartIdx = append(devStartIdx, devIdx[0]) if idx > 0 { @@ -180,7 +192,6 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { devEndIdx = append(devEndIdx, devIdx[0]-1) } } - devEndIdx = append(devEndIdx, len(output)) deviceRaw := []string{} @@ -191,6 +202,10 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { for _, deviceStr := range deviceRaw { deviceLines := strings.SplitN(deviceStr, "\n", 2) + if len(deviceLines) < 2 { + return []PhysicalDevice{}, fmt.Errorf("expected more than 2 lines of data, found %d", len(deviceRaw)) + } + toks := strings.Split(deviceLines[0], "#") pdID, err := strconv.Atoi(toks[1]) @@ -230,7 +245,7 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { bSize, err := strconv.Atoi(dToks[0]) if err != nil { - return []PhysicalDevice{}, fmt.Errorf("failed to parse block size from %q: %s", dToks[0], err) + return []PhysicalDevice{}, fmt.Errorf("failed to parse Block Size from token %q: %s", dToks[0], err) } pd.BlockSize = bSize @@ -239,14 +254,14 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { bSize, err := strconv.Atoi(dToks[0]) if err != nil { - return []PhysicalDevice{}, fmt.Errorf("failed to parse block size from %q: %s", dToks[0], err) + return []PhysicalDevice{}, fmt.Errorf("failed to parse Physical Block Size from token %q: %s", dToks[0], err) } pd.PhysicalBlockSize = bSize case toks[0] == "Array": aID, err := strconv.Atoi(toks[1]) if err != nil { - return []PhysicalDevice{}, fmt.Errorf("failed to parse Array ID from %q: %s", toks[1], err) + return []PhysicalDevice{}, fmt.Errorf("failed to parse Array from token %q: %s", toks[1], err) } pd.ArrayID = aID @@ -271,7 +286,7 @@ func parsePhysicalDevices(output string) ([]PhysicalDevice, error) { bSize, err := strconv.Atoi(dToks[0]) if err != nil { - return []PhysicalDevice{}, fmt.Errorf("failed to parse total size from %q: %s", dToks[0], err) + return []PhysicalDevice{}, fmt.Errorf("failed to parse Total Size from token %q: %s", dToks[0], err) } pd.SizeMB = bSize @@ -293,7 +308,15 @@ func parseGetConf(output string) ([]LogicalDevice, []PhysicalDevice, error) { logDevs := []LogicalDevice{} phyDevs := []PhysicalDevice{} + if len(output) < 1 { + return logDevs, phyDevs, fmt.Errorf("cannot parse an empty string") + } + lines := strings.Split(output, "\n\n\n") + if len(lines) < 3 { + return logDevs, phyDevs, fmt.Errorf("expected more than 3 lines of data in input") + } + for _, device := range lines { ldStart := strings.Index(device, "Logical Device number") if ldStart >= 0 { @@ -482,7 +505,7 @@ func newController(cID int, arcGetConfigOut string) (Controller, error) { lDevs, pDevs, err := parseGetConf(arcGetConfigOut) if err != nil { - return Controller{}, fmt.Errorf("failed to parse acrconf getconfig output: %s", err) + return Controller{}, fmt.Errorf("failed to parse arcconf getconfig output: %s", err) } // PD.ID -> PhysicalDevice @@ -494,14 +517,15 @@ func newController(cID int, arcGetConfigOut string) (Controller, error) { pDev := pDevs[idx] ctrl.PhysicalDrives[pDev.ID] = &pDev - for lIdx := range lDevs { - lDev := lDevs[lIdx] - ctrl.LogicalDrives[lDev.ID] = &lDev + } + for lIdx := range lDevs { + lDev := lDevs[lIdx] + ctrl.LogicalDrives[lDev.ID] = &lDev + for idx := range pDevs { + pDev := pDevs[idx] if lDev.ArrayID == pDev.ArrayID { lDev.Devices = append(lDev.Devices, &pDev) - - break } } } diff --git a/smartpqi/arcconf_test.go b/smartpqi/arcconf_test.go index d180558..aeaaed6 100644 --- a/smartpqi/arcconf_test.go +++ b/smartpqi/arcconf_test.go @@ -3,6 +3,7 @@ package smartpqi import ( "encoding/json" "reflect" + "strings" "testing" ) @@ -725,7 +726,7 @@ Logical Device number 0 Size : 572293 MB Stripe-unit size : 128 KB Full Stripe Size : 128 KB - Interface Type : Serial Attached SCSI + Interface Type : Serial Attached ATA Device Type : Data Boot Type : None Heads : 255 @@ -838,6 +839,18 @@ Controller information Command completed successfully. ` +var arcListBad = ` +Controllers found: 3 +---------------------------------------------------------------------- +Controller information +---------------------------------------------------------------------- + Controller ID : Status, Slot, Mode, Name, SerialNumber, WWN +---------------------------------------------------------------------- + Controller 1: : Optimal, Slot 16, Mixed, Cisco 24G TriMode M1 RAID 4GB FBWC 16D UCSC-RAID-M1L16, 3137F30003A, 50000D1E01787B00 + +Command completed successfully. +` + func TestSmartPqiList(t *testing.T) { found, err := parseList(arcList) if err != nil { @@ -856,10 +869,22 @@ func TestSmartPqiList(t *testing.T) { } } +func TestSmartPqiListBad(t *testing.T) { + _, err := parseList(arcListBad) + if err == nil { + t.Errorf("expected error on bad input") + } + + expected := "mismatched output, found 1 controllers, expected 3" + if !strings.Contains(err.Error(), expected) { + t.Errorf("expected '%s' message, got %q", expected, err) + } +} + func TestSmartPqiLogicaDevice(t *testing.T) { expectedLDs := []LogicalDevice{} expectedLDsJSON := []string{ - `{"ArrayID":0,"BlockSize":512,"Devices":null,"DiskName":"/dev/sdd","ID":0,"InterfaceType":"SCSI","Name":"Logical Drive 1","RAIDLevel":"0","SizeMB":572293}`, + `{"ArrayID":0,"BlockSize":512,"Devices":null,"DiskName":"/dev/sdd","ID":0,"InterfaceType":"ATA","Name":"Logical Drive 1","RAIDLevel":"0","SizeMB":572293}`, `{"ArrayID":1,"BlockSize":512,"Devices":null,"DiskName":"/dev/sde","ID":1,"InterfaceType":"SCSI","Name":"Logical Drive 2","RAIDLevel":"0","SizeMB":1144609}`, `{"ArrayID":2,"BlockSize":512,"Devices":null,"DiskName":"/dev/sdc","ID":2,"InterfaceType":"SCSI","Name":"Logical Drive 3","RAIDLevel":"0","SizeMB":1144609}`, } @@ -891,6 +916,106 @@ func TestSmartPqiLogicaDevice(t *testing.T) { } } +func TestSmartPqiLogicaDeviceBadInput(t *testing.T) { + var input = `Logical Device number 0` + found, err := parseLogicalDevices(input) + if err == nil { + t.Errorf("did not return error with bad input") + } + if len(found) != 0 { + t.Errorf("expected 0, found %d", len(found)) + } + if !strings.Contains(err.Error(), "expected more than 2 lines of data, found") { + t.Errorf("expected 'parsing integer error' message, got %q", err) + } +} + +func TestSmartPqiLogicaDeviceBadDeviceNumber(t *testing.T) { + var input = ` +Logical Device number #! + Logical Device name : Logical Drive 1 + Disk Name : /dev/sdd (Disk0) (Bus: 1, Target: 0, Lun: 0) +` + found, err := parseLogicalDevices(input) + if err == nil { + t.Errorf("did not return error with bad output") + } + if len(found) != 0 { + t.Errorf("expected 0, found %d", len(found)) + } + if !strings.Contains(err.Error(), "error while parsing integer from") { + t.Errorf("expected 'parsing integer error' message, got %q", err) + } +} + +func TestSmartPqiLogicaDeviceNumberMultidigit(t *testing.T) { + var input = ` +Logical Device number 127 + Logical Device name : Logical Drive 127 + Disk Name : /dev/sdd (Disk0) (Bus: 1, Target: 0, Lun: 0) + Array : 127 +` + found, err := parseLogicalDevices(input) + if err != nil { + t.Errorf("expected err to be nil, got %s", err) + } + if len(found) != 1 { + t.Errorf("expected 1, found %d", len(found)) + } + if found[0].ArrayID != 127 { + t.Errorf("expected logical device with ArrayID = 127, got %d", found[0].ArrayID) + } +} + +func TestSmartPqiLogicaDeviceBadAtoiTest(t *testing.T) { + var inputArray = ` +Logical Device number 0 + Logical Device name : Logical Drive 1 + Disk Name : /dev/sdd (Disk0) (Bus: 1, Target: 0, Lun: 0) + Array : err + Block Size of member drives : 512 Bytes + Size : 1144641 MB +` + var inputBsize = ` +Logical Device number 0 + Logical Device name : Logical Drive 1 + Disk Name : /dev/sdd (Disk0) (Bus: 1, Target: 0, Lun: 0) + Array : 1 + Block Size of member drives : zero Bytes + Size : 1144641 MB +` + var inputSize = ` +Logical Device number 0 + Logical Device name : Logical Drive 1 + Disk Name : /dev/sdd (Disk0) (Bus: 1, Target: 0, Lun: 0) + Array : 1 + Block Size of member drives : 512 Bytes + Size : xxxxx MB +` + + testCases := []struct { + input string + expectedErr string + }{ + {inputArray, "failed to parse ArrayID from token"}, + {inputBsize, "failed to parse BlockSize from token"}, + {inputSize, "failed to parse Size from token"}, + } + + for idx, testCase := range testCases { + found, err := parseLogicalDevices(testCase.input) + if err == nil { + t.Errorf("did not return error with bad input index %d", idx) + } + if len(found) != 0 { + t.Errorf("expected 0, found %d index %d", len(found), idx) + } + if !strings.Contains(err.Error(), testCase.expectedErr) { + t.Errorf("expected '%s' message, got %q index %d", testCase.expectedErr, err, idx) + } + } +} + func TestArcconfParseConf(t *testing.T) { expectedPDs := []PhysicalDevice{} expectedPDsJSON := []string{ @@ -925,3 +1050,109 @@ func TestArcconfParseConf(t *testing.T) { } } } + +func TestSmartPqiPhysicalDeviceBadFormats(t *testing.T) { + var inputShort = ` + Device #0 +` + var inputDevId = ` + Device #err + Device is a Hard drive + State : Online + Drive has stale RIS data : False + Block Size : 512 Bytes +` + var inputBsize = ` + Device #0 + State : Online + Device is a Hard drive + Drive has stale RIS data : False + Block Size : XXX Bytes +` + var inputPhyBsize = ` + Device #0 + Physical Block Size : abc Bytes +` + var inputTotalSize = ` + Device #0 + Total Size : XXX MB +` + + testCases := []struct { + input string + expectedErr string + }{ + {inputShort, "expected more than 2 lines of data"}, + {inputDevId, "error finding start of PhysicalDevice in data"}, + {inputBsize, "failed to parse Block Size from token"}, + {inputPhyBsize, "failed to parse Physical Block Size from token"}, + {inputTotalSize, "failed to parse Total Size from token"}, + } + + for idx, testCase := range testCases { + found, err := parsePhysicalDevices(testCase.input) + if err == nil { + t.Errorf("did not return error with bad input index %d", idx) + } + if len(found) != 0 { + t.Errorf("expected 0, found %d index %d", len(found), idx) + } + if !strings.Contains(err.Error(), testCase.expectedErr) { + t.Errorf("expected '%s' message, got %q index %d", testCase.expectedErr, err, idx) + } + } +} + +func TestSmartPqiInterface(t *testing.T) { + arc := ArcConf() + if arc == nil { + t.Errorf("expected ArcConf pointer, found nil") + } +} + +func TestSmartPqiNewController(t *testing.T) { + cID := 1 + ctrl, err := newController(cID, arcConfGetConfig) + if err != nil { + t.Errorf("unexpected error creating new controller: %s", err) + } + + expectedJSON := `{"ID":1,"PhysicalDrives":{"0":{"ArrayID":0,"Availability":"Online","BlockSize":512,"Channel":0,"ID":0,"Firmware":"5703","Model":"AL15SEB060N","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"63M0A0BYFJPF","SizeMB":572325,"Type":"HDD","Vendor":"TOSHIBA","WriteCache":"Disabled (write-through)"},"1":{"ArrayID":1,"Availability":"Online","BlockSize":512,"Channel":0,"ID":1,"Firmware":"5701","Model":"AL15SEB120N","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"59M0A06CFJRG","SizeMB":1144641,"Type":"HDD","Vendor":"TOSHIBA","WriteCache":"Disabled (write-through)"},"2":{"ArrayID":2,"Availability":"Online","BlockSize":512,"Channel":0,"ID":2,"Firmware":"CN03","Model":"ST1200MM0009","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"WFK076DT0000E821CET2","SizeMB":1144641,"Type":"HDD","Vendor":"SEAGATE","WriteCache":"Disabled (write-through)"}},"LogicalDrives":{"0":{"ArrayID":0,"BlockSize":512,"Caching":"","Devices":[{"ArrayID":0,"Availability":"Online","BlockSize":512,"Channel":0,"ID":0,"Firmware":"5703","Model":"AL15SEB060N","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"63M0A0BYFJPF","SizeMB":572325,"Type":"HDD","Vendor":"TOSHIBA","WriteCache":"Disabled (write-through)"}],"DiskName":"/dev/sdd","ID":0,"InterfaceType":"SCSI","Name":"Logical Drive 1","RAIDLevel":"0","SizeMB":572293},"1":{"ArrayID":1,"BlockSize":512,"Caching":"","Devices":[{"ArrayID":1,"Availability":"Online","BlockSize":512,"Channel":0,"ID":1,"Firmware":"5701","Model":"AL15SEB120N","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"59M0A06CFJRG","SizeMB":1144641,"Type":"HDD","Vendor":"TOSHIBA","WriteCache":"Disabled (write-through)"}],"DiskName":"/dev/sde","ID":1,"InterfaceType":"SCSI","Name":"Logical Drive 2","RAIDLevel":"0","SizeMB":1144609},"2":{"ArrayID":2,"BlockSize":512,"Caching":"","Devices":[{"ArrayID":2,"Availability":"Online","BlockSize":512,"Channel":0,"ID":2,"Firmware":"CN03","Model":"ST1200MM0009","PhysicalBlockSize":512,"Protocol":"","SerialNumber":"WFK076DT0000E821CET2","SizeMB":1144641,"Type":"HDD","Vendor":"SEAGATE","WriteCache":"Disabled (write-through)"}],"DiskName":"/dev/sdc","ID":2,"InterfaceType":"SCSI","Name":"Logical Drive 3","RAIDLevel":"0","SizeMB":1144609}}}` + expCtrl := Controller{} + if err := json.Unmarshal([]byte(expectedJSON), &expCtrl); err != nil { + t.Errorf("failed to unmarshal expected Controller JSON: %s", err) + } + + if !reflect.DeepEqual(expCtrl, ctrl) { + t.Errorf("controller differed:\n found: %#v\n expct: %#v ", ctrl, expCtrl) + } + +} + +func TestSmartPqiNewControllerBadInput(t *testing.T) { + var inputEmpty = `` + var inputEmptyLines = ` + + +` + testCases := []struct { + input string + expectedErr string + }{ + {inputEmpty, "failed to parse arcconf getconfig output: cannot parse an empty string"}, + {inputEmptyLines, "failed to parse arcconf getconfig output: expected more than 3 lines of data in input"}, + } + + cID := 1 + for idx, testCase := range testCases { + _, err := newController(cID, testCase.input) + if err == nil { + t.Errorf("did not return error with bad input index %d", idx) + continue + } + + if !strings.Contains(err.Error(), testCase.expectedErr) { + t.Errorf("expected '%s' message, got %q index %d", testCase.expectedErr, err, idx) + } + } +}