Merge branch 'beta' into dark-mode

pull/276/head
Jason Kulatunga 2 years ago committed by GitHub
commit 9e0ba4d269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,20 +22,19 @@ See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLL
``` ```
docker run -it --rm -p 8080:8080 \ docker run -it --rm -p 8080:8080 \
-v `pwd`/config:/opt/scrutiny/config \
-v /run/udev:/run/udev:ro \ -v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \ --cap-add SYS_RAWIO \
--device=/dev/sda \ --device=/dev/sda \
--device=/dev/sdb \ --device=/dev/sdb \
-e DEBUG=true \ -e DEBUG=true \
-e COLLECTOR_LOG_FILE=/tmp/collector.log \ -e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \
-e SCRUTINY_LOG_FILE=/tmp/web.log \ -e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \
--name scrutiny \ --name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus ghcr.io/analogj/scrutiny:master-omnibus
# in another terminal trigger the collector # in another terminal trigger the collector
docker exec scrutiny scrutiny-collector-metrics run docker exec scrutiny scrutiny-collector-metrics run
# then use docker cp to copy the log files out of the container.
docker cp scrutiny:/tmp/collector.log collector.log
docker cp scrutiny:/tmp/web.log web.log
``` ```
The log files will be available on your host in the `config` directory. Please attach them to this issue.

@ -60,8 +60,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # cache-from: type=gha
cache-to: type=gha,mode=max # cache-to: type=gha,mode=max
web: web:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -110,8 +110,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # cache-from: type=gha
cache-to: type=gha,mode=max # cache-to: type=gha,mode=max
omnibus: omnibus:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -157,5 +157,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # cache-from: type=gha
cache-to: type=gha,mode=max # cache-to: type=gha,mode=max

@ -169,3 +169,19 @@ docker run -it --rm -p 8080:8080 \
ghcr.io/analogj/scrutiny:master-omnibus ghcr.io/analogj/scrutiny:master-omnibus
/opt/scrutiny/bin/scrutiny-collector-metrics run /opt/scrutiny/bin/scrutiny-collector-metrics run
``` ```
# Running Tests
```bash
docker run -p 8086:8086 -d --rm \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
influxdb:2.2
go test ./...
```

@ -38,5 +38,17 @@ windows/amd64:
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))" @echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/ xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
docker-collector:
@echo "building collector docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
docker-web:
@echo "building web docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
docker-omnibus:
@echo "building omnibus docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
# clean: # clean:
# rm scrutiny-collector-metrics-* scrutiny-web-* # rm scrutiny-collector-metrics-* scrutiny-web-*

@ -116,12 +116,13 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
} }
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName) mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
args := []string{"-x", "-j"} fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
//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. //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(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" { if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
args = append(args, "-d", deviceType) args = append(args, "--device", deviceType)
} }
args = append(args, fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)) args = append(args, fullDeviceName)
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ()) result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
resultBytes := []byte(result) resultBytes := []byte(result)

@ -1,6 +1,7 @@
package config package config
import ( import (
"fmt"
"github.com/analogj/go-util/utils" "github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
@ -8,6 +9,8 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"log" "log"
"os" "os"
"sort"
"strings"
) )
// When initializing this class the following methods must be called: // When initializing this class the following methods must be called:
@ -16,6 +19,8 @@ import (
// This is done automatically when created via the Factory. // This is done automatically when created via the Factory.
type configuration struct { type configuration struct {
*viper.Viper *viper.Viper
deviceOverrides []models.ScanOverride
} }
//Viper uses the following precedence order. Each item takes precedence over the item below it: //Viper uses the following precedence order. Each item takes precedence over the item below it:
@ -38,6 +43,10 @@ func (c *configuration) Init() error {
c.SetDefault("api.endpoint", "http://localhost:8080") c.SetDefault("api.endpoint", "http://localhost:8080")
c.SetDefault("commands.metrics_scan_args", "--scan --json")
c.SetDefault("commands.metrics_info_args", "--info --json")
c.SetDefault("commands.metrics_smart_args", "--xall --json")
//c.SetDefault("collect.short.command", "-a -o on -S on") //c.SetDefault("collect.short.command", "-a -o on -S on")
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig //if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
@ -90,16 +99,89 @@ func (c *configuration) ValidateConfig() error {
// check that device prefix matches OS // check that device prefix matches OS
// check that schema of config file is valid // check that schema of config file is valid
return nil // check that the collector commands are valid
commandArgStrings := map[string]string{
"commands.metrics_scan_args": c.GetString("commands.metrics_scan_args"),
"commands.metrics_info_args": c.GetString("commands.metrics_info_args"),
"commands.metrics_smart_args": c.GetString("commands.metrics_smart_args"),
}
errorStrings := []string{}
for configKey, commandArgString := range commandArgStrings {
args := strings.Split(commandArgString, " ")
//ensure that the args string contains `--json` or `-j` flag
containsJsonFlag := false
containsDeviceFlag := false
for _, flag := range args {
if strings.HasPrefix(flag, "--json") || strings.HasPrefix(flag, "-j") {
containsJsonFlag = true
}
if strings.HasPrefix(flag, "--device") || strings.HasPrefix(flag, "-d") {
containsDeviceFlag = true
}
}
if !containsJsonFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' is missing '--json' flag", configKey))
}
if containsDeviceFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' must not contain '--device' or '-d' flag", configKey))
}
}
//sort(errorStrings)
sort.Strings(errorStrings)
if len(errorStrings) == 0 {
return nil
} else {
return errors.ConfigValidationError(strings.Join(errorStrings, ", "))
}
} }
func (c *configuration) GetScanOverrides() []models.ScanOverride { func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
// we have to support 2 types of device types. // we have to support 2 types of device types.
// - simple device type (device_type: 'sat') // - simple device type (device_type: 'sat')
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2) // and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
// GetString will return "" if this is a list of device types. // GetString will return "" if this is a list of device types.
overrides := []models.ScanOverride{} if c.deviceOverrides == nil {
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true }) overrides := []models.ScanOverride{}
return overrides c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
c.deviceOverrides = overrides
}
return c.deviceOverrides
}
func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
return deviceOverrides.Commands.MetricsInfoArgs
} else {
return c.GetString("commands.metrics_info_args")
}
}
}
return c.GetString("commands.metrics_info_args")
}
func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
return deviceOverrides.Commands.MetricsSmartArgs
} else {
return c.GetString("commands.metrics_smart_args")
}
}
}
return c.GetString("commands.metrics_smart_args")
} }

