From 3e27944eb0fe4d98a77778718259827fd421154f Mon Sep 17 00:00:00 2001 From: Sebastian Borza Date: Mon, 23 Jan 2017 15:26:19 -0600 Subject: [PATCH] initial commit smartctl, telegraf --- plugins/inputs/all/all.go | 1 + plugins/inputs/smartctl/README.md | 41 ++ plugins/inputs/smartctl/smartctl.go | 447 ++++++++++++++++++ plugins/inputs/smartctl/smartctl_nocompile.go | 3 + plugins/inputs/smartctl/smartctl_test.go | 375 +++++++++++++++ 5 files changed, 867 insertions(+) create mode 100644 plugins/inputs/smartctl/README.md create mode 100644 plugins/inputs/smartctl/smartctl.go create mode 100644 plugins/inputs/smartctl/smartctl_nocompile.go create mode 100644 plugins/inputs/smartctl/smartctl_test.go diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 7846f8c9a851b..76523f3195cbf 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -64,6 +64,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/rethinkdb" _ "github.com/influxdata/telegraf/plugins/inputs/riak" _ "github.com/influxdata/telegraf/plugins/inputs/sensors" + _ "github.com/influxdata/telegraf/plugins/inputs/smartctl" _ "github.com/influxdata/telegraf/plugins/inputs/snmp" _ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy" _ "github.com/influxdata/telegraf/plugins/inputs/sqlserver" diff --git a/plugins/inputs/smartctl/README.md b/plugins/inputs/smartctl/README.md new file mode 100644 index 0000000000000..950ed2b96e57f --- /dev/null +++ b/plugins/inputs/smartctl/README.md @@ -0,0 +1,41 @@ +# S.M.A.R.T metrics collection + +A linux-only collector to query metrics from smartmontools for S.M.A.R.T-capable +hard drives (HDDs), solid-state drives (SSDs) and others. + +Read more on smartmontools at the [official](https://www.smartmontools.org/) link here. +Read more on S.M.A.R.T and its origin [here](https://en.wikipedia.org/wiki/S.M.A.R.T.) + +# Sample Config +``` + ## smartctl requires installation of the smartmontools for your distro (linux only) + ## along with root permission to run. In this collector we presume sudo access to the + ## binary. + ## + ## Users have the ability to specify an list of disk name to include, to exclude, + ## or both. In this iteration of the collectors, you must specify the full smartctl + ## path for the disk, we are not currently supporting regex. For example, to include/exclude + ## /dev/sda from your list, you would specify: + ## include = ["/dev/sda -d scsi"] + ## exclude = ['/dev/sda -d scsi"] + ## + ## NOTE: If you specify an include list, this will skip the smartctl --scan function + ## and only collect for those you've requested (minus any exclusions). + [[inputs.smartctl]] + include = ["/dev/bus/0 -d megaraid,24"] + exclude = ["/dev/sda -d scsi"] +``` + +# Points to note +1. The smartmontools are required to run with sudo permissions. This means two things: +you will have to enable your telegraf user with the proper sudo perms to run +smartmontools; and you should make sure the smartmontools pkg is installed :D + +2. This collector is set to run a smartctl scan to find all disks on a system. If +this is not what you'd like to do, you can specify the disks you'd like to collect +by itemizing them in the `include` option in the telegraf config. This will skip +the scan command and ONLY run smartctl on those disks in the list. + +3. A corollary to the `include` set of disks is the `exclude` set. In this case, +the scan will still be run unless `include` is defined but will limit those to +query for based on the set in the `exclude`. diff --git a/plugins/inputs/smartctl/smartctl.go b/plugins/inputs/smartctl/smartctl.go new file mode 100644 index 0000000000000..9c9d949fc76bf --- /dev/null +++ b/plugins/inputs/smartctl/smartctl.go @@ -0,0 +1,447 @@ +// +build linux + +// Package smartctl is a collector for S.M.A.R.T data for HDD, SSD + NVMe devices, linux only +// https://www.smartmontools.org/ +package smartctl + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +// Disk is the struct to capture the specifics of a given device +// It will store some basic information plus anything extended queries return +type Disk struct { + Name string + Vendor string + Product string + Block string + Serial string + Rotation string + Transport string + Health string + ReadCache string + Writeback string + RawData bytes.Buffer + Stats DiskStats +} + +// DiskFail is the struct to return from our goroutine parseDisks +type DiskFail struct { + Name string + Error error +} + +// DiskStats is the set of fields we'll be collecting from smartctl +type DiskStats struct { + CurrentTemp float64 + MaxTemp float64 + ReadError []float64 + WriteError []float64 + VerifyError []float64 +} + +// SmartCtl is the struct that stores our disk paths for checking +type SmartCtl struct { + Init bool + SudoPath string + CtlPath string + Include []string + Exclude []string + Disks []string + DiskOutput map[string]Disk + DiskFailed map[string]error +} + +// tagFinders is a global map of Disk struct elements to corresponding regexp +var tagFinders = map[string]*regexp.Regexp{ + "Vendor": regexp.MustCompile(`Vendor:\s+(\w+)`), + "Product": regexp.MustCompile(`Product:\s+(\w+)`), + "Block": regexp.MustCompile(`Logical block size:\s+(\w+)`), + "Serial": regexp.MustCompile(`Serial number:\s+(\w+)`), + "Rotation": regexp.MustCompile(`Rotation Rate:\s+(\w+)`), + "Transport": regexp.MustCompile(`Transport protocol:\s+(\w+)`), + "Health": regexp.MustCompile(`SMART Health Status:\s+(\w+)`), + "ReadCache": regexp.MustCompile(`Read Cache is:\s+(\w+)`), + "Writeback": regexp.MustCompile(`Writeback Cache is:\s+(\w+)`), +} + +// fieldFinders is a global map of DiskStats struct elements +var fieldFinders = map[string]*regexp.Regexp{ + "CurrentTemp": regexp.MustCompile(`Current Drive Temperature:\s+([0-9]+)`), + "MaxTemp": regexp.MustCompile(`Drive Trip Temperature:\s+([0-9]+)`), +} + +// sliceFinders is a global map of slices in the DiskStats struct +var sliceFinders = map[string]*regexp.Regexp{ + "ReadError": regexp.MustCompile(`read:\s+(.*)\n`), + "WriteError": regexp.MustCompile(`write:\s+(.*)\n`), + "VerifyError": regexp.MustCompile(`verify:\s+(.*)\n`), +} + +var sampleConfig = ` + ## smartctl requires installation of the smartmontools for your distro (linux only) + ## along with root permission to run. In this collector we presume sudo access to the + ## binary. + ## + ## Users have the ability to specify an list of disk name to include, to exclude, + ## or both. In this iteration of the collectors, you must specify the full smartctl + ## path for the disk, we are not currently supporting regex. For example, to include/exclude + ## /dev/sda from your list, you would specify: + ## include = ["/dev/sda -d scsi"] + ## exclude = ['/dev/sda -d scsi"] + ## + ## NOTE: If you specify an include list, this will skip the smartctl --scan function + ## and only collect for those you've requested (minus any exclusions). + include = ["/dev/bus/0 -d megaraid,24"] + exclude = ["/dev/sda -d scsi"] +` + +// SampleConfig returns the preformatted string on how to use the smartctl collector +func (s *SmartCtl) SampleConfig() string { + return sampleConfig +} + +// Description returns a preformatted string outlining the smartctl collector +func (s *SmartCtl) Description() string { + return "Use the linux smartmontool to determine HDD, SSD or NVMe physical status" +} + +// ParseString is a generic function that takes a given regexp and applies to a buf, placing +// output into the dataVar +func (s *SmartCtl) ParseString(regexp *regexp.Regexp, buf *bytes.Buffer, dataVar *string) { + str := regexp.FindStringSubmatch((*buf).String()) + + if len(str) > 1 { + *dataVar = str[1] + return + } + + *dataVar = "none" +} + +// ParseStringSlice is a generic function that takes a given regexp and applies to a buf, placing +// output into the dataVar +func (s *SmartCtl) ParseStringSlice(regexp *regexp.Regexp, buf *bytes.Buffer, dataVar *[]string) { + str := regexp.FindStringSubmatch((*buf).String()) + + if len(str) > 1 { + *dataVar = str[1:] + } +} + +// ParseFloat is a generic function that takes a given regexp and applies to a buf, placing +// output into the dataVar +func (s *SmartCtl) ParseFloat(regexp *regexp.Regexp, buf *bytes.Buffer, dataVar *float64) (err error) { + str := regexp.FindStringSubmatch((*buf).String()) + + if len(str) > 1 { + *dataVar, err = strconv.ParseFloat(str[1], 64) + if err != nil { + return fmt.Errorf("[ERROR] Could not convert string (%s) to float64: %v\n", str[1], err) + } + } + + return nil +} + +// ParseFloatSlice is a generic function that takes a given regexp and applies to a buf, placing +// output into the dataVar +func (s *SmartCtl) ParseFloatSlice(regexp *regexp.Regexp, buf *bytes.Buffer, dataVar *[]float64) (err error) { + var errors []string + var values []float64 + var val float64 + + str := regexp.FindStringSubmatch((*buf).String()) + + if len(str) > 1 { + for _, each := range strings.Split(str[1], " ") { + if len(each) <= 0 { + continue + } else if val, err = strconv.ParseFloat(each, 64); err != nil { + errors = append(errors, fmt.Sprintf("[ERROR] Could not parse string (%s) into float64: %v", each, err)) + continue + } + values = append(values, val) + } + } + + *dataVar = values + if len(errors) > 0 { + return fmt.Errorf("%s\n", strings.Join(errors, "\n")) + } + + return nil +} + +// parseDisks is a private function that we call for our goroutine +func (s *SmartCtl) parseDisks(each string, c chan<- Disk, e chan<- DiskFail) { + var out []byte + var data Disk + var stats DiskStats + var err error + + var tagMap = map[string]*string{ + "Vendor": &data.Vendor, + "Product": &data.Product, + "Block": &data.Block, + "Serial": &data.Serial, + "Rotation": &data.Rotation, + "Transport": &data.Transport, + "Health": &data.Health, + "ReadCache": &data.ReadCache, + "Writeback": &data.Writeback, + } + + var fieldMap = map[string]*float64{ + "CurrentTemp": &stats.CurrentTemp, + "MaxTemp": &stats.MaxTemp, + } + + var sliceMap = map[string]*[]float64{ + "ReadError": &stats.ReadError, + "WriteError": &stats.WriteError, + "VerifyError": &stats.VerifyError, + } + + disk := strings.Split(each, " ") + cmd := []string{s.CtlPath, "-x"} + cmd = append(cmd, disk...) + + data = Disk{Name: "empty"} + if out, err = exec.Command(s.SudoPath, cmd...).CombinedOutput(); err != nil { + e <- DiskFail{Name: each, Error: fmt.Errorf("[ERROR] could not collect (%s), err: %v\n", each, err)} + return + } + + if _, err = data.RawData.Write(out); err != nil { + e <- DiskFail{Name: each, Error: fmt.Errorf("[ERROR] could not commit raw data to struct (%s): %v\n", each, err)} + return + } + + if len(disk) > 2 { + data.Name = strings.Replace(fmt.Sprintf("%s_%s", disk[0], disk[2]), ",", "_", -1) + } else { + data.Name = strings.Replace(disk[0], ",", "_", -1) + } + + // NOTE: for this loop to work you must keep the idx + Disk element names equal + for idx := range tagFinders { + s.ParseString(tagFinders[idx], &data.RawData, tagMap[idx]) + } + + stats = DiskStats{} + for idx := range fieldFinders { + if err = s.ParseFloat(fieldFinders[idx], &data.RawData, fieldMap[idx]); err != nil { + fmt.Printf("[ERROR] ParseFloat: %v\n", err) + } + } + + for idx := range sliceFinders { + if err = s.ParseFloatSlice(sliceFinders[idx], &data.RawData, sliceMap[idx]); err != nil { + fmt.Printf("[ERROR] ParseFloatSlice: %v\n", err) + } + } + + data.Stats = stats + c <- data +} + +// ParseDisks takes in a list of Disks and accumulates the smartctl info where possible for each entry +func (s *SmartCtl) ParseDisks() (err error) { + c := make(chan Disk, len(s.Disks)) + e := make(chan DiskFail, len(s.Disks)) + var a int + + for _, each := range s.Disks { + go s.parseDisks(each, c, e) + } + + for { + if a == len(s.Disks) { + break + } + + select { + case data := <-c: + if len(data.Name) > 0 && data.Name != "empty" { + s.DiskOutput[data.Name] = data + } + a++ + case err := <-e: + s.DiskFailed[err.Name] = err.Error + a++ + default: + time.Sleep(50 * time.Millisecond) + } + } + + return err +} + +// splitDisks is a private helper function to parse out the disks we care about +func (s *SmartCtl) splitDisks(out string) (disks []string) { + for _, each := range strings.Split(out, "\n") { + if len(each) > 0 { + disks = append(disks, strings.Split(each, " #")[0]) + } + } + return disks +} + +func (s *SmartCtl) gatherDisks() (err error) { + var out []byte + + cmd := []string{s.CtlPath, "--scan"} + + if out, err = exec.Command(s.SudoPath, cmd...).CombinedOutput(); err != nil { + return fmt.Errorf("[ERROR] Could not gather disks from smartctl --scan: %v\n", err) + } + + s.Disks = s.splitDisks(string(out)) + + return nil +} + +// ExcludeDisks is a private function to reduce the set of disks to query against +func (s *SmartCtl) ExcludeDisks() (disks []string) { + elems := make(map[string]bool) + + for _, each := range s.Disks { + elems[each] = false + } + + for _, each := range s.Exclude { + if _, ok := elems[each]; ok { + delete(elems, each) + } + } + + for key := range elems { + disks = append(disks, key) + } + + return disks +} + +// initStruct is a private function to confirm we have smartctl reqs installed/accessible +func (s *SmartCtl) initStruct() (err error) { + if s.SudoPath, err = exec.LookPath("sudo"); err != nil { + s.Init = false + return fmt.Errorf("could not pull path for 'sudo': %v\n", err) + } + + if s.CtlPath, err = exec.LookPath("smartctl"); err != nil { + s.Init = false + return fmt.Errorf("could not pull path for 'smartctl': %v\n", err) + } + + // NOTE: if we specify the Include list in the config, this will skip the smartctl --scan + if len(s.Include) > 0 { + s.Disks = s.splitDisks(strings.Join(s.Include, "\n")) + } else if err = s.gatherDisks(); err != nil { + return err + } + + if len(s.Exclude) > 0 { + s.Disks = s.ExcludeDisks() + } + + s.DiskOutput = make(map[string]Disk, len(s.Disks)) + s.DiskFailed = make(map[string]error) + + s.Init = true + + return nil +} + +// init adds the smartctl collector as an input to telegraf +func init() { + inputs.Add("smartctl", func() telegraf.Input { return &SmartCtl{} }) +} + +// Gather is the primary function to collect smartctl data +func (s *SmartCtl) Gather(acc telegraf.Accumulator) (err error) { + var health float64 + + if !s.Init { + if err = s.initStruct(); err != nil { + return fmt.Errorf("could not initialize smartctl plugin: %v\n", err) + } + } + + // actually gather the stats + if err = s.ParseDisks(); err != nil { + return fmt.Errorf("could not parse all the disks in our list: %v\n", err) + } + + for _, each := range s.DiskOutput { + tags := map[string]string{ + "name": each.Name, + "vendor": each.Vendor, + "product": each.Product, + "block_size": each.Block, + "serial": each.Serial, + "rpm": each.Rotation, + "transport": each.Transport, + "read_cache": each.ReadCache, + "writeback": each.Writeback, + } + + if each.Health == "OK" { + health = 1.0 + } else { + health = 0.0 + } + + fields := make(map[string]interface{}) + fields["health"] = health + fields["current_temp"] = each.Stats.CurrentTemp + fields["max_temp"] = each.Stats.MaxTemp + + // add the read error row + if len(each.Stats.ReadError) == 7 { + fields["ecc_corr_fast_read"] = each.Stats.ReadError[0] + fields["ecc_corr_delay_read"] = each.Stats.ReadError[1] + fields["ecc_reread"] = each.Stats.ReadError[2] + fields["total_err_corr_read"] = each.Stats.ReadError[3] + fields["corr_algo_read"] = each.Stats.ReadError[4] + fields["data_read"] = each.Stats.ReadError[5] + fields["uncorr_err_read"] = each.Stats.ReadError[6] + } + + // add the write error row + if len(each.Stats.WriteError) == 7 { + fields["ecc_corr_fast_write"] = each.Stats.WriteError[0] + fields["ecc_corr_delay_write"] = each.Stats.WriteError[1] + fields["ecc_rewrite"] = each.Stats.WriteError[2] + fields["total_err_corr_write"] = each.Stats.WriteError[3] + fields["corr_algo_write"] = each.Stats.WriteError[4] + fields["data_write"] = each.Stats.WriteError[5] + fields["uncorr_err_write"] = each.Stats.WriteError[6] + } + + // add the verify error row + if len(each.Stats.VerifyError) == 7 { + fields["ecc_corr_fast_verify"] = each.Stats.VerifyError[0] + fields["ecc_corr_delay_verify"] = each.Stats.VerifyError[1] + fields["ecc_reverify"] = each.Stats.VerifyError[2] + fields["total_err_corr_verify"] = each.Stats.VerifyError[3] + fields["corr_algo_verify"] = each.Stats.VerifyError[4] + fields["data_verify"] = each.Stats.VerifyError[5] + fields["uncorr_err_verify"] = each.Stats.VerifyError[6] + } + + acc.AddFields("smartctl", fields, tags) + } + + return nil +} diff --git a/plugins/inputs/smartctl/smartctl_nocompile.go b/plugins/inputs/smartctl/smartctl_nocompile.go new file mode 100644 index 0000000000000..9f5ff2f49f866 --- /dev/null +++ b/plugins/inputs/smartctl/smartctl_nocompile.go @@ -0,0 +1,3 @@ +// +build !linux + +package smartctl diff --git a/plugins/inputs/smartctl/smartctl_test.go b/plugins/inputs/smartctl/smartctl_test.go new file mode 100644 index 0000000000000..91f70079aa762 --- /dev/null +++ b/plugins/inputs/smartctl/smartctl_test.go @@ -0,0 +1,375 @@ +// +build linux + +// Package smartctl is a collector for S.M.A.R.T data for HDD, SSD + NVMe devices, linux only +// https://www.smartmontools.org/ +package smartctl + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strings" + "testing" + + "github.com/influxdata/telegraf/testutil" +) + +var bencher = &SmartCtl{ + Disks: []string{"/dev/sda -d scsi", "/dev/bus/0 -d megaraid,4", "/dev/bus/0 -d megaraid,24"}, + Init: true, + DiskOutput: make(map[string]Disk, 3), + DiskFailed: make(map[string]error), +} + +// TestSmartCtl_Gather sends a naked SmartCtl function to the machine to see how data can parse. Virtually +// any output is accepted here, provided it's not an err +func TestSmartCtl_Gather(t *testing.T) { + tester := &SmartCtl{} + + acc := new(testutil.Accumulator) + err := tester.Gather(acc) + + if err != nil { + if strings.Contains(fmt.Sprintf("%v", err), "executable") { + t.Logf("[INFO] smartctl binary doesn't exist, but in testmode we continue: %v", err) + } else { + t.Errorf("[ERROR] Did not expect error, but received err: %v, data: %v\n", err, tester.DiskFailed) + } + } else if len(tester.DiskFailed) > 0 { + t.Logf("[INFO] Did not receive error, but some disks failed: %v, init: %t, output: %#v\n", tester.DiskFailed, tester.Init, tester.Disks) + } else { + t.Logf("[INFO] init: %t, output: %#v, len: %d\n", tester.Init, tester.Disks, len(tester.Disks)) + } +} + +// TestSmartCtl_GatherInclude sends a specific disk name in the Include portion to Gather, which +// should skip smartctl --scan altogether; we fail if err != nil or the disklist returned is > 1 +func TestSmartCtl_GatherInclude(t *testing.T) { + tester := bencher + tester.Init = false + tester.Include = []string{"/dev/myrandomtester -d scsi"} + + acc := new(testutil.Accumulator) + err := tester.Gather(acc) + + if err != nil { + if strings.Contains(fmt.Sprintf("%v", err), "executable") { + t.Logf("[INFO] smartctl binary doesn't exist, but in testmode we continue: %v", err) + } else { + t.Errorf("[ERROR] Did not expect error, but received err: %v, data: %v\n", err, tester.DiskFailed) + } + } + + if len(tester.DiskFailed) > 1 { + t.Errorf("[ERROR] len of failures should be 1 (/dev/sdc only), include: %v, disks: %v\n", tester.Include, tester.Disks) + } +} + +// TestSmartCtl_GatherExclude checks whether our ability to exclude disks from scanning is possible. Here we pass +// a known-good disk in Include, and Exclude it shortly thereafter (with an extra element in our disklist). If we see +// anything > 0 in the list of disks to parse, we fail out +func TestSmartCtl_GatherExclude(t *testing.T) { + tester := &SmartCtl{Include: []string{"/dev/sda -d scsi"}, Exclude: []string{"/dev/sda -d scsi", "/dev/bus/0 -d megaraid,4"}} + + acc := new(testutil.Accumulator) + err := tester.Gather(acc) + + if err != nil { + if strings.Contains(fmt.Sprintf("%v", err), "executable") { + t.Logf("[INFO] smartctl binary doesn't exist, but in testmode we continue: %v", err) + } else { + t.Errorf("[ERROR] Did not expect error, but received err: %v, data: %v\n", err, tester.DiskFailed) + } + } + + if len(tester.DiskOutput) > 0 { + t.Errorf("[ERROR] Disks should have cancelled out, include: %v, exclude: %v, disks: %v\n", tester.Include, tester.Exclude, tester.Disks) + } +} + +// TestSmartCtl_ParseDisks checks whether we can parse any of the data at all +func TestSmartCtl_ParseDisks(t *testing.T) { + tester := bencher + tester.SudoPath, _ = exec.LookPath("sudo") + tester.CtlPath, _ = exec.LookPath("smartctl") + + if err := tester.ParseDisks(); err != nil { + t.Errorf("[ERROR] Unable to parse disks from the system: %v\n", err) + } + + if len(tester.DiskFailed) > 0 { + t.Logf("[INFO] Did not get a good result back from ParseDisks: %v\n", tester.DiskFailed) + } + + if len(tester.DiskOutput) > 0 { + t.Logf("[INFO] We parsed the disk info:\n") + + for _, each := range tester.DiskOutput { + t.Logf("[%s] vendor: %s, product: %s, block: %s, serial: %s, rotation: %s, transport: %s, health: %s\n", + each.Name, each.Vendor, each.Product, each.Block, each.Serial, each.Rotation, each.Transport, each.Health) + t.Logf("[%s] stats: %#v\n", each.Name, each.Stats) + } + } +} + +// BenchmarkParseString will check how performant our parsing is +func BenchmarkParseString(b *testing.B) { + var buf bytes.Buffer + var str string + buf.WriteString(testData) + + findVendor := regexp.MustCompile(`Vendor:\s+(\w+)`) + + for n := 0; n < b.N; n++ { + bencher.ParseString(findVendor, &buf, &str) + } +} + +// BenchmarkParseStringSlice will check how performant our parsing is +func BenchmarkParseStringSlice(b *testing.B) { + var buf bytes.Buffer + var str []string + buf.WriteString(testData) + + findVerify := regexp.MustCompile(`verify:\s+(.*)\n`) + + for n := 0; n < b.N; n++ { + bencher.ParseStringSlice(findVerify, &buf, &str) + } +} + +// BenchmarkParseFloat will check how performant our parsing is +func BenchmarkParseFloat(b *testing.B) { + var buf bytes.Buffer + var val float64 + buf.WriteString(testData) + + findTemp := regexp.MustCompile(`Current Drive Temperature:\s+([0-9]+)`) + + for n := 0; n < b.N; n++ { + bencher.ParseFloat(findTemp, &buf, &val) + } +} + +// BenchmarkParseFloatSlice will check how performant our parsing is +func BenchmarkParseFloatSlice(b *testing.B) { + var buf bytes.Buffer + var val []float64 + buf.WriteString(testData) + + findVerify := regexp.MustCompile(`verify:\s+(.*)\n`) + + for n := 0; n < b.N; n++ { + bencher.ParseFloatSlice(findVerify, &buf, &val) + } +} + +// BenchmarkExcludeDisks checks how quickly we can drill down to the set of disks to parse for +func BenchmarkExcludeDisks(b *testing.B) { + tester := &SmartCtl{Disks: []string{ + "/dev/bus/0 -d megaraid,0", + "/dev/bus/0 -d megaraid,1", + "/dev/bus/0 -d megaraid,2", + "/dev/bus/0 -d megaraid,3", + "/dev/bus/0 -d megaraid,4", + "/dev/bus/0 -d megaraid,5", + "/dev/bus/0 -d megaraid,6", + "/dev/bus/0 -d megaraid,7", + "/dev/bus/0 -d megaraid,8", + "/dev/bus/0 -d megaraid,9", + "/dev/bus/0 -d megaraid,10", + "/dev/bus/0 -d megaraid,11", + "/dev/bus/0 -d megaraid,12", + "/dev/bus/0 -d megaraid,13", + "/dev/bus/0 -d megaraid,14", + "/dev/bus/0 -d megaraid,15", + "/dev/bus/0 -d megaraid,16", + "/dev/bus/0 -d megaraid,17", + "/dev/bus/0 -d megaraid,18", + "/dev/bus/0 -d megaraid,19", + "/dev/bus/0 -d megaraid,20", + "/dev/bus/0 -d megaraid,21", + "/dev/bus/0 -d megaraid,22", + "/dev/bus/0 -d megaraid,23", + "/dev/bus/0 -d megaraid,24", + "/dev/bus/0 -d megaraid,25", + }, Exclude: []string{"/dev/bus/0 -d megaraid,21"}, Init: true} + + for n := 0; n < b.N; n++ { + tester.ExcludeDisks() + } +} + +var testData = ` +smartctl 6.2 2013-07-26 r3841 [x86_64-linux-3.10.0-327.10.1.el7.jump7.x86_64] (local build) +Copyright (C) 2002-13, Bruce Allen, Christian Franke, www.smartmontools.org + +=== START OF INFORMATION SECTION === +Vendor: SEAGATE +Product: ST1200MM0007 +Revision: IS06 +User Capacity: 1,200,243,695,616 bytes [1.20 TB] +Logical block size: 512 bytes +Logical block provisioning type unreported, LBPME=-1, LBPRZ=0 +Rotation Rate: 10000 rpm +Form Factor: 2.5 inches +Logical Unit id: 0x5000c5007f4fefff +Serial number: S3L183CF +Device type: disk +Transport protocol: SAS +Local Time is: Tue Jan 17 16:19:22 2017 CST +SMART support is: Available - device has SMART capability. +SMART support is: Enabled +Temperature Warning: Disabled or Not Supported +Read Cache is: Enabled +Writeback Cache is: Disabled + +=== START OF READ SMART DATA SECTION === +SMART Health Status: OK + +Current Drive Temperature: 26 C +Drive Trip Temperature: 60 C + +Manufactured in week 03 of year 2015 +Specified cycle count over device lifetime: 10000 +Accumulated start-stop cycles: 52 +Specified load-unload count over device lifetime: 300000 +Accumulated load-unload cycles: 761 +Elements in grown defect list: 0 + +Vendor (Seagate) cache information + Blocks sent to initiator = 2498780339 + Blocks received from initiator = 3563773581 + Blocks read from cache and sent to initiator = 2325589923 + Number of read and write commands whose size <= segment size = 15756254 + Number of read and write commands whose size > segment size = 740 + +Vendor (Seagate/Hitachi) factory information + number of hours powered up = 12809.43 + number of minutes until next internal SMART test = 38 + +Error counter log: + Errors Corrected by Total Correction Gigabytes Total + ECC rereads/ errors algorithm processed uncorrected + fast | delayed rewrites corrected invocations [10^9 bytes] errors +read: 1492558298 0 0 1492558298 0 9653.059 0 +write: 0 0 1 1 1 4144.460 0 +verify: 2501279159 0 0 2501279159 0 14578.622 0 + +Non-medium error count: 6 + +SMART Self-test log +Num Test Status segment LifeTime LBA_first_err [SK ASC ASQ] + Description number (hours) +# 1 Reserved(7) Completed 48 72 - [- - -] +# 2 Background short Completed 64 70 - [- - -] +# 3 Background short Completed 64 70 - [- - -] +# 4 Background short Completed 64 68 - [- - -] +# 5 Background short Completed 64 61 - [- - -] +# 6 Background short Completed 64 59 - [- - -] +# 7 Background short Completed 64 53 - [- - -] +# 8 Background short Completed 64 47 - [- - -] +# 9 Background short Completed 64 35 - [- - -] +#10 Background short Completed 64 25 - [- - -] +#11 Background short Completed 64 21 - [- - -] +#12 Background short Completed 64 16 - [- - -] +#13 Background short Completed 64 0 - [- - -] +Long (extended) Self Test duration: 8400 seconds [140.0 minutes] + +Background scan results log + Status: waiting until BMS interval timer expires + Accumulated power on time, hours:minutes 12809:26 [768566 minutes] + Number of background scans performed: 105, scan progress: 0.00% + Number of background medium scans performed: 105 + + # when lba(hex) [sk,asc,ascq] reassign_status + 1 474:13 00000000123b12c6 [1,17,1] Recovered via rewrite in-place + 2 553:44 00000000123b12c7 [1,17,1] Recovered via rewrite in-place + 3 666:11 00000000123b12c8 [1,17,1] Recovered via rewrite in-place + 4 2120:54 000000001292b871 [1,17,1] Recovered via rewrite in-place + 5 2193:52 000000001292b876 [1,17,1] Recovered via rewrite in-place + 6 2193:52 000000001292b877 [1,17,1] Recovered via rewrite in-place + 7 2865:53 00000000123b12c7 [1,17,1] Recovered via rewrite in-place + 8 2865:53 000000001292b878 [1,17,1] Recovered via rewrite in-place + 9 2865:53 000000001292b879 [1,17,1] Recovered via rewrite in-place + 10 3033:53 00000000123b12c6 [1,17,1] Recovered via rewrite in-place + 11 3201:54 00000000123b12c8 [1,17,1] Recovered via rewrite in-place + 12 4156:03 00000000123b12c7 [1,17,1] Recovered via rewrite in-place + 13 5974:27 000000001292b874 [1,17,1] Recovered via rewrite in-place + 14 8157:21 000000001292b875 [1,17,1] Recovered via rewrite in-place + 15 11685:33 00000000123b12c8 [1,17,1] Recovered via rewrite in-place + +Protocol Specific port log page for SAS SSP +relative target port id = 1 + generation code = 0 + number of phys = 1 + phy identifier = 0 + attached device type: expander device + attached reason: SMP phy control function + reason: power on + negotiated logical link rate: phy enabled; 6 Gbps + attached initiator port: ssp=0 stp=0 smp=0 + attached target port: ssp=0 stp=0 smp=1 + SAS address = 0x5000c5007f4feffd + attached SAS address = 0x500056b36789abff + attached phy identifier = 4 + Invalid DWORD count = 0 + Running disparity error count = 0 + Loss of DWORD synchronization = 0 + Phy reset problem = 0 + Phy event descriptors: + Invalid word count: 0 + Running disparity error count: 0 + Loss of dword synchronization count: 0 + Phy reset problem count: 0 +relative target port id = 2 + generation code = 0 + number of phys = 1 + phy identifier = 1 + attached device type: no device attached + attached reason: unknown + reason: unknown + negotiated logical link rate: phy enabled; unknown + attached initiator port: ssp=0 stp=0 smp=0 + attached target port: ssp=0 stp=0 smp=0 + SAS address = 0x5000c5007f4feffe + attached SAS address = 0x0 + attached phy identifier = 0 + Invalid DWORD count = 0 + Running disparity error count = 0 + Loss of DWORD synchronization = 0 + Phy reset problem = 0 + Phy event descriptors: + Invalid word count: 0 + Running disparity error count: 0 + Loss of dword synchronization count: 0 + Phy reset problem count: 0 +` + +var testFailedCollect = ` +smartctl 6.2 2013-07-26 r3841 [x86_64-linux-3.10.0-327.10.1.el7.jump7.x86_64] (local build) +Copyright (C) 2002-13, Bruce Allen, Christian Franke, www.smartmontools.org + +=== START OF INFORMATION SECTION === +Vendor: DELL +Product: PERC H710P +Revision: 3.13 +User Capacity: 299,439,751,168 bytes [299 GB] +Logical block size: 512 bytes +Logical Unit id: 0x6848f690ee7499001c10f9d8041a7867 +Serial number: 0067781a04d8f9101c009974ee90f648 +Device type: disk +Local Time is: Thu Jan 19 14:03:10 2017 CST +SMART support is: Unavailable - device lacks SMART capability. +Read Cache is: Enabled +Writeback Cache is: Enabled + +=== START OF READ SMART DATA SECTION === + +Error Counter logging not supported + +Device does not support Self Test logging +Device does not support Background scan results logging +scsiPrintSasPhy Log Sense Failed [unsupported scsi opcode] +`