new device detection engine (OS aware). Uses smartctl --scan to detect drives (and conditionally uses jaypipes/ghw). WWN is calculated from smartctl data, then retrieved from GHW, and fallsback to serial number. WWN calcuation code is based on IEEE spec, for "Registered" IEEE format - NAA5. TODO: support NAA6 and other formats?
This commit is contained in:
@@ -3,15 +3,8 @@ package collector
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -21,77 +14,6 @@ type BaseCollector struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (c *BaseCollector) DetectStorageDevices() ([]models.Device, error) {
|
||||
|
||||
//availableDisksJson, err := c.ExecCmd("smartctl", []string{"-j", "--scan"}, "", os.Environ())
|
||||
//if err != nil {
|
||||
// c.logger.Errorf("Error getting block storage info: %v", err)
|
||||
// return nil, err
|
||||
//}
|
||||
//
|
||||
//var smartctlScan models.Scan
|
||||
//err = json.Unmarshal([]byte(availableDisksJson), &smartctlScan)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
block, err := ghw.Block()
|
||||
if err != nil {
|
||||
c.logger.Errorf("Error getting block storage info: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
approvedDisks := []models.Device{}
|
||||
for _, disk := range block.Disks {
|
||||
|
||||
// ignore optical drives and floppy disks
|
||||
if disk.DriveType == ghw.DRIVE_TYPE_FDD || disk.DriveType == ghw.DRIVE_TYPE_ODD {
|
||||
c.logger.Debugf(" => Ignore: Optical or floppy disk - (found %s)\n", disk.DriveType.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore removable disks
|
||||
if disk.IsRemovable {
|
||||
c.logger.Debugf(" => Ignore: Removable disk (%v)\n", disk.IsRemovable)
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore virtual disks & mobile phone storage devices
|
||||
if disk.StorageController == ghw.STORAGE_CONTROLLER_VIRTIO || disk.StorageController == ghw.STORAGE_CONTROLLER_MMC {
|
||||
c.logger.Debugf(" => Ignore: Virtual/multi-media storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip unknown storage controllers, not usually S.M.A.R.T compatible.
|
||||
if disk.StorageController == ghw.STORAGE_CONTROLLER_UNKNOWN {
|
||||
c.logger.Debugf(" => Ignore: Unknown storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
diskModel := models.Device{
|
||||
WWN: disk.WWN,
|
||||
Manufacturer: disk.Vendor,
|
||||
ModelName: disk.Model,
|
||||
InterfaceType: disk.StorageController.String(),
|
||||
//InterfaceSpeed: string
|
||||
SerialNumber: disk.SerialNumber,
|
||||
Capacity: int64(disk.SizeBytes),
|
||||
//Firmware string
|
||||
//RotationSpeed int
|
||||
|
||||
DeviceName: disk.Name,
|
||||
}
|
||||
if len(diskModel.WWN) == 0 {
|
||||
//(macOS and some other os's) do not provide a WWN, so we're going to fallback to
|
||||
//diskname as identifier if WWN is not present
|
||||
diskModel.WWN = disk.Name
|
||||
}
|
||||
|
||||
approvedDisks = append(approvedDisks, diskModel)
|
||||
}
|
||||
return approvedDisks, nil
|
||||
}
|
||||
|
||||
func (c *BaseCollector) getJson(url string, target interface{}) error {
|
||||
|
||||
r, err := httpClient.Get(url)
|
||||
@@ -118,29 +40,6 @@ func (c *BaseCollector) postJson(url string, body interface{}, target interface{
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (c *BaseCollector) ExecCmd(cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
|
||||
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
var stdBuffer bytes.Buffer
|
||||
mw := io.MultiWriter(os.Stdout, &stdBuffer)
|
||||
|
||||
cmd.Stdout = mw
|
||||
cmd.Stderr = mw
|
||||
|
||||
if environ != nil {
|
||||
cmd.Env = environ
|
||||
}
|
||||
if workingDir != "" && path.IsAbs(workingDir) {
|
||||
cmd.Dir = workingDir
|
||||
} else if workingDir != "" {
|
||||
return "", errors.New("Working Directory must be an absolute path")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return stdBuffer.String(), err
|
||||
|
||||
}
|
||||
|
||||
func (c *BaseCollector) LogSmartctlExitCode(exitCode int) {
|
||||
if exitCode&0x01 != 0 {
|
||||
c.logger.Errorln("smartctl could not parse commandline")
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package collector_test
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
bc := collector.BaseCollector{}
|
||||
|
||||
//test
|
||||
result, err := bc.ExecCmd("echo", []string{"hello world"}, "", nil)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world\n", result)
|
||||
}
|
||||
|
||||
func TestExecCmd_Date(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
bc := collector.BaseCollector{}
|
||||
|
||||
//test
|
||||
_, err := bc.ExecCmd("date", []string{}, "", nil)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
//
|
||||
//func TestExecCmd_Error(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// //setup
|
||||
// bc := collector.BaseCollector {}
|
||||
//
|
||||
// //test
|
||||
// _, err := bc.ExecCmd("smartctl", []string{"-a", "/dev/doesnotexist"}, "", nil)
|
||||
//
|
||||
// //assert
|
||||
// exitError, castOk := err.(*exec.ExitError);
|
||||
// require.True(t, castOk)
|
||||
// require.Equal(t, 1, exitError.ExitCode())
|
||||
//
|
||||
//}
|
||||
//
|
||||
|
||||
func TestExecCmd_InvalidCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
bc := collector.BaseCollector{}
|
||||
|
||||
//test
|
||||
_, err := bc.ExecCmd("invalid_binary", []string{}, "", nil)
|
||||
|
||||
//assert
|
||||
_, castOk := err.(*exec.ExitError)
|
||||
require.False(t, castOk)
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/common"
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -43,13 +46,18 @@ func (mc *MetricsCollector) Run() error {
|
||||
apiEndpoint.Path = "/api/devices/register"
|
||||
|
||||
deviceRespWrapper := new(models.DeviceWrapper)
|
||||
detectedStorageDevices, err := mc.DetectStorageDevices()
|
||||
|
||||
deviceDetector := detect.Detect{
|
||||
Logger: mc.logger,
|
||||
}
|
||||
detectedStorageDevices, err := deviceDetector.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
||||
mc.logger.Debugf("Detected devices: %v", detectedStorageDevices)
|
||||
jsonObj, _ := json.Marshal(detectedStorageDevices)
|
||||
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
|
||||
err = mc.postJson(apiEndpoint.String(), models.DeviceWrapper{
|
||||
Data: detectedStorageDevices,
|
||||
}, &deviceRespWrapper)
|
||||
@@ -93,7 +101,7 @@ func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, device
|
||||
defer wg.Done()
|
||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||
|
||||
result, err := mc.ExecCmd("smartctl", []string{"-a", "-j", fmt.Sprintf("/dev/%s", deviceName)}, "", nil)
|
||||
result, err := common.ExecCmd("smartctl", []string{"-a", "-j", fmt.Sprintf("/dev/%s", deviceName)}, "", nil)
|
||||
resultBytes := []byte(result)
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
|
||||
Reference in New Issue
Block a user