@ -30,7 +30,7 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
//test //test
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml")) err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
require.NoError(t, err, "should correctly load simple device config") require.NoError(t, err, "should correctly load simple device config")
scanOverrides := testConfig.GetScanOverrides() scanOverrides := testConfig.GetDeviceOverrides()
//assert //assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides) require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
@ -45,7 +45,7 @@ func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
//test //test
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml")) err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config") require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetScanOverrides() scanOverrides := testConfig.GetDeviceOverrides()
//assert //assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides) require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
@ -60,7 +60,7 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
//test //test
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml")) err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config") require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetScanOverrides() scanOverrides := testConfig.GetDeviceOverrides()
//assert //assert
require.Equal(t, []models.ScanOverride{ require.Equal(t, []models.ScanOverride{
@ -75,3 +75,53 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
Ignore: false, Ignore: false,
}}, scanOverrides) }}, scanOverrides)
} }
func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_missing_json.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_scan_args' is missing '--json' flag"`, "should throw an error because json flag is missing")
}
func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_includes_device.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_info_args' must not contain '--device' or '-d' flag, configuration key 'commands.metrics_smart_args' must not contain '--device' or '-d' flag"`, "should throw an error because device flags detected")
}
func TestConfiguration_OverrideCommands(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_commands.yaml"))
require.NoError(t, err, "should not throw an error")
require.Equal(t, "--xall --json -T permissive", testConfig.GetString("commands.metrics_smart_args"))
}
func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml"))
require.NoError(t, err, "should correctly override device command")
//assert
require.Equal(t, "--info --json -T permissive", testConfig.GetCommandMetricsInfoArgs("/dev/sda"))
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
}

@ -22,5 +22,7 @@ type Interface interface {
GetStringSlice(key string) []string GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
GetScanOverrides() []models.ScanOverride GetDeviceOverrides() []models.ScanOverride
GetCommandMetricsInfoArgs(deviceName string) string
GetCommandMetricsSmartArgs(deviceName string) string
} }

