Identify drives by a Scrutiny UUID instead of wwn (#960)

* Generate a UUIDv5 from a random namespace  based on WWN, model name, and serial number
* Migrate sqlite and influxdb data accordingly
* Update frontend API routes and components
* Fixes #923
This commit is contained in:
Aram Akhavan
2026-03-25 20:16:17 -07:00
committed by GitHub
parent e4c40f7e80
commit c3b2eb2b4f
69 changed files with 815 additions and 402 deletions
+14 -13
View File
@@ -15,6 +15,7 @@ import (
"github.com/analogj/scrutiny/collector/pkg/detect" "github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"github.com/gofrs/uuid/v5"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -64,9 +65,9 @@ func (mc *MetricsCollector) Run() error {
return err return err
} }
//filter any device with empty wwn (they are invalid) // Remove any device without a scrutiny UUID, but this should never happen...
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool { detectedStorageDevices := lo.Filter(rawDetectedStorageDevices, func(device models.Device, _ int) bool {
return len(dev.WWN) > 0 return device.ScrutinyUUID.IsNil()
}) })
mc.logger.Infoln("Sending detected devices to API, for filtering & validation") mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
@@ -90,7 +91,7 @@ func (mc *MetricsCollector) Run() error {
// execute collection in parallel go-routines // execute collection in parallel go-routines
//wg.Add(1) //wg.Add(1)
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType) //go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
mc.Collect(device.WWN, device.DeviceName, device.DeviceType) mc.Collect(device.ScrutinyUUID, device.DeviceName, device.DeviceType)
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 { if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second) time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
@@ -117,10 +118,10 @@ func (mc *MetricsCollector) Validate() error {
} }
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) { // func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) { func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string, deviceType string) {
//defer wg.Done() //defer wg.Done()
if len(deviceWWN) == 0 { if scrutiny_uuid.IsNil() {
mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName) mc.logger.Errorf("no scrutiny UUID was created for %s. Skipping collection for this device (no data association possible).\n", deviceName)
return return
} }
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName) mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
@@ -140,7 +141,7 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
// smartctl command exited with an error, we should still push the data to the API server // smartctl command exited with an error, we should still push the data to the API server
mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName) mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName)
mc.LogSmartctlExitCode(exitError.ExitCode()) mc.LogSmartctlExitCode(exitError.ExitCode())
mc.Publish(deviceWWN, resultBytes) mc.Publish(scrutiny_uuid, resultBytes)
} else { } else {
mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName) mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName)
mc.logger.Errorf("ERROR MESSAGE: %v", err) mc.logger.Errorf("ERROR MESSAGE: %v", err)
@@ -149,19 +150,19 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
return return
} else { } else {
//successful run, pass the results directly to webapp backend for parsing and processing. //successful run, pass the results directly to webapp backend for parsing and processing.
mc.Publish(deviceWWN, resultBytes) mc.Publish(scrutiny_uuid, resultBytes)
} }
} }
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error { func (mc *MetricsCollector) Publish(scrutinyUuid uuid.UUID, payload []byte) error {
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN) mc.logger.Infof("Publishing smartctl results for %s\n", scrutinyUuid)
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String()) apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN))) apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", scrutinyUuid.String()))
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload)) resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
if err != nil { if err != nil {
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", deviceWWN, err) mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", scrutinyUuid, err)
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
+3 -7
View File
@@ -101,15 +101,11 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
device.WWN = strings.ToLower(wwn.ToString()) device.WWN = strings.ToLower(wwn.ToString())
d.Logger.Debugf("NAA: %d OUI: %d Id: %d => WWN: %s", wwn.Naa, wwn.Oui, wwn.Id, device.WWN) d.Logger.Debugf("NAA: %d OUI: %d Id: %d => WWN: %s", wwn.Naa, wwn.Oui, wwn.Id, device.WWN)
} else { } else {
d.Logger.Info("Using WWN Fallback") d.Logger.Debug("Using WWN Fallback")
d.wwnFallback(device) d.wwnFallback(device)
} }
if len(device.WWN) == 0 {
// no WWN populated after WWN lookup and fallback. we need to throw an error device.ScrutinyUUID = GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
errMsg := fmt.Sprintf("no WWN (or fallback) populated for device: %s. Device will be registered, but no data will be published for this device. ", device.DeviceName)
d.Logger.Errorf("%v", errMsg)
return fmt.Errorf("%v", errMsg)
}
return nil return nil
} }
+3 -8
View File
@@ -1,10 +1,11 @@
package detect package detect
import ( import (
"strings"
"github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw" "github.com/jaypipes/ghw"
"strings"
) )
func DevicePrefix() string { func DevicePrefix() string {
@@ -89,7 +90,7 @@ func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]models.D
return missingDevices, nil return missingDevices, nil
} }
//WWN values NVMe and SCSI // WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) { func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block() block, err := ghw.Block()
if err == nil { if err == nil {
@@ -102,12 +103,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
} }
} }
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase. //wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
} }
+3 -8
View File
@@ -1,10 +1,11 @@
package detect package detect
import ( import (
"strings"
"github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw" "github.com/jaypipes/ghw"
"strings"
) )
func DevicePrefix() string { func DevicePrefix() string {
@@ -27,7 +28,7 @@ func (d *Detect) Start() ([]models.Device, error) {
return detectedDevices, nil return detectedDevices, nil
} }
//WWN values NVMe and SCSI // WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) { func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block() block, err := ghw.Block()
if err == nil { if err == nil {
@@ -40,12 +41,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
} }
} }
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase. //wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
} }
-6
View File
@@ -45,12 +45,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
} }
} }
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase. //wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
} }
+2 -10
View File
@@ -3,7 +3,6 @@ package detect
import ( import (
"github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"strings"
) )
func DevicePrefix() string { func DevicePrefix() string {
@@ -26,14 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
return detectedDevices, nil return detectedDevices, nil
} }
//WWN values NVMe and SCSI // WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) { func (d *Detect) wwnFallback(detectedDevice *models.Device) {
// No fallback on windows
//fallback to serial number
if len(detectedDevice.WWN) == 0 {
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
} }
+16
View File
@@ -0,0 +1,16 @@
package detect
import (
"github.com/gofrs/uuid/v5"
)
// Randomly generated UUID v4 namespace for Scrutiny
var ScrutinyNamespaceUUID = uuid.Must(uuid.FromString("3ea22b35-682b-49fb-a655-abffed108e48"))
// WWN's are not actually unique so we use Model Name and Serial Number
// to hopefully create something that is actually unique despite
// manufacturer laziness
func GenerateScrutinyUUID(modelName string, serialNumber string, wwn string) uuid.UUID {
name := modelName + serialNumber + wwn
return uuid.NewV5(ScrutinyNamespaceUUID, name)
}
@@ -0,0 +1,67 @@
package detect
import (
"bytes"
"encoding/json"
"os"
"testing"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/gofrs/uuid/v5"
"github.com/stretchr/testify/require"
)
func TestGenerateScrutinyUUID(t *testing.T) {
t.Run("NVMe device from test data", func(t *testing.T) {
testData, err := os.ReadFile("testdata/smartctl_info_nvme.json")
require.NoError(t, err)
var smartInfo collector.SmartInfo
err = json.Unmarshal(testData, &smartInfo)
require.NoError(t, err)
device := &models.Device{
ModelName: smartInfo.ModelName,
SerialNumber: smartInfo.SerialNumber,
}
// NVMe drives don't have a WWN
// so scrutiny falls back to serial number
device.WWN = device.SerialNumber
uuid := GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
require.NotEmpty(t, uuid.String(), "Generated UUID should not be empty")
require.Equal(t, uint8(5), uuid.Version(), "Expected UUID version 5")
uuid2 := GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
require.True(t, bytes.Equal(uuid.Bytes(), uuid2.Bytes()), "UUID generation should be deterministic for the same input")
})
// Test with different device data to ensure uniqueness
t.Run("different devices produce different UUIDs", func(t *testing.T) {
device1 := models.Device{
ModelName: "Samsung SSD 860 EVO 1TB",
SerialNumber: "S3ZANX0K123456A",
WWN: "5002538e40a22954",
}
device2 := device1
device2.SerialNumber = "S3ZANX0K123456B"
uuid1 := GenerateScrutinyUUID(device1.ModelName, device1.SerialNumber, device1.WWN)
uuid2 := GenerateScrutinyUUID(device2.ModelName, device2.SerialNumber, device2.WWN)
require.False(t, bytes.Equal(uuid1.Bytes(), uuid2.Bytes()), "Different devices should produce different UUIDs")
})
}
func TestScrutinyNamespaceUUID(t *testing.T) {
// Make sure no one changes the namespace
expectedNamespace, err := uuid.FromString("3ea22b35-682b-49fb-a655-abffed108e48")
if err != nil {
t.Fatalf("Failed to parse expected namespace UUID: %v", err)
}
require.True(t, bytes.Equal(ScrutinyNamespaceUUID.Bytes(), expectedNamespace.Bytes()), "Scrutiny Namespace UUID should never change")
}
+9 -4
View File
@@ -1,12 +1,17 @@
package models package models
import (
"github.com/gofrs/uuid/v5"
)
type Device struct { type Device struct {
WWN string `json:"wwn"` ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"`
WWN string `json:"wwn"`
DeviceName string `json:"device_name"` DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"` DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"` DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"` DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"` ModelName string `json:"model_name"`
+1
View File
@@ -9,6 +9,7 @@ require (
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-gormigrate/gormigrate/v2 v2.1.5 github.com/go-gormigrate/gormigrate/v2 v2.1.5
github.com/go-viper/mapstructure/v2 v2.5.0 github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0 github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/jaypipes/ghw v0.21.2 github.com/jaypipes/ghw v0.21.2
github.com/nicholas-fedor/shoutrrr v0.13.2 github.com/nicholas-fedor/shoutrrr v0.13.2
+26
View File
@@ -61,6 +61,10 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -174,6 +178,7 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -183,30 +188,51 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+4 -2
View File
@@ -4,8 +4,9 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI" const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe" const DeviceProtocolNvme = "NVMe"
//go:generate stringer -type=AttributeStatus
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc // AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
//
//go:generate stringer -type=AttributeStatus
type AttributeStatus uint8 type AttributeStatus uint8
const ( const (
@@ -23,8 +24,9 @@ func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &
func AttributeStatusToggle(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 } func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
//go:generate stringer -type=DeviceStatus
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc // DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
//
//go:generate stringer -type=DeviceStatus
type DeviceStatus uint8 type DeviceStatus uint8
const ( const (
+11 -10
View File
@@ -7,6 +7,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "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/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gofrs/uuid/v5"
) )
// Create mock using: // Create mock using:
@@ -17,19 +18,19 @@ type DeviceRepo interface {
RegisterDevice(ctx context.Context, dev models.Device) error RegisterDevice(ctx context.Context, dev models.Device) error
GetDevices(ctx context.Context) ([]models.Device, error) GetDevices(ctx context.Context) ([]models.Device, error)
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error)
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error)
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error)
UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error
DeleteDevice(ctx context.Context, wwn string) error DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error)
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error)
LoadSettings(ctx context.Context) (*models.Settings, error) LoadSettings(ctx context.Context) (*models.Settings, error)
SaveSettings(ctx context.Context, settings models.Settings) error SaveSettings(ctx context.Context, settings models.Settings) error
@@ -1,10 +1,12 @@
package m20250221084400 package m20250221084400
import ( import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time" "time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
) )
// Deprecated: m20250221084400.Device is deprecated, only used by db migrations
type Device struct { type Device struct {
Archived bool `json:"archived"` Archived bool `json:"archived"`
//GORM attributes, see: http://gorm.io/docs/conventions.html //GORM attributes, see: http://gorm.io/docs/conventions.html
@@ -0,0 +1,44 @@
package m20260216155600
import (
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/gofrs/uuid/v5"
)
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
Archived bool `json:"archived"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn"`
DeviceName string `json:"device_name"`
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"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"`
}
@@ -1,5 +1,10 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: webapp/backend/pkg/database/interface.go // Source: webapp/backend/pkg/database/interface.go
//
// Generated by this command:
//
// mockgen -source=webapp/backend/pkg/database/interface.go -destination=webapp/backend/pkg/database/mock/mock_database.go
//
// Package mock_database is a generated GoMock package. // Package mock_database is a generated GoMock package.
package mock_database package mock_database
@@ -12,6 +17,7 @@ import (
models "github.com/analogj/scrutiny/webapp/backend/pkg/models" models "github.com/analogj/scrutiny/webapp/backend/pkg/models"
collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
uuid "github.com/gofrs/uuid/v5"
gomock "go.uber.org/mock/gomock" gomock "go.uber.org/mock/gomock"
) )
@@ -19,6 +25,7 @@ import (
type MockDeviceRepo struct { type MockDeviceRepo struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockDeviceRepoMockRecorder recorder *MockDeviceRepoMockRecorder
isgomock struct{}
} }
// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo. // MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo.
@@ -52,47 +59,33 @@ func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close))
} }
// UpdateDeviceArchived mocks base method.
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, wwn)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived)
}
// DeleteDevice mocks base method. // DeleteDevice mocks base method.
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error { func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn) ret := m.ctrl.Call(m, "DeleteDevice", ctx, scrutiny_uuid)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteDevice indicates an expected call of DeleteDevice. // DeleteDevice indicates an expected call of DeleteDevice.
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, scrutiny_uuid any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, scrutiny_uuid)
} }
// GetDeviceDetails mocks base method. // GetDeviceDetails mocks base method.
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) { func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn) ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, scrutiny_uuid)
ret0, _ := ret[0].(models.Device) ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetDeviceDetails indicates an expected call of GetDeviceDetails. // GetDeviceDetails indicates an expected call of GetDeviceDetails.
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, scrutiny_uuid any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, scrutiny_uuid)
} }
// GetDevices mocks base method. // GetDevices mocks base method.
@@ -105,52 +98,52 @@ func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error
} }
// GetDevices indicates an expected call of GetDevices. // GetDevices indicates an expected call of GetDevices.
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx)
} }
// GetSmartAttributeHistory mocks base method. // GetSmartAttributeHistory mocks base method.
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) { func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes) ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
ret0, _ := ret[0].([]measurements.Smart) ret0, _ := ret[0].([]measurements.Smart)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory. // GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory.
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
} }
// GetSmartTemperatureHistory mocks base method. // GetSmartTemperatureHistory mocks base method.
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) { func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey) ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey)
ret0, _ := ret[0].(map[string][]measurements.SmartTemperature) ret0, _ := ret[0].(map[uuid.UUID][]measurements.SmartTemperature)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory. // GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory.
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey)
} }
// GetSummary mocks base method. // GetSummary mocks base method.
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) { func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSummary", ctx) ret := m.ctrl.Call(m, "GetSummary", ctx)
ret0, _ := ret[0].(map[string]*models.DeviceSummary) ret0, _ := ret[0].(map[uuid.UUID]*models.DeviceSummary)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetSummary indicates an expected call of GetSummary. // GetSummary indicates an expected call of GetSummary.
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx)
} }
@@ -164,7 +157,7 @@ func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
} }
// HealthCheck indicates an expected call of HealthCheck. // HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx)
} }
@@ -179,7 +172,7 @@ func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, er
} }
// LoadSettings indicates an expected call of LoadSettings. // LoadSettings indicates an expected call of LoadSettings.
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx)
} }
@@ -193,7 +186,7 @@ func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device)
} }
// RegisterDevice indicates an expected call of RegisterDevice. // RegisterDevice indicates an expected call of RegisterDevice.
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev)
} }
@@ -207,66 +200,80 @@ func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Setti
} }
// SaveSettings indicates an expected call of SaveSettings. // SaveSettings indicates an expected call of SaveSettings.
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings)
} }
// SaveSmartAttributes mocks base method. // SaveSmartAttributes mocks base method.
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) { func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData) ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, scrutiny_uuid, collectorSmartData)
ret0, _ := ret[0].(measurements.Smart) ret0, _ := ret[0].(measurements.Smart)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// SaveSmartAttributes indicates an expected call of SaveSmartAttributes. // SaveSmartAttributes indicates an expected call of SaveSmartAttributes.
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, scrutiny_uuid, collectorSmartData any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, scrutiny_uuid, collectorSmartData)
} }
// SaveSmartTemperature mocks base method. // SaveSmartTemperature mocks base method.
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error { func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory) ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature. // SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
} }
// UpdateDevice mocks base method. // UpdateDevice mocks base method.
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) { func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData) ret := m.ctrl.Call(m, "UpdateDevice", ctx, scrutiny_uuid, collectorSmartData)
ret0, _ := ret[0].(models.Device) ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// UpdateDevice indicates an expected call of UpdateDevice. // UpdateDevice indicates an expected call of UpdateDevice.
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, scrutiny_uuid, collectorSmartData any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, scrutiny_uuid, collectorSmartData)
}
// UpdateDeviceArchived mocks base method.
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, scrutiny_uuid, archived)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, scrutiny_uuid, archived any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, scrutiny_uuid, archived)
} }
// UpdateDeviceStatus mocks base method. // UpdateDeviceStatus mocks base method.
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) { func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status) ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, scrutiny_uuid, status)
ret0, _ := ret[0].(models.Device) ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus. // UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus.
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call { func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, scrutiny_uuid, status any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, scrutiny_uuid, status)
} }
@@ -13,10 +13,12 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/gofrs/uuid/v5"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/domain" "github.com/influxdata/influxdb-client-go/v2/domain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -333,16 +335,16 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// get a map of all devices and associated SMART data // get a map of all devices and associated SMART data
func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) { func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error) {
devices, err := sr.GetDevices(ctx) devices, err := sr.GetDevices(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
summaries := map[string]*models.DeviceSummary{} summaries := map[uuid.UUID]*models.DeviceSummary{}
for _, device := range devices { for _, device := range devices {
summaries[device.WWN] = &models.DeviceSummary{Device: device} summaries[device.ScrutinyUUID] = &models.DeviceSummary{Device: device}
} }
// Get parser flux query result // Get parser flux query result
@@ -357,7 +359,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date") |> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|> last() |> last()
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
weeklyData = from(bucket: bucketBaseName + "_weekly") weeklyData = from(bucket: bucketBaseName + "_weekly")
|> range(start: -10y, stop: now()) |> range(start: -10y, stop: now())
@@ -365,7 +367,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date") |> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|> last() |> last()
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
monthlyData = from(bucket: bucketBaseName + "_monthly") monthlyData = from(bucket: bucketBaseName + "_monthly")
|> range(start: -10y, stop: now()) |> range(start: -10y, stop: now())
@@ -373,7 +375,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date") |> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|> last() |> last()
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
yearlyData = from(bucket: bucketBaseName + "_yearly") yearlyData = from(bucket: bucketBaseName + "_yearly")
|> range(start: -10y, stop: now()) |> range(start: -10y, stop: now())
@@ -381,12 +383,12 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date") |> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|> last() |> last()
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
union(tables: [dailyData, weeklyData, monthlyData, yearlyData]) union(tables: [dailyData, weeklyData, monthlyData, yearlyData])
|> sort(columns: ["_time"], desc: false) |> sort(columns: ["_time"], desc: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> last(column: "device_wwn") |> last(column: "scrutiny_uuid")
|> yield(name: "last") |> yield(name: "last")
`, `,
sr.appConfig.GetString("web.influxdb.bucket"), sr.appConfig.GetString("web.influxdb.bucket"),
@@ -404,14 +406,15 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
//get summary data from Influxdb. //get summary data from Influxdb.
//result.Record().Values() //result.Record().Values()
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok { if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
//ensure summaries is intialized for this wwn //ensure summaries is intialized for this scrutiny_uuid
if _, exists := summaries[deviceWWN.(string)]; !exists { if _, exists := summaries[scrutinyUUID]; !exists {
summaries[deviceWWN.(string)] = &models.DeviceSummary{} summaries[scrutinyUUID] = &models.DeviceSummary{}
} }
summaries[deviceWWN.(string)].SmartResults = &models.SmartSummary{ summaries[scrutinyUUID].SmartResults = &models.SmartSummary{
Temp: result.Record().Values()["temp"].(int64), Temp: result.Record().Values()["temp"].(int64),
PowerOnHours: result.Record().Values()["power_on_hours"].(int64), PowerOnHours: result.Record().Values()["power_on_hours"].(int64),
CollectorDate: result.Record().Values()["_time"].(time.Time), CollectorDate: result.Record().Values()["_time"].(time.Time),
@@ -434,8 +437,8 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
sr.logger.Printf("========================>>>>>>>>======================") sr.logger.Printf("========================>>>>>>>>======================")
sr.logger.Printf("Error: %v", err) sr.logger.Printf("Error: %v", err)
} }
for wwn, tempHistory := range deviceTempHistory { for scutiny_uuid, tempHistory := range deviceTempHistory {
summaries[wwn].TempHistory = tempHistory summaries[scutiny_uuid].TempHistory = tempHistory
} }
return summaries, nil return summaries, nil
@@ -8,6 +8,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "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/collector"
"github.com/gofrs/uuid/v5"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -19,7 +20,7 @@ import (
// update device fields that may change: (DeviceType, HostID) // update device fields that may change: (DeviceType, HostID)
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error { func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{ if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "wwn"}}, Columns: []clause.Column{{Name: "scrutiny_uuid"}},
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}), DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
}).Create(&dev).Error; err != nil { }).Create(&dev).Error; err != nil {
return err return err
@@ -38,9 +39,9 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
} }
// update device (only metadata) from collector // update device (only metadata) from collector
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) { func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) {
var device models.Device var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
return device, fmt.Errorf("could not get device from DB: %v", err) return device, fmt.Errorf("could not get device from DB: %v", err)
} }
@@ -53,9 +54,9 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
} }
// Update Device Status // Update Device Status
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) { func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) {
var device models.Device var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
return device, fmt.Errorf("could not get device from DB: %v", err) return device, fmt.Errorf("could not get device from DB: %v", err)
} }
@@ -63,12 +64,12 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string
return device, sr.gormClient.Model(&device).Updates(device).Error return device, sr.gormClient.Model(&device).Updates(device).Error
} }
func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) { func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
var device models.Device var device models.Device
fmt.Println("GetDeviceDetails from GORM") fmt.Println("GetDeviceDetails from GORM")
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
return models.Device{}, err return models.Device{}, err
} }
@@ -76,17 +77,17 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
} }
// Update Device Archived State // Update Device Archived State
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error { func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error {
var device models.Device var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
return fmt.Errorf("could not get device from DB: %v", err) return fmt.Errorf("could not get device from DB: %v", err)
} }
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error return sr.gormClient.Model(&device).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Update("archived", archived).Error
} }
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error { func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error {
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil { if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Delete(&models.Device{}).Error; err != nil {
return err return err
} }
@@ -99,14 +100,14 @@ func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) erro
} }
for _, bucket := range buckets { for _, bucket := range buckets {
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, bucket) sr.logger.Infof("Deleting data for %s in bucket: %s", scrutiny_uuid.String(), bucket)
if err := sr.influxClient.DeleteAPI().DeleteWithName( if err := sr.influxClient.DeleteAPI().DeleteWithName(
ctx, ctx,
sr.appConfig.GetString("web.influxdb.org"), sr.appConfig.GetString("web.influxdb.org"),
bucket, bucket,
time.Now().AddDate(-10, 0, 0), time.Now().AddDate(-10, 0, 0),
time.Now(), time.Now(),
fmt.Sprintf(`device_wwn="%s"`, wwn), fmt.Sprintf(`scrutiny_uuid="%s"`, scrutiny_uuid.String()),
); err != nil { ); err != nil {
return err return err
} }
@@ -8,17 +8,18 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gofrs/uuid/v5"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/api"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SMART // SMART
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) { func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
deviceSmartData := measurements.Smart{} deviceSmartData := measurements.Smart{}
err := deviceSmartData.FromCollectorSmartInfo(wwn, collectorSmartData) err := deviceSmartData.FromCollectorSmartInfo(scrutiny_uuid, collectorSmartData)
if err != nil { if err != nil {
sr.logger.Errorln("Could not process SMART metrics", err) sr.logger.Errorln("Could not process SMART metrics", err)
return measurements.Smart{}, err return measurements.Smart{}, err
@@ -34,14 +35,14 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
// When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry. // When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry.
// For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries // For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries
// 2 to 4 are returned (2 being the third newest, since it is zero-indexed) // 2 to 4 are returned (2 being the third newest, since it is zero-indexed)
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) { func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
// Get SMartResults from InfluxDB // Get SMartResults from InfluxDB
//TODO: change the filter startrange to a real number. //TODO: change the filter startrange to a real number.
// Get parser flux query result // Get parser flux query result
//appConfig.GetString("web.influxdb.bucket") //appConfig.GetString("web.influxdb.bucket")
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes) queryStr := sr.aggregateSmartAttributesQuery(scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
log.Infoln(queryStr) log.Infoln(queryStr)
smartResults := []measurements.Smart{} smartResults := []measurements.Smart{}
@@ -100,7 +101,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking,
return influxWriteApi.WritePoint(ctx, p) return influxWriteApi.WritePoint(ctx, p)
} }
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string { func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
/* /*
@@ -108,28 +109,28 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
weekData = from(bucket: "metrics") weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) |> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|> tail(n: 10, offset: 0) |> tail(n: 10, offset: 0)
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
monthData = from(bucket: "metrics_weekly") monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w) |> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) |> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|> tail(n: 10, offset: 0) |> tail(n: 10, offset: 0)
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
yearData = from(bucket: "metrics_monthly") yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo) |> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) |> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|> tail(n: 10, offset: 0) |> tail(n: 10, offset: 0)
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
foreverData = from(bucket: "metrics_yearly") foreverData = from(bucket: "metrics_yearly")
|> range(start: -10y, stop: -1y) |> range(start: -10y, stop: -1y)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" ) |> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|> tail(n: 10, offset: 0) |> tail(n: 10, offset: 0)
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
@@ -150,7 +151,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
if len(nestedDurationKeys) == 1 { if len(nestedDurationKeys) == 1 {
//there's only one bucket being queried, no need to union, just aggregate the dataset and return //there's only one bucket being queried, no need to union, just aggregate the dataset and return
partialQueryStr = append(partialQueryStr, []string{ partialQueryStr = append(partialQueryStr, []string{
sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes), sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
fmt.Sprintf(`%sData`, nestedDurationKeys[0]), fmt.Sprintf(`%sData`, nestedDurationKeys[0]),
`|> sort(columns: ["_time"], desc: true)`, `|> sort(columns: ["_time"], desc: true)`,
`|> yield()`, `|> yield()`,
@@ -165,9 +166,9 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
if selectEntries > 0 { if selectEntries > 0 {
// We only need the last `n + offset` # of entries from each table to guarantee we can // We only need the last `n + offset` # of entries from each table to guarantee we can
// get the last `n` # of entries starting from `offset` of the union // get the last `n` # of entries starting from `offset` of the union
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes)) subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
} else { } else {
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes)) subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, 0, 0, attributes))
} }
} }
partialQueryStr = append(partialQueryStr, subQueries...) partialQueryStr = append(partialQueryStr, subQueries...)
@@ -184,7 +185,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
return strings.Join(partialQueryStr, "\n") return strings.Join(partialQueryStr, "\n")
} }
func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string { func (sr *scrutinyRepository) generateSmartAttributesSubquery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
bucketName := sr.lookupBucketName(durationKey) bucketName := sr.lookupBucketName(durationKey)
durationRange := sr.lookupDuration(durationKey) durationRange := sr.lookupDuration(durationKey)
@@ -192,7 +193,7 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati
fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName), fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName),
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
`|> filter(fn: (r) => r["_measurement"] == "smart" )`, `|> filter(fn: (r) => r["_measurement"] == "smart" )`,
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn), fmt.Sprintf(`|> filter(fn: (r) => r["scrutiny_uuid"] == "%s" )`, scrutiny_uuid.String()),
} }
partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`) partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`)
@@ -7,12 +7,14 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20260216155600"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "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/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
@@ -424,6 +426,53 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.Create(&defaultSettings).Error return tx.Create(&defaultSettings).Error
}, },
}, },
{
ID: "m20260216155600", // add ScrutinyUUID as primary key
Migrate: func(tx *gorm.DB) error {
devices := []m20260216155600.Device{}
if err := tx.Find(&devices).Error; err != nil {
return err
}
sr.logger.Debug("Generating Scrutiny UUIDs")
for i := range devices {
device := &devices[i]
device.ScrutinyUUID = detect.GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
}
// sqlite doesn't support altering columns
// so we have to create a new one, drop the old one, then rename.
sr.logger.Debug("Creating new devices table")
tx.Table("devices_new").AutoMigrate(&m20260216155600.Device{})
if len(devices) > 0 {
if err := tx.Table("devices_new").Create(&devices).Error; err != nil {
return err
}
}
sr.logger.Debug("Dropping old devices table")
if err := tx.Migrator().DropTable(&m20260216155600.Device{}); err != nil {
return err
}
sr.logger.Debug("Renaming new device table")
if err := tx.Migrator().RenameTable("devices_new", "devices"); err != nil {
return err
}
//
wwnToUUID := make(map[string]string)
for _, device := range devices {
wwnToUUID[device.WWN] = device.ScrutinyUUID.String()
}
err := m20260216155600_ChangeInfluxDBTags(sr, ctx, wwnToUUID)
if ignorePastRetentionPolicyError(err) != nil {
return err
}
return nil
},
},
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
@@ -473,6 +522,91 @@ func ignorePastRetentionPolicyError(err error) error {
return err return err
} }
func m20260216155600_ChangeInfluxDBTags(sr *scrutinyRepository, ctx context.Context, wwnToUUID map[string]string) error {
bucket := sr.appConfig.GetString("web.influxdb.bucket")
org := sr.appConfig.GetString("web.influxdb.org")
bucketNames := []string{
bucket,
fmt.Sprintf("%s_weekly", bucket),
fmt.Sprintf("%s_monthly", bucket),
fmt.Sprintf("%s_yearly", bucket),
}
const batchSize = 1000
bucketsAPI := sr.influxClient.BucketsAPI()
for _, bucketName := range bucketNames {
newBucketName := fmt.Sprintf("%s_new", bucketName)
// Step 1: Create the new bucket. Copy retention rules from the original.
sr.logger.Debugf("Creating temporary bucket %s...", newBucketName)
oldBucket, err := bucketsAPI.FindBucketByName(ctx, bucketName)
if err != nil {
return fmt.Errorf("Failed to find bucket %s: %w", bucketName, err)
}
// Delete leftover _new bucket from a previous failed migration attempt.
if existingNew, _ := bucketsAPI.FindBucketByName(ctx, newBucketName); existingNew != nil {
sr.logger.Debugf("Found leftover bucket %s from previous migration, deleting...", newBucketName)
if err := bucketsAPI.DeleteBucket(ctx, existingNew); err != nil {
return fmt.Errorf("Failed to delete leftover bucket %s: %w", newBucketName, err)
}
}
orgObj, err := sr.influxClient.OrganizationsAPI().FindOrganizationByName(ctx, org)
if err != nil {
return fmt.Errorf("failed to find organization %s: %w", org, err)
}
newBucket, err := bucketsAPI.CreateBucketWithName(ctx, orgObj, newBucketName, oldBucket.RetentionRules...)
if err != nil {
return fmt.Errorf("failed to create bucket %s: %w", newBucketName, err)
}
for wwn, scrutinyUUID := range wwnToUUID {
sr.logger.Debugf("Copying points from %s to %s for wwn %s...", bucketName, newBucketName, wwn)
offset := 0
for ; ; offset += batchSize {
queryStr := fmt.Sprintf(`
from(bucket: "%s")
|> range(start: -10y, stop: now())
|> filter(fn: (r) => r["_measurement"] == "smart" or r["_measurement"] == "temp")
|> filter(fn: (r) => r["device_wwn"] == "%s")
|> limit(n: %d, offset: %d)
|> drop(columns: ["device_wwn"])
|> set(key: "scrutiny_uuid", value: "%s")
|> to(bucket: "%s")
`, bucketName, wwn, batchSize, offset, scrutinyUUID, newBucketName)
result, err := sr.influxQueryApi.Query(ctx, queryStr)
if err != nil {
return fmt.Errorf("failed to copy points from %s to %s for wwn %s (offset %d): %w", bucketName, newBucketName, wwn, offset, err)
}
if !result.Next() {
break
}
}
sr.logger.Debugf("Copied approx. %d points for wwn %s", offset, wwn)
}
sr.logger.Debugf("Replacing bucket %s with %s...", bucketName, newBucketName)
if err := bucketsAPI.DeleteBucket(ctx, oldBucket); err != nil {
return fmt.Errorf("Failed to delete old bucket %s: %w", bucketName, err)
}
newBucket.Name = bucketName
if _, err := bucketsAPI.UpdateBucket(ctx, newBucket); err != nil {
return fmt.Errorf("Failed to rename bucket %s to %s: %w", newBucketName, bucketName, err)
}
sr.logger.Debugf("Bucket %s migrated successfully", bucketName)
}
return nil
}
// Deprecated // Deprecated
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) { func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
//extract temperature data for every datapoint //extract temperature data for every datapoint
@@ -3,12 +3,13 @@ package database
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/api"
) )
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Tasks // Tasks
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error { func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
weeklyTaskName := "tsk-weekly-aggr" weeklyTaskName := "tsk-weekly-aggr"
weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0") weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0")
@@ -108,7 +109,7 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name stri
smart_data = from(bucket: sourceBucket) smart_data = from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"]) |> group(columns: ["scrutiny_uuid", "_field"])
non_numeric_smart_data = smart_data non_numeric_smart_data = smart_data
|> filter(fn: (r) => types.isType(v: r._value, type: "string") or types.isType(v: r._value, type: "bool")) |> filter(fn: (r) => types.isType(v: r._value, type: "string") or types.isType(v: r._value, type: "bool"))
@@ -139,20 +140,19 @@ destOrg = "%s"
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"]) |> group(columns: ["scrutiny_uuid", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp") |> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp") |> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp") |> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)`,
`,
name, name,
cron, cron,
sourceBucket, sourceBucket,
@@ -43,20 +43,19 @@ destOrg = "scrutiny"
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"]) |> group(columns: ["scrutiny_uuid", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp") |> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp") |> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp") |> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)`, influxDbScript)
`, influxDbScript)
} }
func Test_DownsampleScript_Monthly(t *testing.T) { func Test_DownsampleScript_Monthly(t *testing.T) {
@@ -94,20 +93,19 @@ destOrg = "scrutiny"
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"]) |> group(columns: ["scrutiny_uuid", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp") |> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp") |> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp") |> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)`, influxDbScript)
`, influxDbScript)
} }
func Test_DownsampleScript_Yearly(t *testing.T) { func Test_DownsampleScript_Yearly(t *testing.T) {
@@ -145,18 +143,17 @@ destOrg = "scrutiny"
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" ) |> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"]) |> group(columns: ["scrutiny_uuid", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket) from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp") |> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp") |> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp") |> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)`, influxDbScript)
`, influxDbScript)
} }
@@ -8,13 +8,14 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gofrs/uuid/v5"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
) )
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Temperature Data // Temperature Data
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error { func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory { if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table { for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
@@ -24,15 +25,15 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
} }
intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60 intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx)*intervalSec
alignedDatapointTime := datapointTime - datapointTime % intervalSec alignedDatapointTime := datapointTime - datapointTime%intervalSec
smartTemp := measurements.SmartTemperature{ smartTemp := measurements.SmartTemperature{
Date: time.Unix(alignedDatapointTime, 0), Date: time.Unix(alignedDatapointTime, 0),
Temp: temp, Temp: temp,
} }
tags, fields := smartTemp.Flatten() tags, fields := smartTemp.Flatten()
tags["device_wwn"] = wwn tags["scrutiny_uuid"] = scrutiny_uuid.String()
p := influxdb2.NewPoint("temp", p := influxdb2.NewPoint("temp",
tags, tags,
fields, fields,
@@ -44,7 +45,6 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
} }
} }
// Even if ata_sct_temperature_history is present, also add current temperature. See #824 // Even if ata_sct_temperature_history is present, also add current temperature. See #824
smartTemp := measurements.SmartTemperature{ smartTemp := measurements.SmartTemperature{
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0), Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
@@ -52,7 +52,7 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
} }
tags, fields := smartTemp.Flatten() tags, fields := smartTemp.Flatten()
tags["device_wwn"] = wwn tags["scrutiny_uuid"] = scrutiny_uuid.String()
p := influxdb2.NewPoint("temp", p := influxdb2.NewPoint("temp",
tags, tags,
fields, fields,
@@ -60,10 +60,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
return sr.influxWriteApi.WritePoint(ctx, p) return sr.influxWriteApi.WritePoint(ctx, p)
} }
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) { func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
//we can get temp history for "week", "month", DURATION_KEY_YEAR, "forever" //we can get temp history for "week", "month", DURATION_KEY_YEAR, "forever"
deviceTempHistory := map[string][]measurements.SmartTemperature{} deviceTempHistory := map[uuid.UUID][]measurements.SmartTemperature{}
//TODO: change the query range to a variable. //TODO: change the query range to a variable.
queryStr := sr.aggregateTempQuery(durationKey) queryStr := sr.aggregateTempQuery(durationKey)
@@ -73,14 +73,15 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
// Use Next() to iterate over query result lines // Use Next() to iterate over query result lines
for result.Next() { for result.Next() {
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok { if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
//check if deviceWWN has been seen and initialized already //check if scrutinyUUID has been seen and initialized already
if _, ok := deviceTempHistory[deviceWWN.(string)]; !ok { if _, ok := deviceTempHistory[scrutinyUUID]; !ok {
deviceTempHistory[deviceWWN.(string)] = []measurements.SmartTemperature{} deviceTempHistory[scrutinyUUID] = []measurements.SmartTemperature{}
} }
currentTempHistory := deviceTempHistory[deviceWWN.(string)] currentTempHistory := deviceTempHistory[scrutinyUUID]
smartTemp := measurements.SmartTemperature{} smartTemp := measurements.SmartTemperature{}
for key, val := range result.Record().Values() { for key, val := range result.Record().Values() {
@@ -88,7 +89,7 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
} }
smartTemp.Date = result.Record().Values()["_time"].(time.Time) smartTemp.Date = result.Record().Values()["_time"].(time.Time)
currentTempHistory = append(currentTempHistory, smartTemp) currentTempHistory = append(currentTempHistory, smartTemp)
deviceTempHistory[deviceWWN.(string)] = currentTempHistory deviceTempHistory[scrutinyUUID] = currentTempHistory
} }
} }
if result.Err() != nil { if result.Err() != nil {
@@ -113,18 +114,18 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
monthData = from(bucket: "metrics_weekly") monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: now()) |> range(start: -1mo, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
union(tables: [weekData, monthData]) union(tables: [weekData, monthData])
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> sort(columns: ["_time"], desc: false) |> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols() |> schema.fieldsAsCols()
@@ -148,7 +149,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
`|> filter(fn: (r) => r["_measurement"] == "temp" )`, `|> filter(fn: (r) => r["_measurement"] == "temp" )`,
fmt.Sprintf(`|> aggregateWindow(every: %s, fn: mean, createEmpty: false)`, durationResolution), fmt.Sprintf(`|> aggregateWindow(every: %s, fn: mean, createEmpty: false)`, durationResolution),
`|> group(columns: ["device_wwn"])`, `|> group(columns: ["scrutiny_uuid"])`,
`|> toInt()`, `|> toInt()`,
"", "",
}...) }...)
@@ -164,7 +165,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
} else { } else {
partialQueryStr = append(partialQueryStr, []string{ partialQueryStr = append(partialQueryStr, []string{
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
`|> group(columns: ["device_wwn"])`, `|> group(columns: ["scrutiny_uuid"])`,
`|> sort(columns: ["_time"], desc: false)`, `|> sort(columns: ["_time"], desc: false)`,
"|> schema.fieldsAsCols()", "|> schema.fieldsAsCols()",
}...) }...)
@@ -32,7 +32,7 @@ weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
weekData weekData
@@ -64,18 +64,18 @@ weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
monthData = from(bucket: "metrics_weekly") monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w) |> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
union(tables: [weekData, monthData]) union(tables: [weekData, monthData])
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> sort(columns: ["_time"], desc: false) |> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript) |> schema.fieldsAsCols()`, influxDbScript)
} }
@@ -104,25 +104,25 @@ weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
monthData = from(bucket: "metrics_weekly") monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w) |> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
yearData = from(bucket: "metrics_monthly") yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo) |> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
union(tables: [weekData, monthData, yearData]) union(tables: [weekData, monthData, yearData])
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> sort(columns: ["_time"], desc: false) |> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript) |> schema.fieldsAsCols()`, influxDbScript)
} }
@@ -151,32 +151,32 @@ weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now()) |> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
monthData = from(bucket: "metrics_weekly") monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w) |> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
yearData = from(bucket: "metrics_monthly") yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo) |> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
foreverData = from(bucket: "metrics_yearly") foreverData = from(bucket: "metrics_yearly")
|> range(start: -10y, stop: -1y) |> range(start: -10y, stop: -1y)
|> filter(fn: (r) => r["_measurement"] == "temp" ) |> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> toInt() |> toInt()
union(tables: [weekData, monthData, yearData, foreverData]) union(tables: [weekData, monthData, yearData, foreverData])
|> group(columns: ["device_wwn"]) |> group(columns: ["scrutiny_uuid"])
|> sort(columns: ["_time"], desc: false) |> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript) |> schema.fieldsAsCols()`, influxDbScript)
} }
+8 -4
View File
@@ -207,10 +207,10 @@ type SmartInfo struct {
ID int `json:"id"` ID int `json:"id"`
SubsystemID int `json:"subsystem_id"` SubsystemID int `json:"subsystem_id"`
} `json:"nvme_pci_vendor"` } `json:"nvme_pci_vendor"`
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"` NvmeIeeeOuiIdentifier uint32 `json:"nvme_ieee_oui_identifier"`
NvmeTotalCapacity int64 `json:"nvme_total_capacity"` NvmeTotalCapacity int64 `json:"nvme_total_capacity"`
NvmeControllerID int `json:"nvme_controller_id"` NvmeControllerID int `json:"nvme_controller_id"`
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"` NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
NvmeNamespaces []struct { NvmeNamespaces []struct {
ID int `json:"id"` ID int `json:"id"`
Size struct { Size struct {
@@ -226,6 +226,10 @@ type SmartInfo struct {
Bytes int64 `json:"bytes"` Bytes int64 `json:"bytes"`
} `json:"utilization"` } `json:"utilization"`
FormattedLbaSize int `json:"formatted_lba_size"` FormattedLbaSize int `json:"formatted_lba_size"`
Eui64 struct {
Oui uint32 `json:"oui"`
ExtId uint64 `json:"ext_id"`
} `json:"eui64"`
} `json:"nvme_namespaces"` } `json:"nvme_namespaces"`
NvmeSmartHealthInformationLog NvmeSmartHealthInformationLog `json:"nvme_smart_health_information_log"` NvmeSmartHealthInformationLog NvmeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
+5 -2
View File
@@ -1,9 +1,11 @@
package models package models
import ( import (
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"time" "github.com/gofrs/uuid/v5"
) )
type DeviceWrapper struct { type DeviceWrapper struct {
@@ -19,7 +21,7 @@ type Device struct {
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt *time.Time DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"` WWN string `json:"wwn"`
DeviceName string `json:"device_name"` DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"` DeviceUUID string `json:"device_uuid"`
@@ -45,6 +47,7 @@ type Device struct {
// Data set by Scrutiny // Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"` DeviceStatus pkg.DeviceStatus `json:"device_status"`
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"`
} }
func (dv *Device) IsAta() bool { func (dv *Device) IsAta() bool {
+3 -1
View File
@@ -1,10 +1,12 @@
package models package models
import ( import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"time" "time"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
) )
// This is used in server_test.go
type DeviceSummaryWrapper struct { type DeviceSummaryWrapper struct {
Success bool `json:"success"` Success bool `json:"success"`
Errors []error `json:"errors"` Errors []error `json:"errors"`
@@ -10,11 +10,13 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/gofrs/uuid/v5"
) )
type Smart struct { type Smart struct {
Date time.Time `json:"date"` Date time.Time `json:"date"`
DeviceWWN string `json:"device_wwn"` //(tag) DeviceWWN string `json:"device_wwn` // deprecated
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"` //(tag)
DeviceProtocol string `json:"device_protocol"` DeviceProtocol string `json:"device_protocol"`
//Metrics (fields) //Metrics (fields)
@@ -31,7 +33,7 @@ type Smart struct {
func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{}) { func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{}) {
tags = map[string]string{ tags = map[string]string{
"device_wwn": sm.DeviceWWN, "scrutiny_uuid": sm.ScrutinyUUID.String(),
"device_protocol": sm.DeviceProtocol, "device_protocol": sm.DeviceProtocol,
} }
@@ -53,10 +55,15 @@ func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{
func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) { func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
//go though the massive map returned from influxdb. If a key is associated with the Smart struct, assign it. If it starts with "attr.*" group it by attributeId, and pass to attribute inflate. //go though the massive map returned from influxdb. If a key is associated with the Smart struct, assign it. If it starts with "attr.*" group it by attributeId, and pass to attribute inflate.
scrutiny_uuid, err := uuid.FromString(attrs["scrutiny_uuid"].(string))
if err != nil {
return nil, err
}
sm := Smart{ sm := Smart{
//required fields //required fields
Date: attrs["_time"].(time.Time), Date: attrs["_time"].(time.Time),
DeviceWWN: attrs["device_wwn"].(string), ScrutinyUUID: scrutiny_uuid,
DeviceProtocol: attrs["device_protocol"].(string), DeviceProtocol: attrs["device_protocol"].(string),
Attributes: map[string]SmartAttribute{}, Attributes: map[string]SmartAttribute{},
@@ -112,14 +119,14 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
} }
log.Printf("Found Smart Device (%s) Attributes (%v)", sm.DeviceWWN, len(sm.Attributes)) log.Printf("Found Smart Device (%s) Attributes (%v)", sm.ScrutinyUUID, len(sm.Attributes))
return &sm, nil return &sm, nil
} }
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries) // Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error { func (sm *Smart) FromCollectorSmartInfo(scrutiny_uuid uuid.UUID, info collector.SmartInfo) error {
sm.DeviceWWN = wwn sm.ScrutinyUUID = scrutiny_uuid
sm.Date = time.Unix(info.LocalTime.TimeT, 0) sm.Date = time.Unix(info.LocalTime.TimeT, 0)
//smart metrics //smart metrics
@@ -133,11 +140,12 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
sm.DeviceProtocol = info.Device.Protocol sm.DeviceProtocol = info.Device.Protocol
// process ATA/NVME/SCSI protocol data // process ATA/NVME/SCSI protocol data
sm.Attributes = map[string]SmartAttribute{} sm.Attributes = map[string]SmartAttribute{}
if sm.DeviceProtocol == pkg.DeviceProtocolAta { switch sm.DeviceProtocol {
case pkg.DeviceProtocolAta:
sm.ProcessAtaSmartInfo(info.AtaSmartAttributes.Table) sm.ProcessAtaSmartInfo(info.AtaSmartAttributes.Table)
} else if sm.DeviceProtocol == pkg.DeviceProtocolNvme { case pkg.DeviceProtocolNvme:
sm.ProcessNvmeSmartInfo(info.NvmeSmartHealthInformationLog) sm.ProcessNvmeSmartInfo(info.NvmeSmartHealthInformationLog)
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi { case pkg.DeviceProtocolScsi:
sm.ProcessScsiSmartInfo(info.ScsiGrownDefectList, info.ScsiErrorCounterLog) sm.ProcessScsiSmartInfo(info.ScsiGrownDefectList, info.ScsiErrorCounterLog)
} }
@@ -67,7 +67,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
} }
} }
//populate attribute status, using SMART Thresholds & Observed Metadata // populate attribute status, using SMART Thresholds & Observed Metadata
// Chainable // Chainable
func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute { func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
@@ -67,9 +67,8 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
} }
} }
// // populate attribute status, using SMART Thresholds & Observed Metadata
//populate attribute status, using SMART Thresholds & Observed Metadata // Chainable
//Chainable
func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute { func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
//-1 is a special number meaning no threshold. //-1 is a special number meaning no threshold.
@@ -10,15 +10,17 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gofrs/uuid/v5"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestSmart_Flatten(t *testing.T) { func TestSmart_Flatten(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
smart := measurements.Smart{ smart := measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: pkg.DeviceProtocolAta, DeviceProtocol: pkg.DeviceProtocolAta,
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -31,16 +33,17 @@ func TestSmart_Flatten(t *testing.T) {
tags, fields := smart.Flatten() tags, fields := smart.Flatten()
//assert //assert
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, tags) require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
require.Equal(t, map[string]interface{}{"power_cycle_count": int64(10), "power_on_hours": int64(10), "temp": int64(50)}, fields) require.Equal(t, map[string]interface{}{"power_cycle_count": int64(10), "power_on_hours": int64(10), "temp": int64(50)}, fields)
} }
func TestSmart_Flatten_ATA(t *testing.T) { func TestSmart_Flatten_ATA(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
smart := measurements.Smart{ smart := measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: pkg.DeviceProtocolAta, DeviceProtocol: pkg.DeviceProtocolAta,
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -72,7 +75,7 @@ func TestSmart_Flatten_ATA(t *testing.T) {
tags, fields := smart.Flatten() tags, fields := smart.Flatten()
//assert //assert
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, tags) require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
require.Equal(t, map[string]interface{}{ require.Equal(t, map[string]interface{}{
"attr.1.attribute_id": "1", "attr.1.attribute_id": "1",
"attr.1.failure_rate": float64(0), "attr.1.failure_rate": float64(0),
@@ -107,9 +110,10 @@ func TestSmart_Flatten_ATA(t *testing.T) {
func TestSmart_Flatten_SCSI(t *testing.T) { func TestSmart_Flatten_SCSI(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
smart := measurements.Smart{ smart := measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: pkg.DeviceProtocolScsi, DeviceProtocol: pkg.DeviceProtocolScsi,
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -127,7 +131,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
tags, fields := smart.Flatten() tags, fields := smart.Flatten()
//assert //assert
require.Equal(t, map[string]string{"device_protocol": "SCSI", "device_wwn": "test-wwn"}, tags) require.Equal(t, map[string]string{"device_protocol": "SCSI", "scrutiny_uuid": smartUUID.String()}, tags)
require.Equal(t, map[string]interface{}{ require.Equal(t, map[string]interface{}{
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0), "attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
@@ -145,9 +149,10 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
func TestSmart_Flatten_NVMe(t *testing.T) { func TestSmart_Flatten_NVMe(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
smart := measurements.Smart{ smart := measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: pkg.DeviceProtocolNvme, DeviceProtocol: pkg.DeviceProtocolNvme,
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -165,7 +170,7 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
tags, fields := smart.Flatten() tags, fields := smart.Flatten()
//assert //assert
require.Equal(t, map[string]string{"device_protocol": "NVMe", "device_wwn": "test-wwn"}, tags) require.Equal(t, map[string]string{"device_protocol": "NVMe", "scrutiny_uuid": smartUUID.String()}, tags)
require.Equal(t, map[string]interface{}{ require.Equal(t, map[string]interface{}{
"attr.available_spare.attribute_id": "available_spare", "attr.available_spare.attribute_id": "available_spare",
"attr.available_spare.failure_rate": float64(0), "attr.available_spare.failure_rate": float64(0),
@@ -182,9 +187,10 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
func TestNewSmartFromInfluxDB_ATA(t *testing.T) { func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
attrs := map[string]interface{}{ attrs := map[string]interface{}{
"_time": timeNow, "_time": timeNow,
"device_wwn": "test-wwn", "scrutiny_uuid": smartUUID.String(),
"device_protocol": pkg.DeviceProtocolAta, "device_protocol": pkg.DeviceProtocolAta,
"attr.1.attribute_id": "1", "attr.1.attribute_id": "1",
"attr.1.failure_rate": float64(0), "attr.1.failure_rate": float64(0),
@@ -209,7 +215,7 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, &measurements.Smart{ require.Equal(t, &measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: "ATA", DeviceProtocol: "ATA",
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -230,9 +236,10 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
func TestNewSmartFromInfluxDB_NVMe(t *testing.T) { func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
attrs := map[string]interface{}{ attrs := map[string]interface{}{
"_time": timeNow, "_time": timeNow,
"device_wwn": "test-wwn", "scrutiny_uuid": smartUUID.String(),
"device_protocol": pkg.DeviceProtocolNvme, "device_protocol": pkg.DeviceProtocolNvme,
"attr.available_spare.attribute_id": "available_spare", "attr.available_spare.attribute_id": "available_spare",
"attr.available_spare.failure_rate": float64(0), "attr.available_spare.failure_rate": float64(0),
@@ -253,7 +260,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, &measurements.Smart{ require.Equal(t, &measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: "NVMe", DeviceProtocol: "NVMe",
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -269,9 +276,10 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
func TestNewSmartFromInfluxDB_SCSI(t *testing.T) { func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
//setup //setup
timeNow := time.Now() timeNow := time.Now()
smartUUID := uuid.Must(uuid.NewV4())
attrs := map[string]interface{}{ attrs := map[string]interface{}{
"_time": timeNow, "_time": timeNow,
"device_wwn": "test-wwn", "scrutiny_uuid": smartUUID.String(),
"device_protocol": pkg.DeviceProtocolScsi, "device_protocol": pkg.DeviceProtocolScsi,
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0), "attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
@@ -292,7 +300,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, &measurements.Smart{ require.Equal(t, &measurements.Smart{
Date: timeNow, Date: timeNow,
DeviceWWN: "test-wwn", ScrutinyUUID: smartUUID,
DeviceProtocol: "SCSI", DeviceProtocol: "SCSI",
Temp: 50, Temp: 50,
PowerOnHours: 10, PowerOnHours: 10,
@@ -320,11 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status) require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
require.Equal(t, 18, len(smartMdl.Attributes)) require.Equal(t, 18, len(smartMdl.Attributes))
@@ -352,11 +361,12 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusFailedSmart, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedSmart, smartMdl.Status)
require.Equal(t, 0, len(smartMdl.Attributes)) require.Equal(t, 0, len(smartMdl.Attributes))
} }
@@ -376,11 +386,12 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusFailedScrutiny|pkg.DeviceStatusFailedSmart, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedScrutiny|pkg.DeviceStatusFailedSmart, smartMdl.Status)
require.Equal(t, 17, len(smartMdl.Attributes)) require.Equal(t, 17, len(smartMdl.Attributes))
} }
@@ -400,11 +411,12 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(), require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
"scrutiny should detect that %d failed (status: %d, %s)", "scrutiny should detect that %d failed (status: %d, %s)",
@@ -433,11 +445,12 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status) require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(), require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
"scrutiny should detect that %s failed (status: %d, %s)", "scrutiny should detect that %s failed (status: %d, %s)",
@@ -464,11 +477,12 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status) require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
require.Equal(t, 16, len(smartMdl.Attributes)) require.Equal(t, 16, len(smartMdl.Attributes))
@@ -491,11 +505,12 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
//test //test
smartMdl := measurements.Smart{} smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) smartUUID := uuid.Must(uuid.NewV4())
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
//assert //assert
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN) require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status) require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
require.Equal(t, 13, len(smartMdl.Attributes)) require.Equal(t, 13, len(smartMdl.Attributes))
+3 -2
View File
@@ -22,6 +22,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nicholas-fedor/shoutrrr" "github.com/nicholas-fedor/shoutrrr"
shoutrrrTypes "github.com/nicholas-fedor/shoutrrr/pkg/types" shoutrrrTypes "github.com/nicholas-fedor/shoutrrr/pkg/types"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@@ -32,7 +33,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure" const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes) // ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool { func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, scrutiny_uuid uuid.UUID, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool {
// 1. check if the device is healthy // 1. check if the device is healthy
if device.DeviceStatus == pkg.DeviceStatusPassed { if device.DeviceStatus == pkg.DeviceStatusPassed {
return false return false
@@ -100,7 +101,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
var lastPoints []measurements.Smart var lastPoints []measurements.Smart
var err error var err error
if !repeatNotifications { if !repeatNotifications {
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes) lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, scrutiny_uuid, database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
if err == nil || len(lastPoints) < 1 { if err == nil || len(lastPoints) < 1 {
logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.") logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.")
} }
+28 -15
View File
@@ -12,6 +12,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
@@ -26,11 +27,12 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) { func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
@@ -42,10 +44,11 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) { func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
@@ -57,10 +60,11 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdSmart statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) { func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
@@ -72,10 +76,11 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdScrutiny statusThreshold := pkg.MetricsStatusThresholdScrutiny
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) { func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
@@ -91,11 +96,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) { func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
@@ -114,11 +120,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) { func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
@@ -134,11 +141,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) { func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
@@ -154,11 +162,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) { func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
@@ -177,11 +186,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
}} }}
statusThreshold := pkg.MetricsStatusThresholdSmart statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) { func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
t.Parallel() t.Parallel()
@@ -196,12 +206,13 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1) fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) { func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
@@ -217,12 +228,13 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1) fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
//assert //assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
} }
func TestShouldNotify_NoRepeat(t *testing.T) { func TestShouldNotify_NoRepeat(t *testing.T) {
t.Parallel() t.Parallel()
@@ -238,12 +250,13 @@ func TestShouldNotify_NoRepeat(t *testing.T) {
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
scrutinyUUID := uuid.Must(uuid.NewV4())
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1) fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
//assert //assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase)) require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
} }
func TestNewPayload(t *testing.T) { func TestNewPayload(t *testing.T) {
@@ -1,17 +1,26 @@
package handler package handler
import ( import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
) )
func ArchiveDevice(c *gin.Context) { func ArchiveDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true) scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
if err != nil {
logger.Errorln("Invalid scrutiny uuid", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = deviceRepo.UpdateDeviceArchived(c, scrutiny_uuid, true)
if err != nil { if err != nil {
logger.Errorln("An error occurred while archiving device", err) logger.Errorln("An error occurred while archiving device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -1,17 +1,24 @@
package handler package handler
import ( import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
) )
func DeleteDevice(c *gin.Context) { func DeleteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
err := deviceRepo.DeleteDevice(c, c.Param("wwn")) if err != nil {
logger.Errorln("Invalid scrutiny uuid", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = deviceRepo.DeleteDevice(c, scrutiny_uuid)
if err != nil { if err != nil {
logger.Errorln("An error occurred while deleting device", err) logger.Errorln("An error occurred while deleting device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -6,14 +6,20 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func GetDeviceDetails(c *gin.Context) { func GetDeviceDetails(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn")) if err != nil {
logger.Errorln("Invalid scrutiny uuid", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
device, err := deviceRepo.GetDeviceDetails(c, scrutiny_uuid)
if err != nil { if err != nil {
logger.Errorln("An error occurred while retrieving device details", err) logger.Errorln("An error occurred while retrieving device details", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -25,7 +31,7 @@ func GetDeviceDetails(c *gin.Context) {
durationKey = "forever" durationKey = "forever"
} }
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, 0, 0, nil) smartResults, err := deviceRepo.GetSmartAttributeHistory(c, scrutiny_uuid, durationKey, 0, 0, nil)
if err != nil { if err != nil {
logger.Errorln("An error occurred while retrieving device smart results", err) logger.Errorln("An error occurred while retrieving device smart results", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -1,10 +1,11 @@
package handler package handler
import ( import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
) )
func GetDevicesSummary(c *gin.Context) { func GetDevicesSummary(c *gin.Context) {
@@ -1,12 +1,13 @@
package handler package handler
import ( import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
) )
// register devices that are detected by various collectors. // register devices that are detected by various collectors.
@@ -23,9 +24,9 @@ func RegisterDevices(c *gin.Context) {
return return
} }
//filter any device with empty wwn (they are invalid) // Filter any device without a scrutiny UUID. This should never happen...
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool { detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
return len(dev.WWN) > 0 return !dev.ScrutinyUUID.IsNil()
}) })
errs := []error{} errs := []error{}
@@ -1,17 +1,24 @@
package handler package handler
import ( import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
) )
func UnarchiveDevice(c *gin.Context) { func UnarchiveDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false) if err != nil {
logger.Errorln("Invalid scrutiny uuid", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = deviceRepo.UpdateDeviceArchived(c, scrutiny_uuid, false)
if err != nil { if err != nil {
logger.Errorln("An error occurred while unarchiving device", err) logger.Errorln("An error occurred while unarchiving device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -10,6 +10,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/notify" "github.com/analogj/scrutiny/webapp/backend/pkg/notify"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -22,12 +23,15 @@ func UploadDeviceMetrics(c *gin.Context) {
//appConfig := c.MustGet("CONFIG").(config.Interface) //appConfig := c.MustGet("CONFIG").(config.Interface)
if c.Param("wwn") == "" { scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
c.JSON(http.StatusBadRequest, gin.H{"success": false}) if err != nil {
logger.Errorln("Invalid scrutiny uuid", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
} }
var collectorSmartData collector.SmartInfo var collectorSmartData collector.SmartInfo
err := c.BindJSON(&collectorSmartData) err = c.BindJSON(&collectorSmartData)
if err != nil { if err != nil {
logger.Errorln("Cannot parse SMART data", err) logger.Errorln("Cannot parse SMART data", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -35,7 +39,7 @@ func UploadDeviceMetrics(c *gin.Context) {
} }
//update the device information if necessary //update the device information if necessary
updatedDevice, err := deviceRepo.UpdateDevice(c, c.Param("wwn"), collectorSmartData) updatedDevice, err := deviceRepo.UpdateDevice(c, scrutiny_uuid, collectorSmartData)
if err != nil { if err != nil {
logger.Errorln("An error occurred while updating device data from smartctl metrics:", err) logger.Errorln("An error occurred while updating device data from smartctl metrics:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -43,7 +47,7 @@ func UploadDeviceMetrics(c *gin.Context) {
} }
// insert smart info // insert smart info
smartData, err := deviceRepo.SaveSmartAttributes(c, c.Param("wwn"), collectorSmartData) smartData, err := deviceRepo.SaveSmartAttributes(c, scrutiny_uuid, collectorSmartData)
if err != nil { if err != nil {
logger.Errorln("An error occurred while saving smartctl metrics", err) logger.Errorln("An error occurred while saving smartctl metrics", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -52,7 +56,7 @@ func UploadDeviceMetrics(c *gin.Context) {
if smartData.Status != pkg.DeviceStatusPassed { if smartData.Status != pkg.DeviceStatusPassed {
//there is a failure detected by Scrutiny, update the device status on the homepage. //there is a failure detected by Scrutiny, update the device status on the homepage.
updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status) updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, scrutiny_uuid, smartData.Status)
if err != nil { if err != nil {
logger.Errorln("An error occurred while updating device status", err) logger.Errorln("An error occurred while updating device status", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -61,7 +65,7 @@ func UploadDeviceMetrics(c *gin.Context) {
} }
// save smart temperature data (ignore failures) // save smart temperature data (ignore failures)
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY))) err = deviceRepo.SaveSmartTemperature(c, scrutiny_uuid, updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
if err != nil { if err != nil {
logger.Errorln("An error occurred while saving smartctl temp data", err) logger.Errorln("An error occurred while saving smartctl temp data", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -73,6 +77,7 @@ func UploadDeviceMetrics(c *gin.Context) {
logger, logger,
updatedDevice, updatedDevice,
smartData, smartData,
scrutiny_uuid,
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))), pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))), pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)), appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)),
+13 -12
View File
@@ -2,6 +2,10 @@ package web
import ( import (
"fmt" "fmt"
"net/http"
"path/filepath"
"strings"
"github.com/analogj/go-util/utils" "github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors" "github.com/analogj/scrutiny/webapp/backend/pkg/errors"
@@ -9,9 +13,6 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware" "github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
"path/filepath"
"strings"
) )
type AppEngine struct { type AppEngine struct {
@@ -37,15 +38,15 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
api.GET("/health", handler.HealthCheck) api.GET("/health", handler.HealthCheck)
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly
api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown) api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data api.POST("/device/:scrutiny_uuid/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests) api.POST("/device/:scrutiny_uuid/selftest", handler.UploadDeviceSelfTests)
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details api.GET("/device/:scrutiny_uuid/details", handler.GetDeviceDetails) //used by Details
api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device api.POST("/device/:scrutiny_uuid/archive", handler.ArchiveDevice) //used by UI to archive device
api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device api.POST("/device/:scrutiny_uuid/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device api.DELETE("/device/:scrutiny_uuid", handler.DeleteDevice) //used by UI to delete device
api.GET("/settings", handler.GetSettings) //used to get settings api.GET("/settings", handler.GetSettings) //used to get settings
api.POST("/settings", handler.SaveSettings) //used to save settings api.POST("/settings", handler.SaveSettings) //used to save settings
+16 -10
View File
@@ -19,6 +19,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "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/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/web" "github.com/analogj/scrutiny/webapp/backend/pkg/web"
"github.com/gofrs/uuid/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -35,7 +36,7 @@ docker run --rm -it -p 8086:8086 \
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \ -e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \ -e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \ -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
influxdb:2.0 influxdb:2.2
*/ */
//func TestMain(m *testing.M) { //func TestMain(m *testing.M) {
@@ -216,7 +217,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
require.Equal(suite.T(), 200, wr.Code) require.Equal(suite.T(), 200, wr.Code)
mr := httptest.NewRecorder() mr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/9a4d34b5-b2ee-51ef-8506-90eea09be417/smart", metricsfile)
router.ServeHTTP(mr, req) router.ServeHTTP(mr, req)
require.Equal(suite.T(), 200, mr.Code) require.Equal(suite.T(), 200, mr.Code)
@@ -275,28 +276,31 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
router.ServeHTTP(wr, req) router.ServeHTTP(wr, req)
require.Equal(suite.T(), 200, wr.Code) require.Equal(suite.T(), 200, wr.Code)
// NOTE: The scrutiny_uuid's below must come from devicesfile because those get inserted into the database.
// They don't match the scrutiny_uuid that would be derived from the smart info files because the drives
// in those files don't match those in the registration. Currently, scrutiny does not reconcile the two.
mr := httptest.NewRecorder() mr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/smart", metricsfile)
router.ServeHTTP(mr, req) router.ServeHTTP(mr, req)
require.Equal(suite.T(), 200, mr.Code) require.Equal(suite.T(), 200, mr.Code)
fr := httptest.NewRecorder() fr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ec3183/smart", failfile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/3ea22b35-682b-49fb-a655-abffed108e48/smart", failfile)
router.ServeHTTP(fr, req) router.ServeHTTP(fr, req)
require.Equal(suite.T(), 200, fr.Code) require.Equal(suite.T(), 200, fr.Code)
nr := httptest.NewRecorder() nr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5002538e40a22954/smart", nvmefile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/d8796fe7-2422-520c-8991-e970993dad3e/smart", nvmefile)
router.ServeHTTP(nr, req) router.ServeHTTP(nr, req)
require.Equal(suite.T(), 200, nr.Code) require.Equal(suite.T(), 200, nr.Code)
sr := httptest.NewRecorder() sr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca252c859cc/smart", scsifile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/00328b73-9f8a-53ad-8f20-8d0b1be00f47/smart", scsifile)
router.ServeHTTP(sr, req) router.ServeHTTP(sr, req)
require.Equal(suite.T(), 200, sr.Code) require.Equal(suite.T(), 200, sr.Code)
s2r := httptest.NewRecorder() s2r := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ebc248/smart", scsi2file) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/e5ccc378-24fc-5a9d-b1ce-8732096a9ea5/smart", scsi2file)
router.ServeHTTP(s2r, req) router.ServeHTTP(s2r, req)
require.Equal(suite.T(), 200, s2r.Code) require.Equal(suite.T(), 200, s2r.Code)
@@ -555,7 +559,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
require.Equal(suite.T(), 200, wr.Code) require.Equal(suite.T(), 200, wr.Code)
mr := httptest.NewRecorder() mr := httptest.NewRecorder()
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/a4c8e8ed-11a0-4c97-9bba-306440f1b944/smart", metricsfile) req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/bde1d2d2-7e5c-525a-8327-6adbfa382637/smart", metricsfile)
router.ServeHTTP(mr, req) router.ServeHTTP(mr, req)
require.Equal(suite.T(), 200, mr.Code) require.Equal(suite.T(), 200, mr.Code)
@@ -568,6 +572,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
//assert //assert
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN) deviceUUIDString := "bde1d2d2-7e5c-525a-8327-6adbfa382637"
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus) deviceUUID := uuid.Must(uuid.FromString(deviceUUIDString))
require.Equal(suite.T(), deviceUUID, deviceSummary.Data.Summary[deviceUUIDString].Device.ScrutinyUUID)
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary[deviceUUIDString].Device.DeviceStatus)
} }
@@ -14,7 +14,8 @@
"form_factor": "", "form_factor": "",
"smart_support": false, "smart_support": false,
"device_protocol": "NVMe", "device_protocol": "NVMe",
"device_type": "nvme" "device_type": "nvme",
"scrutiny_uuid": "bde1d2d2-7e5c-525a-8327-6adbfa382637"
} }
] ]
} }
+17 -10
View File
@@ -12,27 +12,29 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 500107862016, "capacity": 500107862016,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c"
}, },
{ {
"wwn": "0x5000cca264eb01d7", "wwn": "0x5000cca264eb01d7",
"device_name": "sdb", "device_name": "sdb",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
"interface_speed": "", "interface_speed": "",
"serial_number": "9RK1XXXXX", "serial_number": "9RK1XXXX",
"firmware": "", "firmware": "",
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 14000519643136, "capacity": 14000519643136,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "3ea22b35-682b-49fb-a655-abffed108e48"
}, },
{ {
"wwn": "0x5000cca264ec3183", "wwn": "0x5000cca264ec3183",
"device_name": "sdc", "device_name": "sdc",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
"interface_speed": "", "interface_speed": "",
"serial_number": "9RK4XXXXX", "serial_number": "9RK4XXXXX",
@@ -40,7 +42,8 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 14000519643136, "capacity": 14000519643136,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "42caca8a-9b95-5c75-b059-305771a2a193"
}, },
{ {
"wwn": "0x5000cca252c859cc", "wwn": "0x5000cca252c859cc",
@@ -54,7 +57,8 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 8001563222016, "capacity": 8001563222016,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "d8796fe7-2422-520c-8991-e970993dad3e"
}, },
{ {
"wwn": "0x5000cca264ebc248", "wwn": "0x5000cca264ebc248",
@@ -68,7 +72,8 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 14000519643136, "capacity": 14000519643136,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "00328b73-9f8a-53ad-8f20-8d0b1be00f47"
}, },
{ {
"wwn": "0x50014ee20b2a72a9", "wwn": "0x50014ee20b2a72a9",
@@ -82,7 +87,8 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 6001175126016, "capacity": 6001175126016,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "e5ccc378-24fc-5a9d-b1ce-8732096a9ea5"
}, },
{ {
"wwn": "0x5000c500673e6b5f", "wwn": "0x5000c500673e6b5f",
@@ -96,7 +102,8 @@
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 6001175126016, "capacity": 6001175126016,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "acfbce7d-0e19-579b-895e-85809dab63fb"
} }
] ]
} }
@@ -4,15 +4,16 @@
"wwn": "0x5000cca264eb01d7", "wwn": "0x5000cca264eb01d7",
"device_name": "sdb", "device_name": "sdb",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
"interface_speed": "", "interface_speed": "",
"serial_number": "9RK1XXXXX", "serial_number": "9RK1XXXX",
"firmware": "", "firmware": "",
"rotational_speed": 0, "rotational_speed": 0,
"capacity": 14000519643136, "capacity": 14000519643136,
"form_factor": "", "form_factor": "",
"smart_support": false "smart_support": false,
"scrutiny_uuid": "9a4d34b5-b2ee-51ef-8506-90eea09be417"
} }
] ]
} }
+1 -1
View File
@@ -38,7 +38,7 @@ export const appRoutes: Route[] = [
// Example // Example
{path: 'dashboard', loadChildren: () => import('app/modules/dashboard/dashboard.module').then(m => m.DashboardModule)}, {path: 'dashboard', loadChildren: () => import('app/modules/dashboard/dashboard.module').then(m => m.DashboardModule)},
{path: 'device/:wwn', loadChildren: () => import('app/modules/detail/detail.module').then(m => m.DetailModule)} {path: 'device/:scrutiny_uuid', loadChildren: () => import('app/modules/detail/detail.module').then(m => m.DetailModule)}
// 404 & Catch all // 404 & Catch all
// {path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/errors/error-404/error-404.module').then(m => m.Error404Module)}, // {path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/errors/error-404/error-404.module').then(m => m.Error404Module)},
@@ -1,6 +1,7 @@
// maps to webapp/backend/pkg/models/device.go // maps to webapp/backend/pkg/models/device.go
export interface DeviceModel { export interface DeviceModel {
archived?: boolean; archived?: boolean;
scrutiny_uuid: string;
wwn: string; wwn: string;
device_name?: string; device_name?: string;
device_uuid?: string; device_uuid?: string;
@@ -4,6 +4,7 @@ import {SmartAttributeModel} from './smart-attribute-model';
export interface SmartModel { export interface SmartModel {
date: string; date: string;
device_wwn: string; device_wwn: string;
scrutiny_uuid: string;
device_protocol: string; device_protocol: string;
temp: number; temp: number;
@@ -40,7 +40,7 @@ export class DetailsMockApi implements TreoMockApi
register(): void register(): void
{ {
this._treoMockApiService this._treoMockApiService
.onGet('/api/device/0x5002538e40a22954/details') .onGet('/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/details')
.reply(() => { .reply(() => {
return [ return [
@@ -50,7 +50,7 @@ export class DetailsMockApi implements TreoMockApi
}); });
this._treoMockApiService this._treoMockApiService
.onGet('/api/device/0x5000cca264eb01d7/details') .onGet('/api/device/3ea22b35-682b-49fb-a655-abffed108e48/details')
.reply(() => { .reply(() => {
return [ return [
@@ -60,7 +60,7 @@ export class DetailsMockApi implements TreoMockApi
}); });
this._treoMockApiService this._treoMockApiService
.onGet('/api/device/0x5000cca264ec3183/details') .onGet('/api/device/42caca8a-9b95-5c75-b059-305771a2a193/details')
.reply(() => { .reply(() => {
return [ return [
@@ -70,7 +70,7 @@ export class DetailsMockApi implements TreoMockApi
}); });
this._treoMockApiService this._treoMockApiService
.onGet('/api/device/0x5000cca252c859cc/details') .onGet('/api/device/d8796fe7-2422-520c-8991-e970993dad3e/details')
.reply(() => { .reply(() => {
return [ return [
@@ -80,7 +80,17 @@ export class DetailsMockApi implements TreoMockApi
}); });
this._treoMockApiService this._treoMockApiService
.onGet('/api/device/0x5000cca264ebc248/details') .onGet('/api/device/00328b73-9f8a-53ad-8f20-8d0b1be00f47/details')
.reply(() => {
return [
200,
_.cloneDeep(sde)
];
});
this._treoMockApiService
.onGet('/api/device/e5ccc378-24fc-5a9d-b1ce-8732096a9ea5/details')
.reply(() => { .reply(() => {
return [ return [
@@ -5,6 +5,7 @@ export const sda = {
'UpdatedAt': '2021-10-24T16:37:56.981833-07:00', 'UpdatedAt': '2021-10-24T16:37:56.981833-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5002538e40a22954', 'wwn': '0x5002538e40a22954',
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
'device_name': 'sda', 'device_name': 'sda',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'Samsung_SSD_860_EVO_500GB', 'model_name': 'Samsung_SSD_860_EVO_500GB',
@@ -26,6 +27,7 @@ export const sda = {
'smart_results': [{ 'smart_results': [{
'date': '2021-10-24T23:20:44Z', 'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5002538e40a22954', 'device_wwn': '0x5002538e40a22954',
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
'device_protocol': 'NVMe', 'device_protocol': 'NVMe',
'temp': 36, 'temp': 36,
'power_on_hours': 2401, 'power_on_hours': 2401,
@@ -5,12 +5,13 @@ export const sdb = {
'UpdatedAt': '2021-10-24T17:06:39.436996-07:00', 'UpdatedAt': '2021-10-24T17:06:39.436996-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264eb01d7', 'wwn': '0x5000cca264eb01d7',
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
'device_name': 'sdb', 'device_name': 'sdb',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD140EDFZ-11A0VA0', 'model_name': 'WDC_WD140EDFZ-11A0VA0',
'interface_type': 'SCSI', 'interface_type': 'SCSI',
'interface_speed': '', 'interface_speed': '',
'serial_number': '9RK1XXXXX', 'serial_number': '9RK1XXXX',
'firmware': '81.00A81', 'firmware': '81.00A81',
'rotational_speed': 0, 'rotational_speed': 0,
'capacity': 14000519643136, 'capacity': 14000519643136,
@@ -25,6 +26,7 @@ export const sdb = {
'smart_results': [{ 'smart_results': [{
'date': '2021-10-24T20:34:04Z', 'date': '2021-10-24T20:34:04Z',
'device_wwn': '0x5000cca264eb01d7', 'device_wwn': '0x5000cca264eb01d7',
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
'device_protocol': 'ATA', 'device_protocol': 'ATA',
'temp': 32, 'temp': 32,
'power_on_hours': 1730, 'power_on_hours': 1730,
@@ -245,6 +247,7 @@ export const sdb = {
}, { }, {
'date': '2021-10-24T23:20:44Z', 'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca264eb01d7', 'device_wwn': '0x5000cca264eb01d7',
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
'device_protocol': 'ATA', 'device_protocol': 'ATA',
'temp': 32, 'temp': 32,
'power_on_hours': 1730, 'power_on_hours': 1730,
@@ -5,6 +5,7 @@ export const sdc = {
'UpdatedAt': '2021-10-24T16:37:56.74865-07:00', 'UpdatedAt': '2021-10-24T16:37:56.74865-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264ec3183', 'wwn': '0x5000cca264ec3183',
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
'device_name': 'sdc', 'device_name': 'sdc',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD140EDFZ-11A0VA0', 'model_name': 'WDC_WD140EDFZ-11A0VA0',
@@ -25,6 +26,7 @@ export const sdc = {
'smart_results': [{ 'smart_results': [{
'date': '2021-10-24T23:20:44Z', 'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca264ec3183', 'device_wwn': '0x5000cca264ec3183',
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
'device_protocol': 'ATA', 'device_protocol': 'ATA',
'temp': 25, 'temp': 25,
'power_on_hours': 65592, 'power_on_hours': 65592,
@@ -5,6 +5,7 @@ export const sdd = {
'UpdatedAt': '2021-10-24T16:37:57.013758-07:00', 'UpdatedAt': '2021-10-24T16:37:57.013758-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca252c859cc', 'wwn': '0x5000cca252c859cc',
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
'device_name': 'sdd', 'device_name': 'sdd',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD80EFAX-68LHPN0', 'model_name': 'WDC_WD80EFAX-68LHPN0',
@@ -25,6 +26,7 @@ export const sdd = {
'smart_results': [{ 'smart_results': [{
'date': '2021-10-24T23:20:44Z', 'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca252c859cc', 'device_wwn': '0x5000cca252c859cc',
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
'device_protocol': 'SCSI', 'device_protocol': 'SCSI',
'temp': 34, 'temp': 34,
'power_on_hours': 43549, 'power_on_hours': 43549,
@@ -5,6 +5,7 @@ export const sde = {
'UpdatedAt': '2021-10-24T16:40:16.495248-07:00', 'UpdatedAt': '2021-10-24T16:40:16.495248-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264ebc248', 'wwn': '0x5000cca264ebc248',
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
'device_name': 'sde', 'device_name': 'sde',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD140EDFZ-11A0VA0', 'model_name': 'WDC_WD140EDFZ-11A0VA0',
@@ -25,6 +26,7 @@ export const sde = {
'smart_results': [{ 'smart_results': [{
'date': '2021-10-24T23:20:44Z', 'date': '2021-10-24T23:20:44Z',
'device_wwn': '0x5000cca264ebc248', 'device_wwn': '0x5000cca264ebc248',
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
'device_protocol': 'SCSI', 'device_protocol': 'SCSI',
'temp': 31, 'temp': 31,
'power_on_hours': 5675, 'power_on_hours': 5675,
@@ -5,6 +5,7 @@ export const sdf = {
'UpdatedAt': '2021-06-24T21:17:31.305246-07:00', 'UpdatedAt': '2021-06-24T21:17:31.305246-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x50014ee20b2a72a9', 'wwn': '0x50014ee20b2a72a9',
'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5',
'device_name': 'sdf', 'device_name': 'sdf',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD60EFRX-68MYMN1', 'model_name': 'WDC_WD60EFRX-68MYMN1',
@@ -4,12 +4,13 @@ import * as moment from 'moment';
export const summary = { export const summary = {
'data': { 'data': {
'summary': { 'summary': {
'0x5000c500673e6b5f': { 'acfbce7d-0e19-579b-895e-85809dab63fb': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.155217-07:00', 'CreatedAt': '2021-04-30T08:17:13.155217-07:00',
'UpdatedAt': '2021-04-30T08:17:13.155217-07:00', 'UpdatedAt': '2021-04-30T08:17:13.155217-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000c500673e6b5f', 'wwn': '0x5000c500673e6b5f',
'scrutiny_uuid': 'acfbce7d-0e19-579b-895e-85809dab63fb',
'device_name': 'sdg', 'device_name': 'sdg',
'device_label': '14TB-WD-DRIVE2', 'device_label': '14TB-WD-DRIVE2',
'device_uuid': '', 'device_uuid': '',
@@ -32,12 +33,13 @@ export const summary = {
'archived': false 'archived': false
} }
}, },
'0x5000cca252c859cc': { 'd8796fe7-2422-520c-8991-e970993dad3e': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.152705-07:00', 'CreatedAt': '2021-04-30T08:17:13.152705-07:00',
'UpdatedAt': '2021-05-02T14:22:50.357164-07:00', 'UpdatedAt': '2021-05-02T14:22:50.357164-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca252c859cc', 'wwn': '0x5000cca252c859cc',
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
'device_name': 'sdd', 'device_name': 'sdd',
'device_label': '14TB-WD-DRIVE1', 'device_label': '14TB-WD-DRIVE1',
'device_uuid': '806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f', 'device_uuid': '806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f',
@@ -69,21 +71,22 @@ export const summary = {
'temp': 34 'temp': 34
}] }]
}, },
'0x5000cca264eb01d7': { '3ea22b35-682b-49fb-a655-abffed108e48': {
'device': { 'device': {
'CreatedAt': '2021-04-28T20:52:49.047154-07:00', 'CreatedAt': '2021-04-28T20:52:49.047154-07:00',
'UpdatedAt': '2021-05-02T14:22:49.86136-07:00', 'UpdatedAt': '2021-05-02T14:22:49.86136-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264eb01d7', 'wwn': '0x5000cca264eb01d7',
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
'device_name': 'sdb', 'device_name': 'sdb',
'device_label': '14TB-WD-DRIVE5', 'device_label': '14TB-WD-DRIVE5',
'device_uuid': '8125ec6d-a7e4-4950-ac84-72d6a4d67128', 'device_uuid': '8125ec6d-a7e4-4950-ac84-72d6a4d67128',
'device_serial_id': 'ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX', 'device_serial_id': 'ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXX',
'manufacturer': 'ATA', 'manufacturer': 'ATA',
'model_name': 'WDC_WD140EDFZ-11A0VA0', 'model_name': 'WDC_WD140EDFZ-11A0VA0',
'interface_type': 'SCSI', 'interface_type': 'SCSI',
'interface_speed': '', 'interface_speed': '',
'serial_number': '9RK1XXXXX', 'serial_number': '9RK1XXXX',
'firmware': '81.00A81', 'firmware': '81.00A81',
'rotational_speed': 0, 'rotational_speed': 0,
'capacity': 14000519643136, 'capacity': 14000519643136,
@@ -106,12 +109,13 @@ export const summary = {
'temp': 32 'temp': 32
}] }]
}, },
'0x5000cca264ebc248': { '00328b73-9f8a-53ad-8f20-8d0b1be00f47': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.153782-07:00', 'CreatedAt': '2021-04-30T08:17:13.153782-07:00',
'UpdatedAt': '2021-05-02T14:22:50.385282-07:00', 'UpdatedAt': '2021-05-02T14:22:50.385282-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264ebc248', 'wwn': '0x5000cca264ebc248',
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
'device_name': 'sde', 'device_name': 'sde',
'device_label': '14TB-WD-DRIVE3', 'device_label': '14TB-WD-DRIVE3',
'device_uuid': '9eb60cde-d6d0-4172-b520-b241a6a5477f', 'device_uuid': '9eb60cde-d6d0-4172-b520-b241a6a5477f',
@@ -134,12 +138,13 @@ export const summary = {
'archived': false 'archived': false
} }
}, },
'0x5000cca264ec3183': { '42caca8a-9b95-5c75-b059-305771a2a193': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.151906-07:00', 'CreatedAt': '2021-04-30T08:17:13.151906-07:00',
'UpdatedAt': '2021-05-02T14:49:51.645012-07:00', 'UpdatedAt': '2021-05-02T14:49:51.645012-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5000cca264ec3183', 'wwn': '0x5000cca264ec3183',
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
'device_name': 'sdc', 'device_name': 'sdc',
'device_label': '14TB-WD-DRIVE6', 'device_label': '14TB-WD-DRIVE6',
'device_uuid': 'e1378723-7861-49b9-8e01-0bd063f0ecdd', 'device_uuid': 'e1378723-7861-49b9-8e01-0bd063f0ecdd',
@@ -555,12 +560,13 @@ export const summary = {
'temp': 39 'temp': 39
}] }]
}, },
'0x50014ee20b2a72a9': { 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.15451-07:00', 'CreatedAt': '2021-04-30T08:17:13.15451-07:00',
'UpdatedAt': '2021-04-30T08:17:13.15451-07:00', 'UpdatedAt': '2021-04-30T08:17:13.15451-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x50014ee20b2a72a9', 'wwn': '0x50014ee20b2a72a9',
'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5',
'device_name': 'sdf', 'device_name': 'sdf',
'device_label': '8.0TB-WD-4', 'device_label': '8.0TB-WD-4',
'device_uuid': 'fc684dcc-aa2f-44f3-a958-d302dc7dd46d', 'device_uuid': 'fc684dcc-aa2f-44f3-a958-d302dc7dd46d',
@@ -583,12 +589,13 @@ export const summary = {
'archived': false 'archived': false
} }
}, },
'0x5002538e40a22954': { 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c': {
'device': { 'device': {
'CreatedAt': '2021-04-30T08:17:13.150792-07:00', 'CreatedAt': '2021-04-30T08:17:13.150792-07:00',
'UpdatedAt': '2021-05-02T14:22:50.330706-07:00', 'UpdatedAt': '2021-05-02T14:22:50.330706-07:00',
'DeletedAt': null, 'DeletedAt': null,
'wwn': '0x5002538e40a22954', 'wwn': '0x5002538e40a22954',
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
'device_name': 'sda', 'device_name': 'sda',
'device_label': '', 'device_label': '',
'device_uuid': '', 'device_uuid': '',
@@ -28,7 +28,7 @@ describe('DashboardDeviceArchiveDialogComponent', () => {
], ],
providers: [ providers: [
{provide: MatDialogRef, useValue: matDialogRefSpy}, {provide: MatDialogRef, useValue: matDialogRefSpy},
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}}, {provide: MAT_DIALOG_DATA, useValue: {scrutiny_uuid: 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', title: 'my-test-device-title'}},
{provide: DashboardDeviceArchiveDialogService, useValue: dashboardDeviceArchiveDialogServiceSpy} {provide: DashboardDeviceArchiveDialogService, useValue: dashboardDeviceArchiveDialogServiceSpy}
], ],
declarations: [DashboardDeviceArchiveDialogComponent] declarations: [DashboardDeviceArchiveDialogComponent]
@@ -56,7 +56,7 @@ describe('DashboardDeviceArchiveDialogComponent', () => {
dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true})); dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true}));
component.onArchiveClick() component.onArchiveClick()
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn'); expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c');
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count()) expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count())
.withContext('one call') .withContext('one call')
.toBe(1); .toBe(1);
@@ -11,7 +11,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit {
constructor( constructor(
public dialogRef: MatDialogRef<DashboardDeviceArchiveDialogComponent>, public dialogRef: MatDialogRef<DashboardDeviceArchiveDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, @Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string},
private _archiveService: DashboardDeviceArchiveDialogService, private _archiveService: DashboardDeviceArchiveDialogService,
) { ) {
} }
@@ -20,7 +20,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit {
} }
onArchiveClick(): void { onArchiveClick(): void {
this._archiveService.archiveDevice(this.data.wwn) this._archiveService.archiveDevice(this.data.scrutiny_uuid)
.subscribe((data) => { .subscribe((data) => {
this.dialogRef.close(data); this.dialogRef.close(data);
}); });
@@ -26,13 +26,13 @@ export class DashboardDeviceArchiveDialogService
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
archiveDevice(wwn: string): Observable<any> archiveDevice(scrutiny_uid: string): Observable<any>
{ {
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {}); return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/archive`, {});
} }
unarchiveDevice(wwn: string): Observable<any> unarchiveDevice(scrutiny_uid: string): Observable<any>
{ {
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {}); return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/unarchive`, {});
} }
} }
@@ -28,7 +28,7 @@ describe('DashboardDeviceDeleteDialogComponent', () => {
], ],
providers: [ providers: [
{provide: MatDialogRef, useValue: matDialogRefSpy}, {provide: MatDialogRef, useValue: matDialogRefSpy},
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}}, {provide: MAT_DIALOG_DATA, useValue: {scrutiny_uuid: 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', title: 'my-test-device-title'}},
{provide: DashboardDeviceDeleteDialogService, useValue: dashboardDeviceDeleteDialogServiceSpy} {provide: DashboardDeviceDeleteDialogService, useValue: dashboardDeviceDeleteDialogServiceSpy}
], ],
declarations: [DashboardDeviceDeleteDialogComponent] declarations: [DashboardDeviceDeleteDialogComponent]
@@ -56,7 +56,7 @@ describe('DashboardDeviceDeleteDialogComponent', () => {
dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true})); dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true}));
component.onDeleteClick() component.onDeleteClick()
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn'); expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c');
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count()) expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count())
.withContext('one call') .withContext('one call')
.toBe(1); .toBe(1);
@@ -11,7 +11,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit {
constructor( constructor(
public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>, public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, @Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string},
private _deleteService: DashboardDeviceDeleteDialogService, private _deleteService: DashboardDeviceDeleteDialogService,
) { ) {
} }
@@ -20,7 +20,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit {
} }
onDeleteClick(): void { onDeleteClick(): void {
this._deleteService.deleteDevice(this.data.wwn) this._deleteService.deleteDevice(this.data.scrutiny_uuid)
.subscribe((data) => { .subscribe((data) => {
this.dialogRef.close(data); this.dialogRef.close(data);
}); });
@@ -27,8 +27,8 @@ export class DashboardDeviceDeleteDialogService
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
deleteDevice(wwn: string): Observable<any> deleteDevice(scrutiny_uuid: string): Observable<any>
{ {
return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {}); return this._httpClient.delete( `${getBasePath()}/api/device/${scrutiny_uuid}`, {});
} }
} }
@@ -16,7 +16,7 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="flex flex-col"> <div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn" <a [routerLink]="'/device/'+ deviceSummary.device.scrutiny_uuid"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a> class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart"> <div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }} Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
@@ -30,7 +30,7 @@
<mat-icon [svgIcon]="'more_vert'"></mat-icon> <mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button> </button>
<mat-menu #previousStatementMenu="matMenu"> <mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn"> <a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.scrutiny_uuid">
<span class="flex items-center"> <span class="flex items-center">
<mat-icon class="icon-size-20 mr-3" <mat-icon class="icon-size-20 mr-3"
[svgIcon]="'assessment'"></mat-icon> [svgIcon]="'assessment'"></mat-icon>
@@ -75,22 +75,22 @@ export class DashboardDeviceComponent implements OnInit {
openArchiveDialog(): void { openArchiveDialog(): void {
if(this.deviceSummary.device.archived){ if(this.deviceSummary.device.archived){
this._archiveService.unarchiveDevice(this.deviceSummary.device.wwn).subscribe((result) => { this._archiveService.unarchiveDevice(this.deviceSummary.device.scrutiny_uuid).subscribe((result) => {
if(result) { if(result) {
this.deviceUnarchived.emit(this.deviceSummary.device.wwn) this.deviceUnarchived.emit(this.deviceSummary.device.scrutiny_uuid)
} }
}) })
return; return;
} }
const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, { const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, {
data: { data: {
wwn: this.deviceSummary.device.wwn, scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display) title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
} }
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
if(result) { if(result) {
this.deviceArchived.emit(this.deviceSummary.device.wwn); this.deviceArchived.emit(this.deviceSummary.device.scrutiny_uuid);
} }
}) })
} }
@@ -99,7 +99,7 @@ export class DashboardDeviceComponent implements OnInit {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, { const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px', // width: '250px',
data: { data: {
wwn: this.deviceSummary.device.wwn, scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display) title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
} }
}); });
@@ -107,7 +107,7 @@ export class DashboardDeviceComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed', result); console.log('The dialog was closed', result);
if (result.success) { if (result.success) {
this.deviceDeleted.emit(this.deviceSummary.device.wwn) this.deviceDeleted.emit(this.deviceSummary.device.scrutiny_uuid)
} }
}); });
} }
@@ -100,10 +100,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
this.summaryData = data; this.summaryData = data;
// generate group data. // generate group data.
for (const wwn in this.summaryData) { for (const scrutiny_uuid in this.summaryData) {
const hostid = this.summaryData[wwn].device.host_id const hostid = this.summaryData[scrutiny_uuid].device.host_id
const hostDeviceList = this.hostGroups[hostid] || [] const hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn) hostDeviceList.push(scrutiny_uuid)
this.hostGroups[hostid] = hostDeviceList this.hostGroups[hostid] = hostDeviceList
} }
console.log(this.hostGroups) console.log(this.hostGroups)
@@ -145,8 +145,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
console.log('DEVICE DATA SUMMARY', this.summaryData) console.log('DEVICE DATA SUMMARY', this.summaryData)
for (const wwn in this.summaryData) { for (const scrutiny_uuid in this.summaryData) {
const deviceSummary = this.summaryData[wwn] const deviceSummary = this.summaryData[scrutiny_uuid]
if (!deviceSummary.temp_history) { if (!deviceSummary.temp_history) {
continue continue
} }
@@ -241,11 +241,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods // @ Public methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] { deviceSummariesForHostGroup(hostGroupScrutinyUUIDs: string[]): DeviceSummaryModel[] {
const deviceSummaries: DeviceSummaryModel[] = [] const deviceSummaries: DeviceSummaryModel[] = []
for (const wwn of hostGroupWWNs) { for (const scrutiny_uuid of hostGroupScrutinyUUIDs) {
if (this.summaryData[wwn]) { if (this.summaryData[scrutiny_uuid]) {
deviceSummaries.push(this.summaryData[wwn]) deviceSummaries.push(this.summaryData[scrutiny_uuid])
} }
} }
return deviceSummaries return deviceSummaries
@@ -259,16 +259,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}); });
} }
onDeviceDeleted(wwn: string): void { onDeviceDeleted(scrutiny_uuid: string): void {
delete this.summaryData[wwn] // remove the device from the summary list. delete this.summaryData[scrutiny_uuid] // remove the device from the summary list.
} }
onDeviceArchived(wwn: string): void { onDeviceArchived(scrutiny_uuid: string): void {
this.summaryData[wwn].device.archived = true; this.summaryData[scrutiny_uuid].device.archived = true;
} }
onDeviceUnarchived(wwn: string): void { onDeviceUnarchived(scrutiny_uuid: string): void {
this.summaryData[wwn].device.archived = false; this.summaryData[scrutiny_uuid].device.archived = false;
} }
/* /*
@@ -286,9 +286,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
.subscribe((tempHistoryData) => { .subscribe((tempHistoryData) => {
// given a list of device temp history, override the data in the "summary" object. // given a list of device temp history, override the data in the "summary" object.
for (const wwn in this.summaryData) { for (const scrutiny_uuid in this.summaryData) {
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`) // console.log(`Updating ${scrutiny_uuid}, length: ${this.data.data.summary[scrutiny_uuid].temp_history.length}`)
this.summaryData[wwn].temp_history = tempHistoryData[wwn] || [] this.summaryData[scrutiny_uuid].temp_history = tempHistoryData[scrutiny_uuid] || []
} }
// Prepare the chart series data // Prepare the chart series data
@@ -98,6 +98,10 @@
<div>{{device?.serial_number}}</div> <div>{{device?.serial_number}}</div>
<div class="text-secondary text-md">Serial Number</div> <div class="text-secondary text-md">Serial Number</div>
</div> </div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.scrutiny_uuid}}</div>
<div class="text-secondary text-md">Scrutiny UUID</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1"> <div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.wwn}}</div> <div>{{device?.wwn}}</div>
<div class="text-secondary text-md">LU WWN Device Id</div> <div class="text-secondary text-md">LU WWN Device Id</div>
@@ -30,6 +30,6 @@ export class DetailResolver implements Resolve<any> {
* @param state * @param state
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> {
return this._detailService.getData(route.params.wwn); return this._detailService.getData(route.params.scrutiny_uuid);
} }
} }
@@ -42,8 +42,8 @@ export class DetailService {
/** /**
* Get data * Get data
*/ */
getData(wwn): Observable<DeviceDetailsResponseWrapper> { getData(scrutiny_uuid): Observable<DeviceDetailsResponseWrapper> {
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( return this._httpClient.get(getBasePath() + `/api/device/${scrutiny_uuid}/details`).pipe(
tap((response: DeviceDetailsResponseWrapper) => { tap((response: DeviceDetailsResponseWrapper) => {
this._data.next(response); this._data.next(response);
}) })
@@ -37,7 +37,7 @@ export class DeviceTitlePipe implements PipeTransform {
} }
static deviceTitleWithFallback(device: DeviceModel, titleType: string): string { static deviceTitleWithFallback(device: DeviceModel, titleType: string): string {
console.log(`Displaying Device ${device.wwn} with: ${titleType}`) console.log(`Displaying Device ${device.scrutiny_uuid} with: ${titleType}`)
const titleParts = [] const titleParts = []
if (device.host_id) titleParts.push(device.host_id) if (device.host_id) titleParts.push(device.host_id)