commit
d6fad90fa4
@ -0,0 +1,35 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExecCmd(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
|
||||
logger.Infof("Executing command: %s %s", cmdName, strings.Join(cmdArgs, " "))
|
||||
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
var stdBuffer bytes.Buffer
|
||||
mw := io.MultiWriter(logger.Logger.Out, &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
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/common"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Detect struct {
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
//private/common functions
|
||||
|
||||
// This function calls smartctl --scan which can be used to detect storage devices.
|
||||
// It has a couple of issues however:
|
||||
// - --scan does not return any results on mac
|
||||
//
|
||||
// To handle these issues, we have OS specific wrapper functions that update/modify these detected devices.
|
||||
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
|
||||
func (d *Detect) smartctlScan() ([]models.Device, error) {
|
||||
//we use smartctl to detect all the drives available.
|
||||
detectedDeviceConnJson, err := common.ExecCmd(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Error scanning for devices: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var detectedDeviceConns models.Scan
|
||||
err = json.Unmarshal([]byte(detectedDeviceConnJson), &detectedDeviceConns)
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Error decoding detected devices: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
detectedDevices := []models.Device{}
|
||||
|
||||
for _, detectedDevice := range detectedDeviceConns.Devices {
|
||||
detectedDevices = append(detectedDevices, models.Device{
|
||||
DeviceType: detectedDevice.Type,
|
||||
DeviceName: strings.TrimPrefix(detectedDevice.Name, DevicePrefix()),
|
||||
})
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//updates a device model with information from smartctl --scan
|
||||
// It has a couple of issues however:
|
||||
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
|
||||
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
|
||||
func (d *Detect) smartCtlInfo(device *models.Device) error {
|
||||
|
||||
args := []string{"--info", "-j"}
|
||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||
if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
|
||||
args = append(args, "-d", device.DeviceType)
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName))
|
||||
|
||||
availableDeviceInfoJson, err := common.ExecCmd(d.Logger, "smartctl", args, "", os.Environ())
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Could not retrieve device information for %s: %v", device.DeviceName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var availableDeviceInfo collector.SmartInfo
|
||||
err = json.Unmarshal([]byte(availableDeviceInfoJson), &availableDeviceInfo)
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Could not decode device information for %s: %v", device.DeviceName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
//DeviceType and DeviceName are already populated.
|
||||
//WWN
|
||||
//InterfaceType:
|
||||
device.ModelName = availableDeviceInfo.ModelName
|
||||
device.InterfaceSpeed = availableDeviceInfo.InterfaceSpeed.Current.String
|
||||
device.SerialNumber = availableDeviceInfo.SerialNumber
|
||||
device.Firmware = availableDeviceInfo.FirmwareVersion
|
||||
device.RotationSpeed = availableDeviceInfo.RotationRate
|
||||
device.Capacity = availableDeviceInfo.UserCapacity.Bytes
|
||||
device.FormFactor = availableDeviceInfo.FormFactor.Name
|
||||
device.DeviceProtocol = availableDeviceInfo.Device.Protocol
|
||||
if len(availableDeviceInfo.Vendor) > 0 {
|
||||
device.Manufacturer = availableDeviceInfo.Vendor
|
||||
}
|
||||
|
||||
//populate WWN is possible if present
|
||||
if availableDeviceInfo.Wwn.Naa != 0 { //valid values are 1-6 (5 is what we handle correctly)
|
||||
d.Logger.Info("Generating WWN")
|
||||
wwn := Wwn{
|
||||
Naa: availableDeviceInfo.Wwn.Naa,
|
||||
Oui: availableDeviceInfo.Wwn.Oui,
|
||||
Id: availableDeviceInfo.Wwn.ID,
|
||||
}
|
||||
device.WWN = wwn.ToString()
|
||||
d.Logger.Debugf("NAA: %d OUI: %d Id: %d => WWN: %s", wwn.Naa, wwn.Oui, wwn.Id, device.WWN)
|
||||
} else {
|
||||
d.Logger.Info("Using WWN Fallback")
|
||||
d.wwnFallback(device)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//uses https://github.com/denisbrodbeck/machineid to get a OS specific unique machine ID.
|
||||
func (d *Detect) getMachineId() (string, error) {
|
||||
return machineid.ProtectedID("scrutiny")
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
return "/dev/"
|
||||
}
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
// call the base/common functionality to get a list of devicess
|
||||
detectedDevices, err := d.smartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//smartctl --scan doesn't seem to detect mac nvme drives, lets see if we can detect them manually.
|
||||
missingDevices, err := d.findMissingDevices(detectedDevices) //we dont care about the error here, just continue retrieving device info.
|
||||
if err == nil {
|
||||
detectedDevices = append(detectedDevices, missingDevices...)
|
||||
}
|
||||
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]models.Device, error) {
|
||||
|
||||
missingDevices := []models.Device{}
|
||||
|
||||
block, err := ghw.Block()
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Error getting block storage info: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, disk := range block.Disks {
|
||||
|
||||
// ignore optical drives and floppy disks
|
||||
if disk.DriveType == ghw.DRIVE_TYPE_FDD || disk.DriveType == ghw.DRIVE_TYPE_ODD {
|
||||
d.Logger.Debugf(" => Ignore: Optical or floppy disk - (found %s)\n", disk.DriveType.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore removable disks
|
||||
if disk.IsRemovable {
|
||||
d.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 {
|
||||
d.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 {
|
||||
d.Logger.Debugf(" => Ignore: Unknown storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
//check if device is already detected.
|
||||
alreadyDetected := false
|
||||
diskName := strings.TrimPrefix(disk.Name, DevicePrefix())
|
||||
for _, detectedDevice := range detectedDevices {
|
||||
|
||||
if detectedDevice.DeviceName == diskName {
|
||||
alreadyDetected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyDetected {
|
||||
missingDevices = append(missingDevices, models.Device{
|
||||
DeviceName: diskName,
|
||||
DeviceType: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
return missingDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
block, err := ghw.Block()
|
||||
if err == nil {
|
||||
for _, disk := range block.Disks {
|
||||
if disk.Name == detectedDevice.DeviceName {
|
||||
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
|
||||
detectedDevice.WWN = disk.WWN
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
return "/dev/"
|
||||
}
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
// call the base/common functionality to get a list of devices
|
||||
detectedDevices, err := d.smartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
block, err := ghw.Block()
|
||||
if err == nil {
|
||||
for _, disk := range block.Disks {
|
||||
if disk.Name == detectedDevice.DeviceName {
|
||||
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
|
||||
detectedDevice.WWN = disk.WWN
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package detect
|
||||
|
||||
func DevicePrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
// call the base/common functionality to get a list of devices
|
||||
detectedDevices, err := d.smartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
|
||||
//fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Wwn struct {
|
||||
Naa uint64 `json:"naa"`
|
||||
Oui uint64 `json:"oui"`
|
||||
Id uint64 `json:"id"`
|
||||
VendorCode string `json:"vendor_code"`
|
||||
}
|
||||
|
||||
// this is an incredibly basic converter, that only works for "Registered" IEEE format - NAA5
|
||||
// https://standards.ieee.org/content/dam/ieee-standards/standards/web/documents/tutorials/fibre.pdf
|
||||
// references:
|
||||
// - https://metacpan.org/pod/Device::WWN
|
||||
// - https://en.wikipedia.org/wiki/World_Wide_Name
|
||||
// - https://storagemeat.blogspot.com/2012/08/decoding-wwids-or-how-to-tell-whats-what.html
|
||||
// - https://bryanchain.com/2016/01/20/breaking-down-an-naa-id-world-wide-name/
|
||||
|
||||
/*
|
||||
+----------+---+---+---+---+---+---+---+---+
|
||||
| Byte/Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|
||||
+----------+---+---+---+---+---+---+---+---+
|
||||
| 0 | NAA (5h) | (MSB) |
|
||||
+----------+---------------+ +
|
||||
| 1 | |
|
||||
+----------+ IEEE OUI |
|
||||
| 2 | |
|
||||
+----------+ +---------------+
|
||||
| 3 | (LSB) | (MSB) |
|
||||
+----------+---------------+ +
|
||||
| 4 | |
|
||||
| | |
|
||||
+----------+ |
|
||||
| 5 | Vendor ID |
|
||||
+----------+ |
|
||||
| 6 | |
|
||||
+----------+ |
|
||||
| 7 | (LSB) |
|
||||
+----------+-------------------------------+
|
||||
|
||||
|
||||
*/
|
||||
|
||||
func (wwn *Wwn) ToString() string {
|
||||
|
||||
var wwnBuffer uint64
|
||||
|
||||
wwnBuffer = wwn.Id //start with vendor ID
|
||||
wwnBuffer += (wwn.Oui << 36) //add left-shifted OUI
|
||||
wwnBuffer += (wwn.Naa << 60) //NAA is a number from 1-6, so decimal == hex.
|
||||
|
||||
//TODO: may need to support additional versions in the future.
|
||||
|
||||
return fmt.Sprintf("%#x", wwnBuffer)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package detect_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWwn_FromStringTable(t *testing.T) {
|
||||
|
||||
//setup
|
||||
var tests = []struct {
|
||||
wwnStr string
|
||||
wwn detect.Wwn
|
||||
}{
|
||||
|
||||
{"0x5002538e40a22954", detect.Wwn{Naa: 5, Oui: 9528, Id: 61213911380}}, //sda
|
||||
{"0x5000cca264eb01d7", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283057623}}, //sdb
|
||||
{"0x5000cca264ec3183", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283135363}}, //sdc
|
||||
{"0x5000cca252c859cc", detect.Wwn{Naa: 5, Oui: 3274, Id: 9978796492}}, //sdd
|
||||
{"0x50014ee20b2a72a9", detect.Wwn{Naa: 5, Oui: 5358, Id: 8777265833}}, //sde
|
||||
{"0x5000cca264ebc248", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283106888}}, //sdf
|
||||
{"0x5000c500673e6b5f", detect.Wwn{Naa: 5, Oui: 3152, Id: 1732143967}}, //sdg
|
||||
}
|
||||
//test
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%s", tt.wwnStr)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
str := tt.wwn.ToString()
|
||||
require.Equal(t, tt.wwnStr, str)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
{
|
||||
"json_format_version": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"smartctl": {
|
||||
"version": [
|
||||
7,
|
||||
0
|
||||
],
|
||||
"svn_revision": "4883",
|
||||
"platform_info": "x86_64-linux-4.19.107-Unraid",
|
||||
"build_info": "(local build)",
|
||||
"argv": [
|
||||
"smartctl",
|
||||
"-a",
|
||||
"-j",
|
||||
"-d",
|
||||
"nvme",
|
||||
"/dev/nvme0"
|
||||
],
|
||||
"exit_status": 0
|
||||
},
|
||||
"device": {
|
||||
"name": "/dev/nvme0",
|
||||
"info_name": "/dev/nvme0",
|
||||
"type": "nvme",
|
||||
"protocol": "NVMe"
|
||||
},
|
||||
"model_name": "Force MP510",
|
||||
"serial_number": "yes",
|
||||
"firmware_version": "ECFM12.3",
|
||||
"nvme_pci_vendor": {
|
||||
"id": 6535,
|
||||
"subsystem_id": 6535
|
||||
},
|
||||
"nvme_ieee_oui_identifier": 6584743,
|
||||
"nvme_total_capacity": 480103981056,
|
||||
"nvme_unallocated_capacity": 0,
|
||||
"nvme_controller_id": 1,
|
||||
"nvme_number_of_namespaces": 1,
|
||||
"nvme_namespaces": [
|
||||
{
|
||||
"id": 1,
|
||||
"size": {
|
||||
"blocks": 937703088,
|
||||
"bytes": 480103981056
|
||||
},
|
||||
"capacity": {
|
||||
"blocks": 937703088,
|
||||
"bytes": 480103981056
|
||||
},
|
||||
"utilization": {
|
||||
"blocks": 937703088,
|
||||
"bytes": 480103981056
|
||||
},
|
||||
"formatted_lba_size": 512,
|
||||
"eui64": {
|
||||
"oui": 6584743,
|
||||
"ext_id": 171819811633
|
||||
}
|
||||
}
|
||||
],
|
||||
"user_capacity": {
|
||||
"blocks": 937703088,
|
||||
"bytes": 480103981056
|
||||
},
|
||||
"logical_block_size": 512,
|
||||
"local_time": {
|
||||
"time_t": 1600619090,
|
||||
"asctime": "Sun Sep 20 16:24:50 2020 Europe"
|
||||
},
|
||||
"smart_status": {
|
||||
"passed": true,
|
||||
"nvme": {
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"nvme_smart_health_information_log": {
|
||||
"critical_warning": 0,
|
||||
"temperature": 38,
|
||||
"available_spare": 100,
|
||||
"available_spare_threshold": 5,
|
||||
"percentage_used": 1,
|
||||
"data_units_read": 6932144,
|
||||
"data_units_written": 16093122,
|
||||
"host_reads": 29878811,
|
||||
"host_writes": 17533252,
|
||||
"controller_busy_time": 305,
|
||||
"power_cycles": 4,
|
||||
"power_on_hours": 6487,
|
||||
"unsafe_shutdowns": 4,
|
||||
"media_errors": 0,
|
||||
"num_err_log_entries": 8382,
|
||||
"warning_temp_time": 0,
|
||||
"critical_comp_time": 0
|
||||
},
|
||||
"temperature": {
|
||||
"current": 38
|
||||
},
|
||||
"power_cycle_count": 4,
|
||||
"power_on_time": {
|
||||
"hours": 6487
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"wwn": "a4c8e8ed-11a0-4c97-9bba-306440f1b944",
|
||||
"device_name": "nvme0",
|
||||
"manufacturer": "",
|
||||
"model_name": "Force MP510",
|
||||
"interface_type": "",
|
||||
"interface_speed": "",
|
||||
"serial_number": "a4c8e8ed-11a0-4c97-9bba-306440f1b944",
|
||||
"firmware": "ECFM12.3",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 480103981056,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "NVMe",
|
||||
"device_type": "nvme"
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
Loading…
Reference in new issue