@ -5,144 +5,121 @@
package mock_config package mock_config
import ( import (
reflect "reflect"
models "github.com/analogj/scrutiny/collector/pkg/models" models "github.com/analogj/scrutiny/collector/pkg/models"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper" viper "github.com/spf13/viper"
reflect "reflect"
) )
// MockInterface is a mock of Interface interface // MockInterface is a mock of Interface interface.
type MockInterface struct { type MockInterface struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder recorder *MockInterfaceMockRecorder
} }
// MockInterfaceMockRecorder is the mock recorder for MockInterface // MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct { type MockInterfaceMockRecorder struct {
mock *MockInterface mock *MockInterface
} }
// NewMockInterface creates a new mock instance // NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface { func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl} mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock} mock.recorder = &MockInterfaceMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder return m.recorder
} }
// Init mocks base method // AllSettings mocks base method.
func (m *MockInterface) Init() error { func (m *MockInterface) AllSettings() map[string]interface{} {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Init") ret := m.ctrl.Call(m, "AllSettings")
ret0, _ := ret[0].(error) ret0, _ := ret[0].(map[string]interface{})
return ret0 return ret0
} }
// Init indicates an expected call of Init // AllSettings indicates an expected call of AllSettings.
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call { func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
} }
// ReadConfig mocks base method // Get mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error { func (m *MockInterface) Get(key string) interface{} {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadConfig", configFilePath) ret := m.ctrl.Call(m, "Get", key)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(interface{})
return ret0 return ret0
} }
// ReadConfig indicates an expected call of ReadConfig // Get indicates an expected call of Get.
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
}
// Set mocks base method
func (m *MockInterface) Set(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", key, value)
}
// Set indicates an expected call of Set
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
}
// SetDefault mocks base method
func (m *MockInterface) SetDefault(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetDefault", key, value)
}
// SetDefault indicates an expected call of SetDefault
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
} }
// AllSettings mocks base method // GetBool mocks base method.
func (m *MockInterface) AllSettings() map[string]interface{} { func (m *MockInterface) GetBool(key string) bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AllSettings") ret := m.ctrl.Call(m, "GetBool", key)
ret0, _ := ret[0].(map[string]interface{}) ret0, _ := ret[0].(bool)
return ret0 return ret0
} }
// AllSettings indicates an expected call of AllSettings // GetBool indicates an expected call of GetBool.
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call { func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
} }
// IsSet mocks base method // GetCommandMetricsInfoArgs mocks base method.
func (m *MockInterface) IsSet(key string) bool { func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSet", key) ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName)
ret0, _ := ret[0].(bool) ret0, _ := ret[0].(string)
return ret0 return ret0
} }
// IsSet indicates an expected call of IsSet // GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs.
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsInfoArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsInfoArgs), deviceName)
} }
// Get mocks base method // GetCommandMetricsSmartArgs mocks base method.
func (m *MockInterface) Get(key string) interface{} { func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key) ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName)
ret0, _ := ret[0].(interface{}) ret0, _ := ret[0].(string)
return ret0 return ret0
} }
// Get indicates an expected call of Get // GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs.
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsSmartArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsSmartArgs), deviceName)
} }
// GetBool mocks base method // GetDeviceOverrides mocks base method.
func (m *MockInterface) GetBool(key string) bool { func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", key) ret := m.ctrl.Call(m, "GetDeviceOverrides")
ret0, _ := ret[0].(bool) ret0, _ := ret[0].([]models.ScanOverride)
return ret0 return ret0
} }
// GetBool indicates an expected call of GetBool // GetDeviceOverrides indicates an expected call of GetDeviceOverrides.
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceOverrides", reflect.TypeOf((*MockInterface)(nil).GetDeviceOverrides))
} }
// GetInt mocks base method // GetInt mocks base method.
func (m *MockInterface) GetInt(key string) int { func (m *MockInterface) GetInt(key string) int {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", key) ret := m.ctrl.Call(m, "GetInt", key)
@ -150,13 +127,13 @@ func (m *MockInterface) GetInt(key string) int {
return ret0 return ret0
} }
// GetInt indicates an expected call of GetInt // GetInt indicates an expected call of GetInt.
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
} }
// GetString mocks base method // GetString mocks base method.
func (m *MockInterface) GetString(key string) string { func (m *MockInterface) GetString(key string) string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetString", key) ret := m.ctrl.Call(m, "GetString", key)
@ -164,13 +141,13 @@ func (m *MockInterface) GetString(key string) string {
return ret0 return ret0
} }
// GetString indicates an expected call of GetString // GetString indicates an expected call of GetString.
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
} }
// GetStringSlice mocks base method // GetStringSlice mocks base method.
func (m *MockInterface) GetStringSlice(key string) []string { func (m *MockInterface) GetStringSlice(key string) []string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStringSlice", key) ret := m.ctrl.Call(m, "GetStringSlice", key)
@ -178,13 +155,79 @@ func (m *MockInterface) GetStringSlice(key string) []string {
return ret0 return ret0
} }
// GetStringSlice indicates an expected call of GetStringSlice // GetStringSlice indicates an expected call of GetStringSlice.
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
} }
// UnmarshalKey mocks base method // Init mocks base method.
func (m *MockInterface) Init() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Init")
ret0, _ := ret[0].(error)
return ret0
}
// Init indicates an expected call of Init.
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
}
// IsSet mocks base method.
func (m *MockInterface) IsSet(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSet", key)
ret0, _ := ret[0].(bool)
return ret0
}
// IsSet indicates an expected call of IsSet.
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
}
// ReadConfig mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
ret0, _ := ret[0].(error)
return ret0
}
// ReadConfig indicates an expected call of ReadConfig.
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
}
// Set mocks base method.
func (m *MockInterface) Set(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", key, value)
}
// Set indicates an expected call of Set.
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
}
// SetDefault mocks base method.
func (m *MockInterface) SetDefault(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetDefault", key, value)
}
// SetDefault indicates an expected call of SetDefault.
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
}
// UnmarshalKey mocks base method.
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error { func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{key, rawVal} varargs := []interface{}{key, rawVal}
@ -196,23 +239,9 @@ func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts
return ret0 return ret0
} }
// UnmarshalKey indicates an expected call of UnmarshalKey // UnmarshalKey indicates an expected call of UnmarshalKey.
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call { func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{key, rawVal}, decoderOpts...) varargs := append([]interface{}{key, rawVal}, decoderOpts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
} }
// GetScanOverrides mocks base method
func (m *MockInterface) GetScanOverrides() []models.ScanOverride {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetScanOverrides")
ret0, _ := ret[0].([]models.ScanOverride)
return ret0
}
// GetScanOverrides indicates an expected call of GetScanOverrides
func (mr *MockInterfaceMockRecorder) GetScanOverrides() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScanOverrides", reflect.TypeOf((*MockInterface)(nil).GetScanOverrides))
}

@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device.

@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.

@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.

@ -0,0 +1,5 @@
version: 1
devices:
- device: /dev/sda
commands:
metrics_info_args: "--info --json -T permissive"

