diff --git a/example.scrutiny.yaml b/example.scrutiny.yaml index 039bcbf0..342628bd 100644 --- a/example.scrutiny.yaml +++ b/example.scrutiny.yaml @@ -8,7 +8,6 @@ # lowercased automatically. As such, Configuration keys are case-insensitive, # and should be lowercase in this file to be consistent with usage. - ###################################################################### # Version # @@ -26,7 +25,7 @@ web: # see docs/TROUBLESHOOTING_REVERSE_PROXY.md # basepath: `/scrutiny` # leave empty unless behind a path prefixed proxy - basepath: '' + basepath: "" database: # can also set absolute path here location: /opt/scrutiny/config/scrutiny.db @@ -40,12 +39,12 @@ web: # and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide # the `token` influxdb: -# scheme: 'http' + # scheme: 'http' host: 0.0.0.0 port: 8086 -# token: 'my-token' -# org: 'my-org' -# bucket: 'bucket' + # token: 'my-token' + # org: 'my-org' + # bucket: 'bucket' retention_policy: true # if you wish to disable TLS certificate verification, # when using self-signed certificates for example, @@ -54,9 +53,21 @@ web: # insecure_skip_verify: false log: - file: '' #absolute or relative paths allowed, eg. web.log + file: "" #absolute or relative paths allowed, eg. web.log level: INFO - +# Optional: ignore or force specific SMART attributes at the host level. +# smart: +# attribute_overrides: +# - protocol: ATA # ATA | NVMe | SCSI +# attribute_id: "187" # string ID as shown in the UI/SMART table +# wwn: "0x5000c5002df89099" # optional: limit to a specific device WWN +# action: ignore # ignore | force_status +# warn_above: 5 # optional: warn when value exceeds +# fail_above: 10 # optional: fail when value exceeds (takes precedence) +# - protocol: NVMe +# attribute_id: "media_errors" +# action: force_status +# status: passed # passed | warn | failed # Notification "urls" look like the following. For more information about service specific configuration see # Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/ diff --git a/webapp/backend/pkg/config/config.go b/webapp/backend/pkg/config/config.go index 605de860..b8225080 100644 --- a/webapp/backend/pkg/config/config.go +++ b/webapp/backend/pkg/config/config.go @@ -52,6 +52,9 @@ func (c *configuration) Init() error { c.SetDefault("web.influxdb.tls.insecure_skip_verify", false) c.SetDefault("web.influxdb.retention_policy", true) + // SMART handling overrides + c.SetDefault("smart.attribute_overrides", []map[string]interface{}{}) + //c.SetDefault("disks.include", []string{}) //c.SetDefault("disks.exclude", []string{}) diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 8ba0e8c8..4b55c718 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -58,8 +58,10 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string return device, fmt.Errorf("Could not get device from DB: %v", err) } - device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status) - return device, sr.gormClient.Model(&device).Updates(device).Error + // Overwrite with the latest evaluated status so old failure bits do not linger. + device.DeviceStatus = status + // Use map update so status=0 (passed) is persisted; gorm skips zero values in struct Updates. + return device, sr.gormClient.Model(&device).Update("device_status", status).Error } func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) { diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go index 96bbad05..947900d9 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go @@ -6,17 +6,21 @@ import ( "strings" "time" + "github.com/analogj/scrutiny/webapp/backend/pkg" + "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" + "github.com/mitchellh/mapstructure" log "github.com/sirupsen/logrus" ) -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SMART -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) { + sr.logger.Infof("SaveSmartAttributes called for wwn=%s", wwn) deviceSmartData := measurements.Smart{} err := deviceSmartData.FromCollectorSmartInfo(wwn, collectorSmartData) if err != nil { @@ -24,6 +28,10 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin return measurements.Smart{}, err } + // apply host-level attribute overrides before persisting or notifying + attributeOverrides := sr.loadAttributeOverrides() + sr.applyAttributeOverrides(&deviceSmartData, wwn, attributeOverrides) + tags, fields := deviceSmartData.Flatten() // write point immediately @@ -196,7 +204,7 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati } partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`) - + if selectEntries > 0 { partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset)) } @@ -204,3 +212,187 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati return strings.Join(partialQueryStr, "\n") } + +// loadAttributeOverrides retrieves the user-provided overrides from configuration. +func (sr *scrutinyRepository) loadAttributeOverrides() []models.AttributeOverride { + // Load raw maps so we can detect presence of threshold keys even when value is zero. + rawOverrides := []map[string]interface{}{} + if err := sr.appConfig.UnmarshalKey("smart.attribute_overrides", &rawOverrides); err != nil { + sr.logger.Debugf("failed to parse smart.attribute_overrides: %v", err) + return []models.AttributeOverride{} + } + + overrides := make([]models.AttributeOverride, 0, len(rawOverrides)) + for _, raw := range rawOverrides { + var ao models.AttributeOverride + if err := mapstructure.Decode(raw, &ao); err != nil { + sr.logger.Debugf("failed to decode attribute override entry: %v", err) + continue + } + if _, ok := raw["warn_above"]; ok { + ao.WarnAboveSet = true + } + if _, ok := raw["fail_above"]; ok { + ao.FailAboveSet = true + } + overrides = append(overrides, ao) + } + + sr.logger.Infof("Loaded %d attribute overrides from config", len(overrides)) + for i, o := range overrides { + sr.logger.Debugf(" Override %d: protocol=%s attributeId=%s wwn=%s action=%s warnAbove=%d warnSet=%t failAbove=%d failSet=%t", i, o.Protocol, o.AttributeId, o.WWN, o.Action, o.WarnAbove, o.WarnAboveSet, o.FailAbove, o.FailAboveSet) + } + return overrides +} + +// applyAttributeOverrides adjusts attribute statuses according to configured overrides +// and recomputes the device status accordingly (while preserving SMART failure bits). +func (sr *scrutinyRepository) applyAttributeOverrides(smart *measurements.Smart, wwn string, overrides []models.AttributeOverride) { + if len(overrides) == 0 { + return + } + + sr.logger.Debugf("Applying attribute overrides to device wwn=%s protocol=%s with %d attributes", wwn, smart.DeviceProtocol, len(smart.Attributes)) + + failedSmart := pkg.DeviceStatusHas(smart.Status, pkg.DeviceStatusFailedSmart) + // reset and rebuild device status; keep SMART failure flag intact. + smart.Status = pkg.DeviceStatusPassed + if failedSmart { + smart.Status = pkg.DeviceStatusFailedSmart + } + + for attrKey, attrData := range smart.Attributes { + override := sr.matchingOverride(smart.DeviceProtocol, wwn, attrKey, overrides) + if override != nil { + sr.logger.Infof("Applying override to attribute %s: action=%s", attrKey, override.Action) + attrData = sr.applyOverrideToAttribute(attrData, *override) + smart.Attributes[attrKey] = attrData + } + + // rebuild device status from attribute statuses after overrides are applied + if pkg.AttributeStatusHas(attrData.GetStatus(), pkg.AttributeStatusFailedScrutiny) { + smart.Status = pkg.DeviceStatusSet(smart.Status, pkg.DeviceStatusFailedScrutiny) + } + } +} + +func (sr *scrutinyRepository) matchingOverride(protocol string, wwn string, attributeId string, overrides []models.AttributeOverride) *models.AttributeOverride { + for ndx := range overrides { + o := overrides[ndx] + sr.logger.Debugf("Checking override %d: seeking protocol=%s attributeId=%s wwn=%s against override protocol=%s attributeId=%s wwn=%s", ndx, protocol, attributeId, wwn, o.Protocol, o.AttributeId, o.WWN) + if !strings.EqualFold(o.Protocol, protocol) { + sr.logger.Debugf(" Protocol mismatch") + continue + } + if o.AttributeId != "" && o.AttributeId != attributeId { + sr.logger.Debugf(" AttributeId mismatch: '%s' != '%s'", o.AttributeId, attributeId) + continue + } + if o.WWN != "" && !strings.EqualFold(o.WWN, wwn) { + sr.logger.Debugf(" WWN mismatch: '%s' != '%s'", o.WWN, wwn) + continue + } + + sr.logger.Debugf(" MATCH!") + return &o + } + return nil +} + +func (sr *scrutinyRepository) applyOverrideToAttribute(attr measurements.SmartAttribute, override models.AttributeOverride) measurements.SmartAttribute { + action := strings.ToLower(strings.TrimSpace(override.Action)) + // threshold overrides take precedence over generic force_status/pass logic (unless ignored) + if action == "ignore" { + return setAttributeStatus(attr, pkg.AttributeStatusPassed, "Ignored by attribute override") + } + + if override.FailAboveSet || override.WarnAboveSet { + return applyThresholdOverride(attr, override) + } + + switch action { + case "ignore": + return setAttributeStatus(attr, pkg.AttributeStatusPassed, "Ignored by attribute override") + case "force_status": + status := strings.ToLower(strings.TrimSpace(override.Status)) + switch status { + case "warn", "warning": + return setAttributeStatus(attr, pkg.AttributeStatusWarningScrutiny, "Status forced to warning by attribute override") + case "failed", "fail", "error": + return setAttributeStatus(attr, pkg.AttributeStatusFailedScrutiny, "Status forced to failed by attribute override") + default: + return setAttributeStatus(attr, pkg.AttributeStatusPassed, "Status forced to passed by attribute override") + } + default: + return attr + } +} + +// applyThresholdOverride compares the attribute value to warn/fail thresholds. +// If both warn and fail are set, fail takes precedence when exceeded. +func applyThresholdOverride(attr measurements.SmartAttribute, override models.AttributeOverride) measurements.SmartAttribute { + val := currentAttributeValue(attr) + status := pkg.AttributeStatusPassed + reason := "Status forced to passed by attribute override" + + // fail_above takes priority over warn_above when both are exceeded + if override.FailAboveSet && val > override.FailAbove { + status = pkg.AttributeStatusFailedScrutiny + reason = fmt.Sprintf("Value %d exceeded fail threshold %d", val, override.FailAbove) + } else if override.WarnAboveSet && val > override.WarnAbove { + status = pkg.AttributeStatusWarningScrutiny + reason = fmt.Sprintf("Value %d exceeded warn threshold %d", val, override.WarnAbove) + } + + return setAttributeStatus(attr, status, reason) +} + +// currentAttributeValue returns the most relevant numeric value for comparison. +// ATA uses raw counts; NVMe/SCSI use the current Value field. +func currentAttributeValue(attr measurements.SmartAttribute) int64 { + switch v := attr.(type) { + case *measurements.SmartAtaAttribute: + if v.RawValue != 0 { + return v.RawValue + } + if v.TransformedValue != 0 { + return v.TransformedValue + } + return v.Value + case *measurements.SmartNvmeAttribute: + if v.TransformedValue != 0 { + return v.TransformedValue + } + return v.Value + case *measurements.SmartScsiAttribute: + if v.TransformedValue != 0 { + return v.TransformedValue + } + return v.Value + default: + return 0 + } +} + +// setAttributeStatus updates the concrete attribute status/reason fields in-place. +func setAttributeStatus(attr measurements.SmartAttribute, status pkg.AttributeStatus, reason string) measurements.SmartAttribute { + switch v := attr.(type) { + case *measurements.SmartAtaAttribute: + v.Status = status + v.StatusReason = reason + v.FailureRate = 0 + return v + case *measurements.SmartNvmeAttribute: + v.Status = status + v.StatusReason = reason + v.FailureRate = 0 + return v + case *measurements.SmartScsiAttribute: + v.Status = status + v.StatusReason = reason + v.FailureRate = 0 + return v + default: + return attr + } +} diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes_test.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes_test.go new file mode 100644 index 00000000..acc67764 --- /dev/null +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes_test.go @@ -0,0 +1,212 @@ +package database + +import ( + "testing" + + "github.com/analogj/scrutiny/webapp/backend/pkg" + "github.com/analogj/scrutiny/webapp/backend/pkg/models" + "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestApplyAttributeOverrides(t *testing.T) { + // Setup + logger := logrus.New() + logger.SetLevel(logrus.PanicLevel) // Silence logs during tests + sr := &scrutinyRepository{ + logger: logger, + } + + wwn := "0x5000c5002df89099" + protocol := "ATA" + + // Helper to create a basic smart struct + createSmart := func(val int64, status pkg.AttributeStatus) *measurements.Smart { + return &measurements.Smart{ + DeviceProtocol: protocol, + Status: pkg.DeviceStatusPassed, + Attributes: map[string]measurements.SmartAttribute{ + "1": &measurements.SmartAtaAttribute{ + AttributeId: 1, + Value: 100, + RawValue: val, + Status: status, + }, + }, + } + } + + t.Run("No Overrides", func(t *testing.T) { + smart := createSmart(0, pkg.AttributeStatusPassed) + overrides := []models.AttributeOverride{} + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.DeviceStatusPassed, smart.Status) + assert.Equal(t, pkg.AttributeStatusPassed, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Ignore Attribute", func(t *testing.T) { + smart := createSmart(10, pkg.AttributeStatusFailedScrutiny) + smart.Status = pkg.DeviceStatusFailedScrutiny // Initially failed + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + WWN: wwn, + Action: "ignore", + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.DeviceStatusPassed, smart.Status) + assert.Equal(t, pkg.AttributeStatusPassed, smart.Attributes["1"].GetStatus()) + assert.Contains(t, smart.Attributes["1"].(*measurements.SmartAtaAttribute).StatusReason, "Ignored") + }) + + t.Run("Force Status Warn", func(t *testing.T) { + smart := createSmart(0, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + Action: "force_status", + Status: "warn", + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.False(t, pkg.DeviceStatusHas(smart.Status, pkg.DeviceStatusFailedScrutiny)) + assert.Equal(t, pkg.AttributeStatusWarningScrutiny, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Force Status Fail", func(t *testing.T) { + smart := createSmart(0, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + Action: "force_status", + Status: "failed", + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.True(t, pkg.DeviceStatusHas(smart.Status, pkg.DeviceStatusFailedScrutiny)) + assert.Equal(t, pkg.AttributeStatusFailedScrutiny, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Threshold Warn", func(t *testing.T) { + smart := createSmart(50, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + WarnAbove: 40, + WarnAboveSet: true, + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.AttributeStatusWarningScrutiny, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Threshold Fail", func(t *testing.T) { + smart := createSmart(50, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + FailAbove: 40, + FailAboveSet: true, + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.True(t, pkg.DeviceStatusHas(smart.Status, pkg.DeviceStatusFailedScrutiny)) + assert.Equal(t, pkg.AttributeStatusFailedScrutiny, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Threshold Precedence (Fail > Warn)", func(t *testing.T) { + smart := createSmart(50, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + WarnAbove: 30, + WarnAboveSet: true, + FailAbove: 40, + FailAboveSet: true, + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.True(t, pkg.DeviceStatusHas(smart.Status, pkg.DeviceStatusFailedScrutiny)) + assert.Equal(t, pkg.AttributeStatusFailedScrutiny, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Threshold Not Exceeded", func(t *testing.T) { + smart := createSmart(10, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + WarnAbove: 20, + WarnAboveSet: true, + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.AttributeStatusPassed, smart.Attributes["1"].GetStatus()) + }) + + t.Run("Protocol Mismatch", func(t *testing.T) { + smart := createSmart(10, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: "NVMe", // Mismatch + AttributeId: "1", + Action: "force_status", + Status: "failed", + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.AttributeStatusPassed, smart.Attributes["1"].GetStatus()) + }) + + t.Run("WWN Mismatch", func(t *testing.T) { + smart := createSmart(10, pkg.AttributeStatusPassed) + + overrides := []models.AttributeOverride{ + { + Protocol: protocol, + AttributeId: "1", + WWN: "0xOtherWWN", // Mismatch + Action: "force_status", + Status: "failed", + }, + } + + sr.applyAttributeOverrides(smart, wwn, overrides) + + assert.Equal(t, pkg.AttributeStatusPassed, smart.Attributes["1"].GetStatus()) + }) +} diff --git a/webapp/backend/pkg/models/attribute_override.go b/webapp/backend/pkg/models/attribute_override.go new file mode 100644 index 00000000..60d4fdb6 --- /dev/null +++ b/webapp/backend/pkg/models/attribute_override.go @@ -0,0 +1,40 @@ +package models + +// AttributeOverride defines optional per-attribute handling rules. +// These are host-level overrides that allow you to ignore or force a status +// for specific SMART attributes without modifying collector output. +// Config path: smart.attribute_overrides +// Example: +// smart: +// +// attribute_overrides: +// - protocol: ATA +// attribute_id: "187" +// wwn: "0x5000c5002df89099" +// action: "ignore" +// - protocol: NVMe +// attribute_id: "media_errors" +// action: "force_status" +// status: "passed" +// +// Supported actions: "ignore", "force_status" (status: passed|warn|failed) +// All matching fields are optional except protocol/action/attribute_id. +// WWN match is used when provided; otherwise override is applied to all devices. +// SerialNumber is accepted for future expansion but currently unused. +type AttributeOverride struct { + Protocol string `json:"protocol" mapstructure:"protocol"` + AttributeId string `json:"attribute_id" mapstructure:"attribute_id"` + WWN string `json:"wwn,omitempty" mapstructure:"wwn"` + SerialNumber string `json:"serial_number,omitempty" mapstructure:"serial_number"` + Action string `json:"action" mapstructure:"action"` + Status string `json:"status,omitempty" mapstructure:"status"` + // Optional numeric thresholds. If set, the attribute will be marked warn/failed + // when its value exceeds the specified thresholds (using raw values for ATA, and + // current Value for NVMe/SCSI). + WarnAbove int64 `json:"warn_above" mapstructure:"warn_above"` + FailAbove int64 `json:"fail_above" mapstructure:"fail_above"` + + // Internal flags to distinguish between an explicitly configured zero and an unset value. + WarnAboveSet bool `json:"-" mapstructure:"-"` + FailAboveSet bool `json:"-" mapstructure:"-"` +} diff --git a/webapp/backend/pkg/web/handler/upload_device_metrics.go b/webapp/backend/pkg/web/handler/upload_device_metrics.go index 08144338..4ca0ad85 100644 --- a/webapp/backend/pkg/web/handler/upload_device_metrics.go +++ b/webapp/backend/pkg/web/handler/upload_device_metrics.go @@ -50,14 +50,12 @@ func UploadDeviceMetrics(c *gin.Context) { return } - if smartData.Status != pkg.DeviceStatusPassed { - //there is a failure detected by Scrutiny, update the device status on the homepage. - updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status) - if err != nil { - logger.Errorln("An error occurred while updating device status", err) - c.JSON(http.StatusInternalServerError, gin.H{"success": false}) - return - } + // Always persist the latest evaluated status so old failure flags are cleared when resolved. + updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status) + if err != nil { + logger.Errorln("An error occurred while updating device status", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return } // save smart temperature data (ignore failures) diff --git a/webapp/backend/pkg/web/server_test.go b/webapp/backend/pkg/web/server_test.go index bd2407c7..4ef1d925 100644 --- a/webapp/backend/pkg/web/server_test.go +++ b/webapp/backend/pkg/web/server_test.go @@ -104,6 +104,8 @@ func (suite *ServerTestSuite) TestHealthRoute() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions { @@ -147,6 +149,8 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions { @@ -190,6 +194,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() @@ -249,6 +255,8 @@ func (suite *ServerTestSuite) TestPopulateMultiple() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() @@ -349,6 +357,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"}) @@ -395,6 +405,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"}) @@ -441,6 +453,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"}) @@ -487,6 +501,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"}) @@ -532,6 +548,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() { fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_username").Return("admin").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.init_password").Return("password12345").AnyTimes() fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()