From af2523cfeeab69feb23c2fb859f0b7212fbcdd3a Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 28 May 2022 09:50:06 -0700 Subject: [PATCH 01/19] setting GinMode to release by default. Users get confused otherwise. --- webapp/backend/pkg/web/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 914a102..6e7ae20 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -13,6 +13,7 @@ import ( "net/http" "os" "path/filepath" + "strings" ) type AppEngine struct { @@ -68,6 +69,11 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine { } func (ae *AppEngine) Start() error { + //set the gin mode + gin.SetMode("release") + if strings.ToLower(ae.Config.GetString("log.level")) == "debug" { + gin.SetMode("debug") + } logger := logrus.New() //set default log level From 2533d8d34f67a4b1874efe37c90f08a53a48154f Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 28 May 2022 09:53:45 -0700 Subject: [PATCH 02/19] using Constants for git release/debug modes. --- webapp/backend/pkg/web/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 6e7ae20..d2a5cf6 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -70,9 +70,9 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine { func (ae *AppEngine) Start() error { //set the gin mode - gin.SetMode("release") + gin.SetMode(gin.ReleaseMode) if strings.ToLower(ae.Config.GetString("log.level")) == "debug" { - gin.SetMode("debug") + gin.SetMode(gin.DebugMode) } logger := logrus.New() From a53397210ce47b24f637630fa2fd0cff6f27b0b0 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 28 May 2022 15:32:44 -0700 Subject: [PATCH 03/19] adding mechanism to override the smartctl commands used by scrutiny for device scanning, device identification and smart data retrieval. adding tests for command overrides. rename GetScanOverrides() to GetDeviceOverrides() fixes #255 --- collector/pkg/collector/metrics.go | 7 +- collector/pkg/config/config.go | 92 +++++++- collector/pkg/config/config_test.go | 56 ++++- collector/pkg/config/interface.go | 4 +- collector/pkg/config/mock/mock_config.go | 215 ++++++++++-------- .../invalid_commands_includes_device.yaml | 4 + .../invalid_commands_missing_json.yaml | 4 + .../config/testdata/override_commands.yaml | 4 + .../testdata/override_device_commands.yaml | 5 + collector/pkg/detect/detect.go | 13 +- collector/pkg/detect/detect_test.go | 23 +- collector/pkg/models/scan_override.go | 4 + example.collector.yaml | 13 ++ 13 files changed, 326 insertions(+), 118 deletions(-) create mode 100644 collector/pkg/config/testdata/invalid_commands_includes_device.yaml create mode 100644 collector/pkg/config/testdata/invalid_commands_missing_json.yaml create mode 100644 collector/pkg/config/testdata/override_commands.yaml create mode 100644 collector/pkg/config/testdata/override_device_commands.yaml diff --git a/collector/pkg/collector/metrics.go b/collector/pkg/collector/metrics.go index 31b6af7..ed1b777 100644 --- a/collector/pkg/collector/metrics.go +++ b/collector/pkg/collector/metrics.go @@ -116,12 +116,13 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT } 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. 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()) resultBytes := []byte(result) diff --git a/collector/pkg/config/config.go b/collector/pkg/config/config.go index 5995659..025082e 100644 --- a/collector/pkg/config/config.go +++ b/collector/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "github.com/analogj/go-util/utils" "github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/models" @@ -8,6 +9,8 @@ import ( "github.com/spf13/viper" "log" "os" + "sort" + "strings" ) // When initializing this class the following methods must be called: @@ -16,6 +19,8 @@ import ( // This is done automatically when created via the Factory. type configuration struct { *viper.Viper + + deviceOverrides []models.ScanOverride } //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("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") //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 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. // - simple device type (device_type: 'sat') // 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. - overrides := []models.ScanOverride{} - c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true }) - return overrides + if c.deviceOverrides == nil { + overrides := []models.ScanOverride{} + 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") } diff --git a/collector/pkg/config/config_test.go b/collector/pkg/config/config_test.go index 8511c14..d5af79d 100644 --- a/collector/pkg/config/config_test.go +++ b/collector/pkg/config/config_test.go @@ -30,7 +30,7 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) { //test err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml")) require.NoError(t, err, "should correctly load simple device config") - scanOverrides := testConfig.GetScanOverrides() + scanOverrides := testConfig.GetDeviceOverrides() //assert 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 err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml")) require.NoError(t, err, "should correctly load ignore device config") - scanOverrides := testConfig.GetScanOverrides() + scanOverrides := testConfig.GetDeviceOverrides() //assert 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 err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml")) require.NoError(t, err, "should correctly load ignore device config") - scanOverrides := testConfig.GetScanOverrides() + scanOverrides := testConfig.GetDeviceOverrides() //assert require.Equal(t, []models.ScanOverride{ @@ -75,3 +75,53 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) { Ignore: false, }}, 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) +} diff --git a/collector/pkg/config/interface.go b/collector/pkg/config/interface.go index 53dd2b8..e810cec 100644 --- a/collector/pkg/config/interface.go +++ b/collector/pkg/config/interface.go @@ -22,5 +22,7 @@ type Interface interface { GetStringSlice(key string) []string UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error - GetScanOverrides() []models.ScanOverride + GetDeviceOverrides() []models.ScanOverride + GetCommandMetricsInfoArgs(deviceName string) string + GetCommandMetricsSmartArgs(deviceName string) string } diff --git a/collector/pkg/config/mock/mock_config.go b/collector/pkg/config/mock/mock_config.go index 4c1d1d1..1b135b6 100644 --- a/collector/pkg/config/mock/mock_config.go +++ b/collector/pkg/config/mock/mock_config.go @@ -5,144 +5,121 @@ package mock_config import ( + reflect "reflect" + models "github.com/analogj/scrutiny/collector/pkg/models" gomock "github.com/golang/mock/gomock" viper "github.com/spf13/viper" - reflect "reflect" ) -// MockInterface is a mock of Interface interface +// MockInterface is a mock of Interface interface. type MockInterface struct { ctrl *gomock.Controller recorder *MockInterfaceMockRecorder } -// MockInterfaceMockRecorder is the mock recorder for MockInterface +// MockInterfaceMockRecorder is the mock recorder for MockInterface. type MockInterfaceMockRecorder struct { mock *MockInterface } -// NewMockInterface creates a new mock instance +// NewMockInterface creates a new mock instance. func NewMockInterface(ctrl *gomock.Controller) *MockInterface { mock := &MockInterface{ctrl: ctrl} mock.recorder = &MockInterfaceMockRecorder{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 { return m.recorder } -// Init mocks base method -func (m *MockInterface) Init() error { +// AllSettings mocks base method. +func (m *MockInterface) AllSettings() map[string]interface{} { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Init") - ret0, _ := ret[0].(error) + ret := m.ctrl.Call(m, "AllSettings") + ret0, _ := ret[0].(map[string]interface{}) return ret0 } -// Init indicates an expected call of Init -func (mr *MockInterfaceMockRecorder) Init() *gomock.Call { +// AllSettings indicates an expected call of AllSettings. +func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call { 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 -func (m *MockInterface) ReadConfig(configFilePath string) error { +// Get mocks base method. +func (m *MockInterface) Get(key string) interface{} { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadConfig", configFilePath) - ret0, _ := ret[0].(error) + ret := m.ctrl.Call(m, "Get", key) + ret0, _ := ret[0].(interface{}) 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 { +// Get indicates an expected call of Get. +func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call { 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 -func (m *MockInterface) AllSettings() map[string]interface{} { +// GetBool mocks base method. +func (m *MockInterface) GetBool(key string) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AllSettings") - ret0, _ := ret[0].(map[string]interface{}) + ret := m.ctrl.Call(m, "GetBool", key) + ret0, _ := ret[0].(bool) return ret0 } -// AllSettings indicates an expected call of AllSettings -func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call { +// GetBool indicates an expected call of GetBool. +func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call { 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 -func (m *MockInterface) IsSet(key string) bool { +// GetCommandMetricsInfoArgs mocks base method. +func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSet", key) - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName) + ret0, _ := ret[0].(string) return ret0 } -// IsSet indicates an expected call of IsSet -func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call { +// GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs. +func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call { 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 -func (m *MockInterface) Get(key string) interface{} { +// GetCommandMetricsSmartArgs mocks base method. +func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", key) - ret0, _ := ret[0].(interface{}) + ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName) + ret0, _ := ret[0].(string) return ret0 } -// Get indicates an expected call of Get -func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call { +// GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs. +func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call { 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 -func (m *MockInterface) GetBool(key string) bool { +// GetDeviceOverrides mocks base method. +func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBool", key) - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "GetDeviceOverrides") + ret0, _ := ret[0].([]models.ScanOverride) return ret0 } -// GetBool indicates an expected call of GetBool -func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call { +// GetDeviceOverrides indicates an expected call of GetDeviceOverrides. +func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call { 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 { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetInt", key) @@ -150,13 +127,13 @@ func (m *MockInterface) GetInt(key string) int { return ret0 } -// GetInt indicates an expected call of GetInt +// GetInt indicates an expected call of GetInt. func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() 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 { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetString", key) @@ -164,13 +141,13 @@ func (m *MockInterface) GetString(key string) string { return ret0 } -// GetString indicates an expected call of GetString +// GetString indicates an expected call of GetString. func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() 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 { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStringSlice", key) @@ -178,13 +155,79 @@ func (m *MockInterface) GetStringSlice(key string) []string { return ret0 } -// GetStringSlice indicates an expected call of GetStringSlice +// GetStringSlice indicates an expected call of GetStringSlice. func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() 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 { m.ctrl.T.Helper() varargs := []interface{}{key, rawVal} @@ -196,23 +239,9 @@ func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts 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 { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{key, rawVal}, decoderOpts...) 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)) -} diff --git a/collector/pkg/config/testdata/invalid_commands_includes_device.yaml b/collector/pkg/config/testdata/invalid_commands_includes_device.yaml new file mode 100644 index 0000000..1e6386f --- /dev/null +++ b/collector/pkg/config/testdata/invalid_commands_includes_device.yaml @@ -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. diff --git a/collector/pkg/config/testdata/invalid_commands_missing_json.yaml b/collector/pkg/config/testdata/invalid_commands_missing_json.yaml new file mode 100644 index 0000000..3ecc667 --- /dev/null +++ b/collector/pkg/config/testdata/invalid_commands_missing_json.yaml @@ -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. diff --git a/collector/pkg/config/testdata/override_commands.yaml b/collector/pkg/config/testdata/override_commands.yaml new file mode 100644 index 0000000..77a3b88 --- /dev/null +++ b/collector/pkg/config/testdata/override_commands.yaml @@ -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. diff --git a/collector/pkg/config/testdata/override_device_commands.yaml b/collector/pkg/config/testdata/override_device_commands.yaml new file mode 100644 index 0000000..fc4fd3d --- /dev/null +++ b/collector/pkg/config/testdata/override_device_commands.yaml @@ -0,0 +1,5 @@ +version: 1 +devices: + - device: /dev/sda + commands: + metrics_info_args: "--info --json -T permissive" \ No newline at end of file diff --git a/collector/pkg/detect/detect.go b/collector/pkg/detect/detect.go index 1475453..5f2fa55 100644 --- a/collector/pkg/detect/detect.go +++ b/collector/pkg/detect/detect.go @@ -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). func (d *Detect) SmartctlScan() ([]models.Device, error) { //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 { d.Logger.Errorf("Error scanning for devices: %v", 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 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"} + fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName) + 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. 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()) 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. - for _, overrideDevice := range d.Config.GetScanOverrides() { + for _, overrideDevice := range d.Config.GetDeviceOverrides() { overrideDeviceFile := strings.ToLower(overrideDevice.Device) if overrideDevice.Ignore { diff --git a/collector/pkg/detect/detect_test.go b/collector/pkg/detect/detect_test.go index 1c00ee7..70cb2f6 100644 --- a/collector/pkg/detect/detect_test.go +++ b/collector/pkg/detect/detect_test.go @@ -18,7 +18,8 @@ func TestDetect_SmartctlScan(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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) testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json") @@ -45,7 +46,8 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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) testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json") @@ -75,7 +77,8 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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) testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json") @@ -104,7 +107,9 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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{ Devices: []models.ScanDevice{ { @@ -134,7 +139,9 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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{ Devices: []models.ScanDevice{ { @@ -163,7 +170,8 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) { defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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", 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() fakeConfig := mock_config.NewMockInterface(mockCtrl) 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{ Devices: []models.ScanDevice{ { diff --git a/collector/pkg/models/scan_override.go b/collector/pkg/models/scan_override.go index 682687d..406582d 100644 --- a/collector/pkg/models/scan_override.go +++ b/collector/pkg/models/scan_override.go @@ -4,4 +4,8 @@ type ScanOverride struct { Device string `mapstructure:"device"` DeviceType []string `mapstructure:"type"` Ignore bool `mapstructure:"ignore"` + Commands struct { + MetricsInfoArgs string `mapstructure:"metrics_info_args"` + MetricsSmartArgs string `mapstructure:"metrics_smart_args"` + } `mapstructure:"commands"` } diff --git a/example.collector.yaml b/example.collector.yaml index 854de94..60ab408 100644 --- a/example.collector.yaml +++ b/example.collector.yaml @@ -53,6 +53,13 @@ devices: # - 3ware,3 # - 3ware,4 # - 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: # 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. # 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 From d9d6ce0f30aeae09f391af0a515c0f7e1fd736d0 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 31 May 2022 08:50:38 -0700 Subject: [PATCH 04/19] added docuemtnation about exit codes. --- docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md | 31 +++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md index 0db3fb8..de7f568 100644 --- a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md +++ b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md @@ -111,7 +111,36 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal ### 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/157 From 8b011878929db6e72c2dce62092abeef2893ddf9 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 31 May 2022 09:13:47 -0700 Subject: [PATCH 05/19] woarkound for volume mount w/privileged --- docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md index de7f568..4359541 100644 --- a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md +++ b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md @@ -146,6 +146,25 @@ Disks in Standby/Sleep can also cause `smartctl` to exit abnormally, usually wit ### 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? From b5dad487e57c73dc9805d07b77f9c9fecb850d61 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 31 May 2022 11:32:58 -0700 Subject: [PATCH 06/19] updating bug report. --- .github/ISSUE_TEMPLATE/bug_report.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dc01c62..acc1155 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -22,20 +22,19 @@ See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLL ``` docker run -it --rm -p 8080:8080 \ +-v `pwd`/config:/opt/scrutiny/config \ -v /run/udev:/run/udev:ro \ --cap-add SYS_RAWIO \ --device=/dev/sda \ --device=/dev/sdb \ -e DEBUG=true \ --e COLLECTOR_LOG_FILE=/tmp/collector.log \ --e SCRUTINY_LOG_FILE=/tmp/web.log \ +-e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \ +-e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \ --name scrutiny \ ghcr.io/analogj/scrutiny:master-omnibus # in another terminal trigger the collector 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. \ No newline at end of file From 488fcfc820ee59cee87e2d80215a9d4564677a4b Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 31 May 2022 13:31:34 -0700 Subject: [PATCH 07/19] added AttributeStatus bit flag ensure DeviceStatus is a valid bit flag. [docs] added running tests section to contribution guide. make sure UI correctly treats scrutiny failures as failed. --- CONTRIBUTING.md | 16 ++++++++ webapp/backend/pkg/constants.go | 37 +++++++++++------- .../database/scrutiny_repository_device.go | 2 +- webapp/backend/pkg/models/device.go | 8 ++-- .../backend/pkg/models/measurements/smart.go | 15 ++++---- .../measurements/smart_ata_attribute.go | 38 +++++++++---------- .../models/measurements/smart_attribute.go | 4 +- .../measurements/smart_nvme_attribute.go | 16 ++++---- .../measurements/smart_scsci_attribute.go | 14 +++---- .../pkg/models/measurements/smart_test.go | 23 ++++++----- .../app/modules/detail/detail.component.ts | 12 ++++-- 11 files changed, 111 insertions(+), 74 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3116caf..c665e9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -169,3 +169,19 @@ docker run -it --rm -p 8080:8080 \ ghcr.io/analogj/scrutiny:master-omnibus /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 ./... + +``` \ No newline at end of file diff --git a/webapp/backend/pkg/constants.go b/webapp/backend/pkg/constants.go index abccc06..fce37f8 100644 --- a/webapp/backend/pkg/constants.go +++ b/webapp/backend/pkg/constants.go @@ -4,25 +4,34 @@ const DeviceProtocolAta = "ATA" const DeviceProtocolScsi = "SCSI" const DeviceProtocolNvme = "NVMe" -const SmartAttributeStatusPassed = 0 -const SmartAttributeStatusFailed = 1 -const SmartAttributeStatusWarning = 2 +type AttributeStatus uint8 -const SmartWhenFailedFailingNow = "FAILING_NOW" -const SmartWhenFailedInThePast = "IN_THE_PAST" +const ( + // 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" -//const SmartStatusFailed = "failed" +func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { return b | flag } +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 ( + // DeviceStatusPassed binary, 1,2,4,8,16,32,etc DeviceStatusPassed DeviceStatus = 0 - DeviceStatusFailedSmart DeviceStatus = iota - DeviceStatusFailedScrutiny DeviceStatus = iota + DeviceStatusFailedSmart DeviceStatus = 1 + DeviceStatusFailedScrutiny DeviceStatus = 2 ) -func Set(b, flag DeviceStatus) DeviceStatus { return b | flag } -func Clear(b, flag DeviceStatus) DeviceStatus { return b &^ flag } -func Toggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag } -func Has(b, flag DeviceStatus) bool { return b&flag != 0 } +func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag } +func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag } +func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag } +func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 } diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 1669f28..897a3e1 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -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) } - device.DeviceStatus = pkg.Set(device.DeviceStatus, status) + device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status) return device, sr.gormClient.Model(&device).Updates(device).Error } diff --git a/webapp/backend/pkg/models/device.go b/webapp/backend/pkg/models/device.go index 28dd150..9c84115 100644 --- a/webapp/backend/pkg/models/device.go +++ b/webapp/backend/pkg/models/device.go @@ -21,9 +21,9 @@ type Device struct { WWN string `json:"wwn" gorm:"primary_key"` DeviceName string `json:"device_name"` - DeviceUUID string `json:"device_uuid"` - DeviceSerialID string `json:"device_serial_id"` - DeviceLabel string `json:"device_label"` + DeviceUUID string `json:"device_uuid"` + DeviceSerialID string `json:"device_serial_id"` + DeviceLabel string `json:"device_label"` Manufacturer string `json:"manufacturer"` ModelName string `json:"model_name"` @@ -166,7 +166,7 @@ func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error { dv.DeviceProtocol = info.Device.Protocol if !info.SmartStatus.Passed { - dv.DeviceStatus = pkg.Set(dv.DeviceStatus, pkg.DeviceStatusFailedSmart) + dv.DeviceStatus = pkg.DeviceStatusSet(dv.DeviceStatus, pkg.DeviceStatusFailedSmart) } return nil diff --git a/webapp/backend/pkg/models/measurements/smart.go b/webapp/backend/pkg/models/measurements/smart.go index c7062c4..40c1397 100644 --- a/webapp/backend/pkg/models/measurements/smart.go +++ b/webapp/backend/pkg/models/measurements/smart.go @@ -110,7 +110,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er sm.PowerCycleCount = info.PowerCycleCount sm.PowerOnHours = info.PowerOnTime.Hours if !info.SmartStatus.Passed { - sm.Status = pkg.DeviceStatusFailedSmart + sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedSmart) } sm.DeviceProtocol = info.Device.Protocol @@ -148,8 +148,9 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa } attrModel.PopulateAttributeStatus() 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 for _, val := range sm.Attributes { - if val.GetStatus() == pkg.SmartAttributeStatusFailed { - sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny) + if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) { + sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny) } } } @@ -204,8 +205,8 @@ func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog //find analyzed attribute status for _, val := range sm.Attributes { - if val.GetStatus() == pkg.SmartAttributeStatusFailed { - sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny) + if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) { + sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny) } } } diff --git a/webapp/backend/pkg/models/measurements/smart_ata_attribute.go b/webapp/backend/pkg/models/measurements/smart_ata_attribute.go index 5b17cfd..eac0913 100644 --- a/webapp/backend/pkg/models/measurements/smart_ata_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_ata_attribute.go @@ -18,13 +18,13 @@ type SmartAtaAttribute struct { WhenFailed string `json:"when_failed"` //Generated data - TransformedValue int64 `json:"transformed_value"` - Status int64 `json:"status"` - StatusReason string `json:"status_reason,omitempty"` - FailureRate float64 `json:"failure_rate,omitempty"` + TransformedValue int64 `json:"transformed_value"` + Status pkg.AttributeStatus `json:"status"` + StatusReason string `json:"status_reason,omitempty"` + FailureRate float64 `json:"failure_rate,omitempty"` } -func (sa *SmartAtaAttribute) GetStatus() int64 { +func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } @@ -77,7 +77,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) { case "transformed_value": sa.TransformedValue = val.(int64) case "status": - sa.Status = val.(int64) + sa.Status = val.(pkg.AttributeStatus) case "status_reason": sa.StatusReason = val.(string) case "failure_rate": @@ -89,16 +89,16 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) { //populate attribute status, using SMART Thresholds & Observed Metadata // Chainable func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute { - if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedFailingNow { + if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow { //this attribute has previously failed - sa.Status = pkg.SmartAttributeStatusFailed - sa.StatusReason = "Attribute is failing manufacturer SMART threshold" + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedSmart) + sa.StatusReason += "Attribute is failing manufacturer SMART threshold" //if the Smart Status is failed, we should exit early, no need to look at thresholds. return sa - } else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast { - sa.Status = pkg.SmartAttributeStatusWarning - sa.StatusReason = "Attribute has previously failed manufacturer SMART threshold" + } else if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedInThePast { + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny) + sa.StatusReason += "Attribute has previously failed manufacturer SMART threshold" } if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok { @@ -138,16 +138,16 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri if smartMetadata.Critical { if obsThresh.AnnualFailureRate >= 0.10 { - sa.Status = pkg.SmartAttributeStatusFailed - sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%" + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny) + sa.StatusReason += "Observed Failure Rate for Critical Attribute is greater than 10%" } } else { if obsThresh.AnnualFailureRate >= 0.20 { - sa.Status = pkg.SmartAttributeStatusFailed - sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%" + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny) + sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 20%" } else if obsThresh.AnnualFailureRate >= 0.10 { - sa.Status = pkg.SmartAttributeStatusWarning - sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%" + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny) + 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 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" } diff --git a/webapp/backend/pkg/models/measurements/smart_attribute.go b/webapp/backend/pkg/models/measurements/smart_attribute.go index 02a4fa7..a8de369 100644 --- a/webapp/backend/pkg/models/measurements/smart_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_attribute.go @@ -1,7 +1,9 @@ package measurements +import "github.com/analogj/scrutiny/webapp/backend/pkg" + type SmartAttribute interface { Flatten() (fields map[string]interface{}) Inflate(key string, val interface{}) - GetStatus() int64 + GetStatus() pkg.AttributeStatus } diff --git a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go index c4fbe0e..f4d314d 100644 --- a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go @@ -12,13 +12,13 @@ type SmartNvmeAttribute struct { Value int64 `json:"value"` Threshold int64 `json:"thresh"` - TransformedValue int64 `json:"transformed_value"` - Status int64 `json:"status"` - StatusReason string `json:"status_reason,omitempty"` - FailureRate float64 `json:"failure_rate,omitempty"` + TransformedValue int64 `json:"transformed_value"` + Status pkg.AttributeStatus `json:"status"` + StatusReason string `json:"status_reason,omitempty"` + FailureRate float64 `json:"failure_rate,omitempty"` } -func (sa *SmartNvmeAttribute) GetStatus() int64 { +func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } @@ -54,7 +54,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) { case "transformed_value": sa.TransformedValue = val.(int64) case "status": - sa.Status = val.(int64) + sa.Status = val.(pkg.AttributeStatus) case "status_reason": sa.StatusReason = val.(string) 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 if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) || (smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) { - sa.Status = pkg.SmartAttributeStatusFailed - sa.StatusReason = "Attribute is failing recommended SMART threshold" + sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny) + sa.StatusReason += "Attribute is failing recommended SMART threshold" } } } diff --git a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go index f9049a6..9b59f6b 100644 --- a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go @@ -12,13 +12,13 @@ type SmartScsiAttribute struct { Value int64 `json:"value"` Threshold int64 `json:"thresh"` - TransformedValue int64 `json:"transformed_value"` - Status int64 `json:"status"` - StatusReason string `json:"status_reason,omitempty"` - FailureRate float64 `json:"failure_rate,omitempty"` + TransformedValue int64 `json:"transformed_value"` + Status pkg.AttributeStatus `json:"status"` + StatusReason string `json:"status_reason,omitempty"` + FailureRate float64 `json:"failure_rate,omitempty"` } -func (sa *SmartScsiAttribute) GetStatus() int64 { +func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus { return sa.Status } @@ -54,7 +54,7 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) { case "transformed_value": sa.TransformedValue = val.(int64) case "status": - sa.Status = val.(int64) + sa.Status = val.(pkg.AttributeStatus) case "status_reason": sa.StatusReason = val.(string) 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 if (smartMetadata.Ideal == "low" && 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" } } diff --git a/webapp/backend/pkg/models/measurements/smart_test.go b/webapp/backend/pkg/models/measurements/smart_test.go index a65b01a..b7fdd18 100644 --- a/webapp/backend/pkg/models/measurements/smart_test.go +++ b/webapp/backend/pkg/models/measurements/smart_test.go @@ -77,7 +77,7 @@ func TestSmart_Flatten_ATA(t *testing.T) { "attr.1.failure_rate": float64(0), "attr.1.raw_string": "0", "attr.1.raw_value": int64(0), - "attr.1.status": int64(0), + "attr.1.status": pkg.AttributeStatus(0), "attr.1.status_reason": "", "attr.1.thresh": int64(1), "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.raw_string": "108", "attr.2.raw_value": int64(108), - "attr.2.status": int64(0), + "attr.2.status": pkg.AttributeStatus(0), "attr.2.status_reason": "", "attr.2.thresh": int64(54), "attr.2.transformed_value": int64(0), @@ -130,7 +130,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) { 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.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.thresh": 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{}{ "attr.available_spare.attribute_id": "available_spare", "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.thresh": 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.raw_string": "108", "attr.1.raw_value": int64(108), - "attr.1.status": int64(0), + "attr.1.status": pkg.AttributeStatus(0), "attr.1.status_reason": "", "attr.1.thresh": int64(54), "attr.1.transformed_value": int64(0), @@ -235,7 +235,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) { "device_protocol": pkg.DeviceProtocolNvme, "attr.available_spare.attribute_id": "available_spare", "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.thresh": int64(0), "attr.available_spare.transformed_value": int64(0), @@ -274,7 +274,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) { "device_protocol": pkg.DeviceProtocolScsi, "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.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.thresh": 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)) //check that temperature was correctly parsed - require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue) 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) { @@ -402,7 +405,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) { require.NoError(t, err) require.Equal(t, "WWN-test", smartMdl.DeviceWWN) 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)", smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId, 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.Equal(t, "WWN-test", smartMdl.DeviceWWN) 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)", smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId, smartMdl.Attributes["media_errors"].GetStatus(), diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts index 8a50ee5..4eb57b5 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.ts @@ -122,11 +122,17 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { // @ Private methods // ----------------------------------------------------------------------------------------------------- getAttributeStatusName(attribute_status){ - if(attribute_status == 0){ + //from Constants.go + // AttributeStatusPassed AttributeStatus = 0 + // AttributeStatusFailedSmart AttributeStatus = 1 + // AttributeStatusWarningScrutiny AttributeStatus = 2 + // AttributeStatusFailedScrutiny AttributeStatus = 4 + + if(attribute_status === 0){ return "passed" - } else if (attribute_status == 1){ + } else if (attribute_status & 1 != 0 || attribute_status & 4 != 0 ){ return "failed" - } else if (attribute_status == 2){ + } else if (attribute_status & 2 != 0){ return "warn" } return From 9aa0e97be04a5d8a2bde766270b161d979709953 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 31 May 2022 13:36:58 -0700 Subject: [PATCH 08/19] display the device UUID and device Label in the details page. fixes #265 --- .../src/app/modules/detail/detail.component.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/webapp/frontend/src/app/modules/detail/detail.component.html b/webapp/frontend/src/app/modules/detail/detail.component.html index e5e7d9a..1d77ec8 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.html +++ b/webapp/frontend/src/app/modules/detail/detail.component.html @@ -71,6 +71,16 @@
{{device?.host_id}}
Host ID
+ +
+
{{device?.device_uuid}}
+
Device UUID
+
+
+
{{device?.device_label}}
+
Device Label
+
+
{{device?.device_type | uppercase}}
Device Type
From 766a73455e1a2298207c1d8482bc54df275a6fb0 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 10:04:36 -0700 Subject: [PATCH 09/19] update the base image for docker iamges to ubuntu:latest - which follows the LTS. fixes #274 --- docker/Dockerfile | 4 ++-- docker/Dockerfile.collector | 4 ++-- docker/Dockerfile.web | 2 +- docs/INSTALL_MANUAL.md | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b6cc906..94b6df0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,14 +26,14 @@ RUN npm install -g @angular/cli@9.1.4 && \ ######## -FROM ubuntu:bionic as runtime +FROM ubuntu:latest as runtime ARG TARGETARCH EXPOSE 8080 WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" 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 \ && case ${TARGETARCH} in \ "amd64") S6_ARCH=amd64 ;; \ diff --git a/docker/Dockerfile.collector b/docker/Dockerfile.collector index 48eccc8..4c1dc8e 100644 --- a/docker/Dockerfile.collector +++ b/docker/Dockerfile.collector @@ -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 ######## -FROM ubuntu:bionic as runtime +FROM ubuntu:latest as runtime WORKDIR /scrutiny 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 /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index 7e1e959..2ff577c 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -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 WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" diff --git a/docs/INSTALL_MANUAL.md b/docs/INSTALL_MANUAL.md index ea4f0c6..1f917d1 100644 --- a/docs/INSTALL_MANUAL.md +++ b/docs/INSTALL_MANUAL.md @@ -113,7 +113,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. 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:** - `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm` - `dnf install smartmontools` From 02e3947906581c013e58706c35f5782fc1b5cc9d Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 10:22:55 -0700 Subject: [PATCH 10/19] disable github action docker build caching - may be causing "cannot reuse body, request must be retried" errors --- .github/workflows/docker-build.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index f4369fe..a903815 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -60,8 +60,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max +# cache-from: type=gha +# cache-to: type=gha,mode=max web: runs-on: ubuntu-latest @@ -110,8 +110,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max +# cache-from: type=gha +# cache-to: type=gha,mode=max omnibus: runs-on: ubuntu-latest permissions: @@ -157,5 +157,5 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file +# cache-from: type=gha +# cache-to: type=gha,mode=max \ No newline at end of file From 19a9957755e4edcea1f5732fd7355f0d44f0d45b Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 10:40:28 -0700 Subject: [PATCH 11/19] using ARG DEBIAN_FRONTEND=noninteractive --- docker/Dockerfile | 1 + docker/Dockerfile.collector | 2 +- docker/Dockerfile.web | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 94b6df0..5fded1e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,6 +33,7 @@ WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb +ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \ && update-ca-certificates \ && case ${TARGETARCH} in \ diff --git a/docker/Dockerfile.collector b/docker/Dockerfile.collector index 4c1dc8e..7f8838c 100644 --- a/docker/Dockerfile.collector +++ b/docker/Dockerfile.collector @@ -13,7 +13,7 @@ RUN go mod vendor && \ FROM ubuntu:latest as runtime WORKDIR /scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" - +ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index 2ff577c..8495b1e 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -28,7 +28,7 @@ FROM ubuntu:latest as runtime EXPOSE 8080 WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" - +ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/ From 8296a973b87706e8a63cefbce8700c740aeecf88 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 10:48:44 -0700 Subject: [PATCH 12/19] trying to fix installation. --- docker/Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 5fded1e..1bebc81 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -41,10 +41,11 @@ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl "arm64") S6_ARCH=aarch64 ;; \ 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 \ - && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / - -ADD https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb /tmp/ -RUN dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb + && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \ + && rm -rf /s6-overlay-${S6_ARCH}.tar.gz \ + && curl https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb -L -s --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ + && dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ + && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb COPY /rootfs / From 4d0fc0eae8f2e5034341ed4d5a4be6cfa70a01a9 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 10:49:22 -0700 Subject: [PATCH 13/19] trying to fix installation. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bebc81..41c6f17 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,7 +42,7 @@ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl 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 \ && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \ - && rm -rf /s6-overlay-${S6_ARCH}.tar.gz \ + && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \ && curl https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb -L -s --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ && dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb From e16933eeacbf0d6573b270309dfa74df41affaff Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 11:06:15 -0700 Subject: [PATCH 14/19] trying to fix installation. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 41c6f17..ff614ac 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -44,7 +44,7 @@ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \ && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \ && curl https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb -L -s --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ - && dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ + && dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb COPY /rootfs / From 965fbb08da9688a6fe328da89101508d3d175490 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 11:35:30 -0700 Subject: [PATCH 15/19] trying to fix installation. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ff614ac..3e5527f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -44,7 +44,7 @@ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \ && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \ && curl https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb -L -s --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ - && dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ + && dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb || true \ && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb COPY /rootfs / From 9c8498cea745b05219c5220d593576ddc0c7bcfc Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 12:20:50 -0700 Subject: [PATCH 16/19] disable and re-enable bitwise operations --- .../app/modules/detail/detail.component.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts index 4eb57b5..6e7a47b 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.ts @@ -121,21 +121,25 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { // ----------------------------------------------------------------------------------------------------- // @ Private methods // ----------------------------------------------------------------------------------------------------- - getAttributeStatusName(attribute_status){ - //from Constants.go + getAttributeStatusName(attributeStatus: number){ + // tslint:disable:no-bitwise + + // from Constants.go // AttributeStatusPassed AttributeStatus = 0 // AttributeStatusFailedSmart AttributeStatus = 1 // AttributeStatusWarningScrutiny AttributeStatus = 2 // AttributeStatusFailedScrutiny AttributeStatus = 4 - if(attribute_status === 0){ - return "passed" - } else if (attribute_status & 1 != 0 || attribute_status & 4 != 0 ){ - return "failed" - } else if (attribute_status & 2 != 0){ - return "warn" + if(attributeStatus === 0){ + return 'passed' + + } else if ((attributeStatus & 1) !== 0 || (attributeStatus & 4) !== 0 ){ + return 'failed' + } else if ((attributeStatus & 2) !== 0){ + return 'warn' } return + // tslint:enable:no-bitwise } getAttributeName(attribute_data){ From 6e02e4da021328eaff74534f2b058977cb634dde Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 12:21:54 -0700 Subject: [PATCH 17/19] fixing func def. --- webapp/frontend/src/app/modules/detail/detail.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts index 6e7a47b..020ef4a 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.ts @@ -121,7 +121,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { // ----------------------------------------------------------------------------------------------------- // @ Private methods // ----------------------------------------------------------------------------------------------------- - getAttributeStatusName(attributeStatus: number){ + getAttributeStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise // from Constants.go @@ -138,18 +138,17 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { } 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] if(!attribute_metadata){ return 'Unknown Attribute Name' } else { return attribute_metadata.display_name } - return } getAttributeDescription(attribute_data){ let attribute_metadata = this.metadata[attribute_data.attribute_id] From ca7772250cfd8380db2d16c5da654312e93f7eea Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 2 Jun 2022 21:06:43 -0700 Subject: [PATCH 18/19] fix s6-overlay overwriting bin symlinks: https://github.com/just-containers/s6-overlay/tree/v2.1.0.1#bin-and-sbin-are-symlinks adding a makefile to build docker images locally. --- Makefile | 12 ++++++++++++ docker/Dockerfile | 9 ++++----- docker/Dockerfile.collector | 2 +- docker/Dockerfile.web | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 360fa8f..a721ed7 100644 --- a/Makefile +++ b/Makefile @@ -38,5 +38,17 @@ windows/amd64: @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/ + +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: # rm scrutiny-collector-metrics-* scrutiny-web-* diff --git a/docker/Dockerfile b/docker/Dockerfile index 3e5527f..19fb17e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,7 +33,6 @@ WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb -ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \ && update-ca-certificates \ && case ${TARGETARCH} in \ @@ -41,11 +40,11 @@ RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl "arm64") S6_ARCH=aarch64 ;; \ 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 \ - && 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 \ && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \ - && curl https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb -L -s --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \ - && dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb || true \ - && 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 / diff --git a/docker/Dockerfile.collector b/docker/Dockerfile.collector index 7f8838c..4c1dc8e 100644 --- a/docker/Dockerfile.collector +++ b/docker/Dockerfile.collector @@ -13,7 +13,7 @@ RUN go mod vendor && \ FROM ubuntu:latest as runtime WORKDIR /scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" -ARG DEBIAN_FRONTEND=noninteractive + RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index 8495b1e..2ff577c 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -28,7 +28,7 @@ FROM ubuntu:latest as runtime EXPOSE 8080 WORKDIR /opt/scrutiny ENV PATH="/opt/scrutiny/bin:${PATH}" -ARG DEBIAN_FRONTEND=noninteractive + RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/ From d9ecf6c0d3eb634cd501ae2788723517d50457ce Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 4 Jun 2022 08:08:45 -0700 Subject: [PATCH 19/19] make sure defaults are available if missing from localStorage fixes #277 --- .../frontend/src/@treo/services/config/config.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/frontend/src/@treo/services/config/config.service.ts b/webapp/frontend/src/@treo/services/config/config.service.ts index b1501f5..6d6c80f 100644 --- a/webapp/frontend/src/@treo/services/config/config.service.ts +++ b/webapp/frontend/src/@treo/services/config/config.service.ts @@ -19,11 +19,11 @@ export class TreoConfigService { let currentScrutinyConfig = defaultConfig - let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY) + const localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY) if(localConfigStr){ - //check localstorage for a value - let localConfig = JSON.parse(localConfigStr) - currentScrutinyConfig = localConfig + // check localstorage for a value + const localConfig = JSON.parse(localConfigStr) + currentScrutinyConfig = Object.assign({}, localConfig, currentScrutinyConfig) // make sure defaults are available if missing from localStorage. } // Set the private defaults