@ -28,7 +28,8 @@ type Detect struct {
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file). // 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) { func (d *Detect) SmartctlScan() ([]models.Device, error) {
//we use smartctl to detect all the drives available. //we use smartctl to detect all the drives available.
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ()) args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
if err != nil { if err != nil {
d.Logger.Errorf("Error scanning for devices: %v", err) d.Logger.Errorf("Error scanning for devices: %v", err)
return nil, err return nil, err
@ -51,13 +52,13 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves // - 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. // - 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 { func (d *Detect) SmartCtlInfo(device *models.Device) error {
fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)
args := []string{"--info", "-j"} args := strings.Split(d.Config.GetCommandMetricsInfoArgs(fullDeviceName), " ")
//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. //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" { if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
args = append(args, "-d", device.DeviceType) args = append(args, "--device", device.DeviceType)
} }
args = append(args, fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)) args = append(args, fullDeviceName)
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ()) availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
if err != nil { if err != nil {
@ -138,7 +139,7 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
//now tha we've "grouped" all the devices, lets override any groups specified in the config file. //now tha we've "grouped" all the devices, lets override any groups specified in the config file.
for _, overrideDevice := range d.Config.GetScanOverrides() { for _, overrideDevice := range d.Config.GetDeviceOverrides() {
overrideDeviceFile := strings.ToLower(overrideDevice.Device) overrideDeviceFile := strings.ToLower(overrideDevice.Device)
if overrideDevice.Ignore { if overrideDevice.Ignore {

@ -18,7 +18,8 @@ func TestDetect_SmartctlScan(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{}) fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl) fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json") testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
@ -45,7 +46,8 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{}) fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl) fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json") testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
@ -75,7 +77,8 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{}) fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl) fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json") testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
@ -104,7 +107,9 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{}) fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{ detectedDevices := models.Scan{
Devices: []models.ScanDevice{ Devices: []models.ScanDevice{
{ {
@ -134,7 +139,9 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}) fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{ detectedDevices := models.Scan{
Devices: []models.ScanDevice{ Devices: []models.ScanDevice{
{ {
@ -163,7 +170,8 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{ fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
{ {
Device: "/dev/bus/0", Device: "/dev/bus/0",
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"}, DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
@ -202,7 +210,8 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("") fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}}) fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
detectedDevices := models.Scan{ detectedDevices := models.Scan{
Devices: []models.ScanDevice{ Devices: []models.ScanDevice{
{ {

@ -4,4 +4,8 @@ type ScanOverride struct {
Device string `mapstructure:"device"` Device string `mapstructure:"device"`
DeviceType []string `mapstructure:"type"` DeviceType []string `mapstructure:"type"`
Ignore bool `mapstructure:"ignore"` Ignore bool `mapstructure:"ignore"`
Commands struct {
MetricsInfoArgs string `mapstructure:"metrics_info_args"`
MetricsSmartArgs string `mapstructure:"metrics_smart_args"`
} `mapstructure:"commands"`
} }

@ -26,24 +26,25 @@ RUN npm install -g @angular/cli@9.1.4 && \
######## ########
FROM ubuntu:bionic as runtime FROM ubuntu:latest as runtime
ARG TARGETARCH ARG TARGETARCH
EXPOSE 8080 EXPOSE 8080
WORKDIR /opt/scrutiny WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}" ENV PATH="/opt/scrutiny/bin:${PATH}"
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates curl tzdata \ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
&& update-ca-certificates \ && update-ca-certificates \
&& case ${TARGETARCH} in \ && case ${TARGETARCH} in \
"amd64") S6_ARCH=amd64 ;; \ "amd64") S6_ARCH=amd64 ;; \
"arm64") S6_ARCH=aarch64 ;; \ "arm64") S6_ARCH=aarch64 ;; \
esac \ esac \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \ && curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / --exclude="./bin" \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C /usr ./bin \
ADD https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb /tmp/ && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
RUN dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
COPY /rootfs / COPY /rootfs /

@ -10,11 +10,11 @@ RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
######## ########
FROM ubuntu:bionic as runtime FROM ubuntu:latest as runtime
WORKDIR /scrutiny WORKDIR /scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}" ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates tzdata && update-ca-certificates RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny

@ -24,7 +24,7 @@ RUN npm install -g @angular/cli@9.1.4 && \
######## ########
FROM ubuntu:bionic as runtime FROM ubuntu:latest as runtime
EXPOSE 8080 EXPOSE 8080
WORKDIR /opt/scrutiny WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}" ENV PATH="/opt/scrutiny/bin:${PATH}"

@ -115,7 +115,8 @@ Unlike the webapp, the collector does have some dependencies:
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient. Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
So you'll need to install the v7+ version using one of the following commands: So you'll need to install the v7+ version using one of the following commands:
- **Ubuntu:** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1` - **Ubuntu (22.04/Jammy/LTS):** `apt-get install -y smartmontools`
- **Ubuntu (18.04/Bionic):** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
- **Centos8:** - **Centos8:**
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm` - `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
- `dnf install smartmontools` - `dnf install smartmontools`

@ -113,12 +113,60 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
### ATA ### ATA
### Standby/Sleeping Disks ### Exit Codes
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
but you can look at the table (and associated links) below to debug `smartctl`.
> smartctl Return Values
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
> for ATA disks; some of these values may also be returned for SCSI disks.
>
> source: http://www.linuxguide.it/command_line/linux-manpage/do.php?file=smartctl#sect7
| Exit Code (Isolated) | Binary | Problem Message |
| --- | --- | --- |
| 1 | Bit 0 | Command line did not parse. |
| 2 | Bit 1 | Device open failed, or device did not return an IDENTIFY DEVICE structure. |
| 4 | Bit 2 | Some SMART command to the disk failed, or there was a checksum error in a SMART data structure (see В´-bВ´ option above). |
| 8 | Bit 3 | SMART status check returned “DISK FAILING". |
| 16 | Bit 4 | We found prefail Attributes <= threshold. |
| 32 | Bit 5 | SMART status check returned “DISK OK” but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past. |
| 64 | Bit 6 | The device error log contains records of errors. |
| 128 | Bit 7 | The device self-test log contains records of errors. |
#### Standby/Sleeping Disks
Disks in Standby/Sleep can also cause `smartctl` to exit abnormally, usually with `exit code: 2`.
- https://github.com/AnalogJ/scrutiny/issues/221 - https://github.com/AnalogJ/scrutiny/issues/221
- https://github.com/AnalogJ/scrutiny/issues/157 - https://github.com/AnalogJ/scrutiny/issues/157
### Volume Mount All Devices (`/dev`) - Privileged ### Volume Mount All Devices (`/dev`) - Privileged
> WARNING: This is an insecure/dangerous workaround. Running Scrutiny (or any Docker image) with `--privileged` is equivalent to running it with root access.
If you have exhausted all other mechanisms to get your disks working with `smartctl` running within a container, you can try running the docker image with the following additional flags:
- `--privileged` (instead of `--cap-add`) - this gives the docker container full access to your system. Scrutiny does not require this permission, however it can be helpful for `smartctl`
- `-v /dev:/dev:ro` (instead of `--device`) - this mounts the `/dev` folder (containing all your device files) into the container, allowing `smartctl` to see your disks, exactly as if it were running on your host directly.
With this workaround your `docker run` command would look similar to the following:
```bash
docker run -it --rm -p 8080:8080 -p 8086:8086 \
-v `pwd`/scrutiny:/opt/scrutiny/config \
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
-v /run/udev:/run/udev:ro \
--privileged \
-v /dev:/dev \
--name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus
```
## Scrutiny detects Failure but SMART Passed? ## Scrutiny detects Failure but SMART Passed?

@ -53,6 +53,13 @@ devices:
# - 3ware,3 # - 3ware,3
# - 3ware,4 # - 3ware,4
# - 3ware,5 # - 3ware,5
#
# # example to show how to override the smartctl command args (per device), see below for how to override these globally.
# - device: /dev/sda
# commands:
# metrics_info_args: '--info --json -T permissive' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
#log: #log:
# file: '' #absolute or relative paths allowed, eg. web.log # file: '' #absolute or relative paths allowed, eg. web.log
@ -64,6 +71,12 @@ devices:
# if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint. # if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint.
# See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info, # See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info,
# example to show how to override the smartctl command args globally
#commands:
# metrics_scan_args: '--scan --json' # used to detect devices
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
######################################################################################################################## ########################################################################################################################
# FEATURES COMING SOON # FEATURES COMING SOON

@ -4,25 +4,34 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI" const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe" const DeviceProtocolNvme = "NVMe"
const SmartAttributeStatusPassed = 0 type AttributeStatus uint8
const SmartAttributeStatusFailed = 1
const SmartAttributeStatusWarning = 2
const SmartWhenFailedFailingNow = "FAILING_NOW" const (
const SmartWhenFailedInThePast = "IN_THE_PAST" // AttributeStatusPassed binary, 1,2,4,8,16,32,etc
AttributeStatusPassed AttributeStatus = 0
AttributeStatusFailedSmart AttributeStatus = 1
AttributeStatusWarningScrutiny AttributeStatus = 2
AttributeStatusFailedScrutiny AttributeStatus = 4
)
const AttributeWhenFailedFailingNow = "FAILING_NOW"
const AttributeWhenFailedInThePast = "IN_THE_PAST"
//const SmartStatusPassed = "passed" func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { return b | flag }
//const SmartStatusFailed = "failed" func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &^ flag }
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
type DeviceStatus int type DeviceStatus uint8
const ( const (
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
DeviceStatusPassed DeviceStatus = 0 DeviceStatusPassed DeviceStatus = 0
DeviceStatusFailedSmart DeviceStatus = iota DeviceStatusFailedSmart DeviceStatus = 1
DeviceStatusFailedScrutiny DeviceStatus = iota DeviceStatusFailedScrutiny DeviceStatus = 2
) )
func Set(b, flag DeviceStatus) DeviceStatus { return b | flag } func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
func Clear(b, flag DeviceStatus) DeviceStatus { return b &^ flag } func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
func Toggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag } func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
func Has(b, flag DeviceStatus) bool { return b&flag != 0 } func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }

@ -58,7 +58,7 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string
return device, fmt.Errorf("Could not get device from DB: %v", err) return device, fmt.Errorf("Could not get device from DB: %v", err)
} }
device.DeviceStatus = pkg.Set(device.DeviceStatus, status) device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
return device, sr.gormClient.Model(&device).Updates(device).Error return device, sr.gormClient.Model(&device).Updates(device).Error
} }

@ -21,9 +21,9 @@ type Device struct {
WWN string `json:"wwn" gorm:"primary_key"` WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"` DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"` DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"` DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"` DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"` ModelName string `json:"model_name"`
@ -166,7 +166,7 @@ func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
dv.DeviceProtocol = info.Device.Protocol dv.DeviceProtocol = info.Device.Protocol
if !info.SmartStatus.Passed { if !info.SmartStatus.Passed {
dv.DeviceStatus = pkg.Set(dv.DeviceStatus, pkg.DeviceStatusFailedSmart) dv.DeviceStatus = pkg.DeviceStatusSet(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
} }
return nil return nil

@ -110,7 +110,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
sm.PowerCycleCount = info.PowerCycleCount sm.PowerCycleCount = info.PowerCycleCount
sm.PowerOnHours = info.PowerOnTime.Hours sm.PowerOnHours = info.PowerOnTime.Hours
if !info.SmartStatus.Passed { if !info.SmartStatus.Passed {
sm.Status = pkg.DeviceStatusFailedSmart sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedSmart)
} }
sm.DeviceProtocol = info.Device.Protocol sm.DeviceProtocol = info.Device.Protocol
@ -148,8 +148,9 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
} }
attrModel.PopulateAttributeStatus() attrModel.PopulateAttributeStatus()
sm.Attributes[strconv.Itoa(collectorAttr.ID)] = &attrModel sm.Attributes[strconv.Itoa(collectorAttr.ID)] = &attrModel
if attrModel.Status == pkg.SmartAttributeStatusFailed {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny) if pkg.AttributeStatusHas(attrModel.Status, pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
} }
} }
} }
@ -178,8 +179,8 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
//find analyzed attribute status //find analyzed attribute status
for _, val := range sm.Attributes { for _, val := range sm.Attributes {
if val.GetStatus() == pkg.SmartAttributeStatusFailed { if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny) sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
} }
} }
} }
@ -204,8 +205,8 @@ func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog
//find analyzed attribute status //find analyzed attribute status
for _, val := range sm.Attributes { for _, val := range sm.Attributes {
if val.GetStatus() == pkg.SmartAttributeStatusFailed { if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny) sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
} }
} }
} }

@ -18,13 +18,13 @@ type SmartAtaAttribute struct {
WhenFailed string `json:"when_failed"` WhenFailed string `json:"when_failed"`
//Generated data //Generated data
TransformedValue int64 `json:"transformed_value"` TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"` Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"` StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"` FailureRate float64 `json:"failure_rate,omitempty"`
} }
func (sa *SmartAtaAttribute) GetStatus() int64 { func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status return sa.Status
} }
@ -77,7 +77,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
case "transformed_value": case "transformed_value":
sa.TransformedValue = val.(int64) sa.TransformedValue = val.(int64)
case "status": case "status":
sa.Status = val.(int64) sa.Status = val.(pkg.AttributeStatus)
case "status_reason": case "status_reason":
sa.StatusReason = val.(string) sa.StatusReason = val.(string)
case "failure_rate": case "failure_rate":
@ -89,16 +89,16 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
//populate attribute status, using SMART Thresholds & Observed Metadata //populate attribute status, using SMART Thresholds & Observed Metadata
// Chainable // Chainable
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute { func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedFailingNow { if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
//this attribute has previously failed //this attribute has previously failed
sa.Status = pkg.SmartAttributeStatusFailed sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedSmart)
sa.StatusReason = "Attribute is failing manufacturer SMART threshold" sa.StatusReason += "Attribute is failing manufacturer SMART threshold"
//if the Smart Status is failed, we should exit early, no need to look at thresholds. //if the Smart Status is failed, we should exit early, no need to look at thresholds.
return sa return sa
} else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast { } else if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedInThePast {
sa.Status = pkg.SmartAttributeStatusWarning sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason = "Attribute has previously failed manufacturer SMART threshold" sa.StatusReason += "Attribute has previously failed manufacturer SMART threshold"
} }
if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok { if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok {
@ -138,16 +138,16 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
if smartMetadata.Critical { if smartMetadata.Critical {
if obsThresh.AnnualFailureRate >= 0.10 { if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = pkg.SmartAttributeStatusFailed sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%" sa.StatusReason += "Observed Failure Rate for Critical Attribute is greater than 10%"
} }
} else { } else {
if obsThresh.AnnualFailureRate >= 0.20 { if obsThresh.AnnualFailureRate >= 0.20 {
sa.Status = pkg.SmartAttributeStatusFailed sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%" sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 20%"
} else if obsThresh.AnnualFailureRate >= 0.10 { } else if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = pkg.SmartAttributeStatusWarning sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%" sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 10%"
} }
} }
@ -157,7 +157,7 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
} }
// no bucket found // no bucket found
if smartMetadata.Critical { if smartMetadata.Critical {
sa.Status = pkg.SmartAttributeStatusWarning sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute" sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
} }

@ -1,7 +1,9 @@
package measurements package measurements
import "github.com/analogj/scrutiny/webapp/backend/pkg"
type SmartAttribute interface { type SmartAttribute interface {
Flatten() (fields map[string]interface{}) Flatten() (fields map[string]interface{})
Inflate(key string, val interface{}) Inflate(key string, val interface{})
GetStatus() int64 GetStatus() pkg.AttributeStatus
} }

@ -12,13 +12,13 @@ type SmartNvmeAttribute struct {
Value int64 `json:"value"` Value int64 `json:"value"`
Threshold int64 `json:"thresh"` Threshold int64 `json:"thresh"`
TransformedValue int64 `json:"transformed_value"` TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"` Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"` StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"` FailureRate float64 `json:"failure_rate,omitempty"`
} }
func (sa *SmartNvmeAttribute) GetStatus() int64 { func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status return sa.Status
} }
@ -54,7 +54,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
case "transformed_value": case "transformed_value":
sa.TransformedValue = val.(int64) sa.TransformedValue = val.(int64)
case "status": case "status":
sa.Status = val.(int64) sa.Status = val.(pkg.AttributeStatus)
case "status_reason": case "status_reason":
sa.StatusReason = val.(string) sa.StatusReason = val.(string)
case "failure_rate": case "failure_rate":
@ -72,8 +72,8 @@ func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold //check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) || if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) { (smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
sa.Status = pkg.SmartAttributeStatusFailed sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason = "Attribute is failing recommended SMART threshold" sa.StatusReason += "Attribute is failing recommended SMART threshold"
} }
} }
} }

@ -12,13 +12,13 @@ type SmartScsiAttribute struct {
Value int64 `json:"value"` Value int64 `json:"value"`
Threshold int64 `json:"thresh"` Threshold int64 `json:"thresh"`
TransformedValue int64 `json:"transformed_value"` TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"` Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"` StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"` FailureRate float64 `json:"failure_rate,omitempty"`
} }
func (sa *SmartScsiAttribute) GetStatus() int64 { func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status return sa.Status
} }
@ -54,7 +54,7 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
case "transformed_value": case "transformed_value":
sa.TransformedValue = val.(int64) sa.TransformedValue = val.(int64)
case "status": case "status":
sa.Status = val.(int64) sa.Status = val.(pkg.AttributeStatus)
case "status_reason": case "status_reason":
sa.StatusReason = val.(string) sa.StatusReason = val.(string)
case "failure_rate": case "failure_rate":
@ -73,7 +73,7 @@ func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold //check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) || if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) { (smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
sa.Status = pkg.SmartAttributeStatusFailed sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason = "Attribute is failing recommended SMART threshold" sa.StatusReason = "Attribute is failing recommended SMART threshold"
} }
} }

@ -77,7 +77,7 @@ func TestSmart_Flatten_ATA(t *testing.T) {
"attr.1.failure_rate": float64(0), "attr.1.failure_rate": float64(0),
"attr.1.raw_string": "0", "attr.1.raw_string": "0",
"attr.1.raw_value": int64(0), "attr.1.raw_value": int64(0),
"attr.1.status": int64(0), "attr.1.status": pkg.AttributeStatus(0),
"attr.1.status_reason": "", "attr.1.status_reason": "",
"attr.1.thresh": int64(1), "attr.1.thresh": int64(1),
"attr.1.transformed_value": int64(0), "attr.1.transformed_value": int64(0),
@ -89,7 +89,7 @@ func TestSmart_Flatten_ATA(t *testing.T) {
"attr.2.failure_rate": float64(0), "attr.2.failure_rate": float64(0),
"attr.2.raw_string": "108", "attr.2.raw_string": "108",
"attr.2.raw_value": int64(108), "attr.2.raw_value": int64(108),
"attr.2.status": int64(0), "attr.2.status": pkg.AttributeStatus(0),
"attr.2.status_reason": "", "attr.2.status_reason": "",
"attr.2.thresh": int64(54), "attr.2.thresh": int64(54),
"attr.2.transformed_value": int64(0), "attr.2.transformed_value": int64(0),
@ -130,7 +130,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
require.Equal(t, map[string]interface{}{ require.Equal(t, map[string]interface{}{
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0), "attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
"attr.read_errors_corrected_by_eccfast.status": int64(0), "attr.read_errors_corrected_by_eccfast.status": pkg.AttributeStatus(0),
"attr.read_errors_corrected_by_eccfast.status_reason": "", "attr.read_errors_corrected_by_eccfast.status_reason": "",
"attr.read_errors_corrected_by_eccfast.thresh": int64(0), "attr.read_errors_corrected_by_eccfast.thresh": int64(0),
"attr.read_errors_corrected_by_eccfast.transformed_value": int64(0), "attr.read_errors_corrected_by_eccfast.transformed_value": int64(0),
@ -168,7 +168,7 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
require.Equal(t, map[string]interface{}{ require.Equal(t, map[string]interface{}{
"attr.available_spare.attribute_id": "available_spare", "attr.available_spare.attribute_id": "available_spare",
"attr.available_spare.failure_rate": float64(0), "attr.available_spare.failure_rate": float64(0),
"attr.available_spare.status": int64(0), "attr.available_spare.status": pkg.AttributeStatus(0),
"attr.available_spare.status_reason": "", "attr.available_spare.status_reason": "",
"attr.available_spare.thresh": int64(0), "attr.available_spare.thresh": int64(0),
"attr.available_spare.transformed_value": int64(0), "attr.available_spare.transformed_value": int64(0),
@ -189,7 +189,7 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
"attr.1.failure_rate": float64(0), "attr.1.failure_rate": float64(0),
"attr.1.raw_string": "108", "attr.1.raw_string": "108",
"attr.1.raw_value": int64(108), "attr.1.raw_value": int64(108),
"attr.1.status": int64(0), "attr.1.status": pkg.AttributeStatus(0),
"attr.1.status_reason": "", "attr.1.status_reason": "",
"attr.1.thresh": int64(54), "attr.1.thresh": int64(54),
"attr.1.transformed_value": int64(0), "attr.1.transformed_value": int64(0),
@ -235,7 +235,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
"device_protocol": pkg.DeviceProtocolNvme, "device_protocol": pkg.DeviceProtocolNvme,
"attr.available_spare.attribute_id": "available_spare", "attr.available_spare.attribute_id": "available_spare",
"attr.available_spare.failure_rate": float64(0), "attr.available_spare.failure_rate": float64(0),
"attr.available_spare.status": int64(0), "attr.available_spare.status": pkg.AttributeStatus(0),
"attr.available_spare.status_reason": "", "attr.available_spare.status_reason": "",
"attr.available_spare.thresh": int64(0), "attr.available_spare.thresh": int64(0),
"attr.available_spare.transformed_value": int64(0), "attr.available_spare.transformed_value": int64(0),
@ -274,7 +274,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
"device_protocol": pkg.DeviceProtocolScsi, "device_protocol": pkg.DeviceProtocolScsi,
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0), "attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
"attr.read_errors_corrected_by_eccfast.status": int64(0), "attr.read_errors_corrected_by_eccfast.status": pkg.AttributeStatus(0),
"attr.read_errors_corrected_by_eccfast.status_reason": "", "attr.read_errors_corrected_by_eccfast.status_reason": "",
"attr.read_errors_corrected_by_eccfast.thresh": int64(0), "attr.read_errors_corrected_by_eccfast.thresh": int64(0),
"attr.read_errors_corrected_by_eccfast.transformed_value": int64(0), "attr.read_errors_corrected_by_eccfast.transformed_value": int64(0),
@ -328,9 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) {
require.Equal(t, 18, len(smartMdl.Attributes)) require.Equal(t, 18, len(smartMdl.Attributes))
//check that temperature was correctly parsed //check that temperature was correctly parsed
require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue) require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue)
require.Equal(t, int64(32), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).TransformedValue) require.Equal(t, int64(32), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).TransformedValue)
//ensure that Scrutiny warning for a non critical attribute does not set device status to failed.
require.Equal(t, pkg.AttributeStatusWarningScrutiny, smartMdl.Attributes["3"].GetStatus())
} }
func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) { func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
@ -402,7 +405,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["199"].GetStatus(), require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
"scrutiny should detect that %d failed (status: %d, %s)", "scrutiny should detect that %d failed (status: %d, %s)",
smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId, smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId,
smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason, smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason,
@ -435,7 +438,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["media_errors"].GetStatus(), require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
"scrutiny should detect that %s failed (status: %d, %s)", "scrutiny should detect that %s failed (status: %d, %s)",
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId, smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId,
smartMdl.Attributes["media_errors"].GetStatus(), smartMdl.Attributes["media_errors"].GetStatus(),

@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type AppEngine struct { type AppEngine struct {
@ -68,6 +69,11 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
} }
func (ae *AppEngine) Start() error { func (ae *AppEngine) Start() error {
//set the gin mode
gin.SetMode(gin.ReleaseMode)
if strings.ToLower(ae.Config.GetString("log.level")) == "debug" {
gin.SetMode(gin.DebugMode)
}
logger := logrus.New() logger := logrus.New()
//set default log level //set default log level

@ -28,7 +28,7 @@ export class TreoConfigService
if (localConfigStr){ if (localConfigStr){
//check localstorage for a value //check localstorage for a value
let localConfig = JSON.parse(localConfigStr) let localConfig = JSON.parse(localConfigStr)
currentScrutinyConfig = localConfig currentScrutinyConfig = Object.assign({}, localConfig, currentScrutinyConfig) // make sure defaults are available if missing from localStorage.
} }
currentScrutinyConfig.theme = this.determineTheme(currentScrutinyConfig); currentScrutinyConfig.theme = this.determineTheme(currentScrutinyConfig);

@ -71,6 +71,16 @@
<div>{{device?.host_id}}</div> <div>{{device?.host_id}}</div>
<div class="text-secondary text-md">Host ID</div> <div class="text-secondary text-md">Host ID</div>
</div> </div>
<div *ngIf="device?.device_uuid" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_uuid}}</div>
<div class="text-secondary text-md">Device UUID</div>
</div>
<div *ngIf="device?.device_label" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_label}}</div>
<div class="text-secondary text-md">Device Label</div>
</div>
<div *ngIf="device?.device_type && device?.device_type != 'ata' && device?.device_type != 'scsi'" class="my-2 col-span-2 lt-md:col-span-1"> <div *ngIf="device?.device_type && device?.device_type != 'ata' && device?.device_type != 'scsi'" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_type | uppercase}}</div> <div>{{device?.device_type | uppercase}}</div>
<div class="text-secondary text-md">Device Type</div> <div class="text-secondary text-md">Device Type</div>

@ -121,25 +121,34 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Private methods // @ Private methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
getAttributeStatusName(attribute_status){ getAttributeStatusName(attributeStatus: number): string {
if(attribute_status == 0){ // tslint:disable:no-bitwise
return "passed"
} else if (attribute_status == 1){ // from Constants.go
return "failed" // AttributeStatusPassed AttributeStatus = 0
} else if (attribute_status == 2){ // AttributeStatusFailedSmart AttributeStatus = 1
return "warn" // AttributeStatusWarningScrutiny AttributeStatus = 2
// AttributeStatusFailedScrutiny AttributeStatus = 4
if(attributeStatus === 0){
return 'passed'
} else if ((attributeStatus & 1) !== 0 || (attributeStatus & 4) !== 0 ){
return 'failed'
} else if ((attributeStatus & 2) !== 0){
return 'warn'
} }
return return ''
// tslint:enable:no-bitwise
} }
getAttributeName(attribute_data){ getAttributeName(attribute_data): string {
let attribute_metadata = this.metadata[attribute_data.attribute_id] let attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){ if(!attribute_metadata){
return 'Unknown Attribute Name' return 'Unknown Attribute Name'
} else { } else {
return attribute_metadata.display_name return attribute_metadata.display_name
} }
return
} }
getAttributeDescription(attribute_data){ getAttributeDescription(attribute_data){
let attribute_metadata = this.metadata[attribute_data.attribute_id] let attribute_metadata = this.metadata[attribute_data.attribute_id]

Loading…
Cancel
Save