diff --git a/collector/pkg/collector/metrics.go b/collector/pkg/collector/metrics.go index 10bb98d..cd38443 100644 --- a/collector/pkg/collector/metrics.go +++ b/collector/pkg/collector/metrics.go @@ -15,6 +15,7 @@ import ( "github.com/analogj/scrutiny/collector/pkg/detect" "github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/models" + "github.com/gofrs/uuid/v5" "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -64,9 +65,9 @@ func (mc *MetricsCollector) Run() error { return err } - //filter any device with empty wwn (they are invalid) - detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool { - return len(dev.WWN) > 0 + // Remove any device without a scrutiny UUID, but this should never happen... + detectedStorageDevices := lo.Filter(rawDetectedStorageDevices, func(device models.Device, _ int) bool { + return device.ScrutinyUUID.IsNil() }) 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 //wg.Add(1) //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 { 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(deviceWWN string, deviceName string, deviceType string) { +func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string, deviceType string) { //defer wg.Done() - if len(deviceWWN) == 0 { - mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName) + if scrutiny_uuid.IsNil() { + mc.logger.Errorf("no scrutiny UUID was created for %s. Skipping collection for this device (no data association possible).\n", deviceName) return } 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 mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName) mc.LogSmartctlExitCode(exitError.ExitCode()) - mc.Publish(deviceWWN, resultBytes) + mc.Publish(scrutiny_uuid, resultBytes) } else { mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName) mc.logger.Errorf("ERROR MESSAGE: %v", err) @@ -149,19 +150,19 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT return } else { //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 { - mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN) +func (mc *MetricsCollector) Publish(scrutinyUuid uuid.UUID, payload []byte) error { + mc.logger.Infof("Publishing smartctl results for %s\n", scrutinyUuid) 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)) 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 } defer resp.Body.Close() diff --git a/collector/pkg/detect/detect.go b/collector/pkg/detect/detect.go index 25f2d97..e23c133 100644 --- a/collector/pkg/detect/detect.go +++ b/collector/pkg/detect/detect.go @@ -101,15 +101,11 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error { 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) } else { - d.Logger.Info("Using WWN Fallback") + d.Logger.Debug("Using WWN Fallback") d.wwnFallback(device) } - if len(device.WWN) == 0 { - // no WWN populated after WWN lookup and fallback. we need to throw an error - 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) - } + + device.ScrutinyUUID = GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN) return nil } diff --git a/collector/pkg/detect/devices_darwin.go b/collector/pkg/detect/devices_darwin.go index 62a6ba2..f691d39 100644 --- a/collector/pkg/detect/devices_darwin.go +++ b/collector/pkg/detect/devices_darwin.go @@ -1,10 +1,11 @@ package detect import ( + "strings" + "github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/models" "github.com/jaypipes/ghw" - "strings" ) func DevicePrefix() string { @@ -89,7 +90,7 @@ func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]models.D return missingDevices, nil } -//WWN values NVMe and SCSI +// WWN values NVMe and SCSI func (d *Detect) wwnFallback(detectedDevice *models.Device) { block, err := ghw.Block() 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. detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) } diff --git a/collector/pkg/detect/devices_freebsd.go b/collector/pkg/detect/devices_freebsd.go index ebe8e88..9515988 100644 --- a/collector/pkg/detect/devices_freebsd.go +++ b/collector/pkg/detect/devices_freebsd.go @@ -1,10 +1,11 @@ package detect import ( + "strings" + "github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/models" "github.com/jaypipes/ghw" - "strings" ) func DevicePrefix() string { @@ -27,7 +28,7 @@ func (d *Detect) Start() ([]models.Device, error) { return detectedDevices, nil } -//WWN values NVMe and SCSI +// WWN values NVMe and SCSI func (d *Detect) wwnFallback(detectedDevice *models.Device) { block, err := ghw.Block() 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. detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) } diff --git a/collector/pkg/detect/devices_linux.go b/collector/pkg/detect/devices_linux.go index 4d2a020..79c862e 100644 --- a/collector/pkg/detect/devices_linux.go +++ b/collector/pkg/detect/devices_linux.go @@ -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. detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) } diff --git a/collector/pkg/detect/devices_windows.go b/collector/pkg/detect/devices_windows.go index 296578e..6b8ebe6 100644 --- a/collector/pkg/detect/devices_windows.go +++ b/collector/pkg/detect/devices_windows.go @@ -3,7 +3,6 @@ package detect import ( "github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/models" - "strings" ) func DevicePrefix() string { @@ -26,14 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) { return detectedDevices, nil } -//WWN values NVMe and SCSI +// WWN values NVMe and SCSI func (d *Detect) wwnFallback(detectedDevice *models.Device) { - - //fallback to serial number - if len(detectedDevice.WWN) == 0 { - detectedDevice.WWN = detectedDevice.SerialNumber - } - - //wwn must always be lowercase. - detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) + // No fallback on windows } diff --git a/collector/pkg/detect/scrutiny_uuid.go b/collector/pkg/detect/scrutiny_uuid.go new file mode 100644 index 0000000..84cd751 --- /dev/null +++ b/collector/pkg/detect/scrutiny_uuid.go @@ -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) +} diff --git a/collector/pkg/detect/scrutiny_uuid_test.go b/collector/pkg/detect/scrutiny_uuid_test.go new file mode 100644 index 0000000..93b4596 --- /dev/null +++ b/collector/pkg/detect/scrutiny_uuid_test.go @@ -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") +} diff --git a/collector/pkg/models/device.go b/collector/pkg/models/device.go index f06aec8..b524373 100644 --- a/collector/pkg/models/device.go +++ b/collector/pkg/models/device.go @@ -1,12 +1,17 @@ package models +import ( + "github.com/gofrs/uuid/v5" +) + type Device struct { - WWN string `json:"wwn"` + ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"` + 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"` + 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"` diff --git a/go.mod b/go.mod index 093d37c..6a7436f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-gormigrate/gormigrate/v2 v2.1.5 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/jaypipes/ghw v0.21.2 github.com/nicholas-fedor/shoutrrr v0.13.2 diff --git a/go.sum b/go.sum index 08d5047..67fdebf 100644 --- a/go.sum +++ b/go.sum @@ -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-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= 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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/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/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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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/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-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/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/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/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/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/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-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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= 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/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/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/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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/webapp/backend/pkg/constants.go b/webapp/backend/pkg/constants.go index a82c9c3..009f13f 100644 --- a/webapp/backend/pkg/constants.go +++ b/webapp/backend/pkg/constants.go @@ -4,8 +4,9 @@ const DeviceProtocolAta = "ATA" const DeviceProtocolScsi = "SCSI" const DeviceProtocolNvme = "NVMe" -//go:generate stringer -type=AttributeStatus // AttributeStatus bitwise flag, 1,2,4,8,16,32,etc +// +//go:generate stringer -type=AttributeStatus type AttributeStatus uint8 const ( @@ -23,8 +24,9 @@ func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b & func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag } 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 +// +//go:generate stringer -type=DeviceStatus type DeviceStatus uint8 const ( diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index 7e3dfcf..251f170 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -7,6 +7,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/gofrs/uuid/v5" ) // Create mock using: @@ -17,19 +18,19 @@ type DeviceRepo interface { RegisterDevice(ctx context.Context, dev models.Device) error GetDevices(ctx context.Context) ([]models.Device, error) - UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) - UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) - GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) - UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error - DeleteDevice(ctx context.Context, wwn string) error + UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) + UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) + GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) + UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error + DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error - SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) - GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) + SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (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) - GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) + GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error) + GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) LoadSettings(ctx context.Context) (*models.Settings, error) SaveSettings(ctx context.Context, settings models.Settings) error diff --git a/webapp/backend/pkg/database/migrations/m20250221084400/device.go b/webapp/backend/pkg/database/migrations/m20250221084400/device.go index b0a2e5a..bcde8ca 100644 --- a/webapp/backend/pkg/database/migrations/m20250221084400/device.go +++ b/webapp/backend/pkg/database/migrations/m20250221084400/device.go @@ -1,10 +1,12 @@ package m20250221084400 import ( - "github.com/analogj/scrutiny/webapp/backend/pkg" "time" + + "github.com/analogj/scrutiny/webapp/backend/pkg" ) +// Deprecated: m20250221084400.Device is deprecated, only used by db migrations type Device struct { Archived bool `json:"archived"` //GORM attributes, see: http://gorm.io/docs/conventions.html diff --git a/webapp/backend/pkg/database/migrations/m20260216155600/device.go b/webapp/backend/pkg/database/migrations/m20260216155600/device.go new file mode 100644 index 0000000..cb0526a --- /dev/null +++ b/webapp/backend/pkg/database/migrations/m20260216155600/device.go @@ -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"` +} diff --git a/webapp/backend/pkg/database/mock/mock_database.go b/webapp/backend/pkg/database/mock/mock_database.go index 6d09cd6..85420d0 100644 --- a/webapp/backend/pkg/database/mock/mock_database.go +++ b/webapp/backend/pkg/database/mock/mock_database.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // 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 @@ -12,6 +17,7 @@ import ( models "github.com/analogj/scrutiny/webapp/backend/pkg/models" collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + uuid "github.com/gofrs/uuid/v5" gomock "go.uber.org/mock/gomock" ) @@ -19,6 +25,7 @@ import ( type MockDeviceRepo struct { ctrl *gomock.Controller recorder *MockDeviceRepoMockRecorder + isgomock struct{} } // 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)) } -// 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. -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() - ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn) + ret := m.ctrl.Call(m, "DeleteDevice", ctx, scrutiny_uuid) ret0, _ := ret[0].(error) return ret0 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn) + ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, scrutiny_uuid) ret0, _ := ret[0].(models.Device) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. @@ -105,52 +98,52 @@ func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error } // 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() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx) } // 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() - 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) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() 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) return ret0, ret1 } // 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() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey) } // 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() 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) return ret0, ret1 } // 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() 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. -func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call { +func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() 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. -func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call { +func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() 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. -func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call { +func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev any) *gomock.Call { mr.mock.ctrl.T.Helper() 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. -func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call { +func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings) } // 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() - ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData) + ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, scrutiny_uuid, collectorSmartData) ret0, _ := ret[0].(measurements.Smart) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - 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) return ret0 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData) + ret := m.ctrl.Call(m, "UpdateDevice", ctx, scrutiny_uuid, collectorSmartData) ret0, _ := ret[0].(models.Device) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status) + ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, scrutiny_uuid, status) ret0, _ := ret[0].(models.Device) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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) } diff --git a/webapp/backend/pkg/database/scrutiny_repository.go b/webapp/backend/pkg/database/scrutiny_repository.go index b9b76cc..1cc3dc2 100644 --- a/webapp/backend/pkg/database/scrutiny_repository.go +++ b/webapp/backend/pkg/database/scrutiny_repository.go @@ -13,10 +13,12 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/glebarez/sqlite" + "github.com/gofrs/uuid/v5" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/domain" "github.com/sirupsen/logrus" + "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 -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) if err != nil { return nil, err } - summaries := map[string]*models.DeviceSummary{} + summaries := map[uuid.UUID]*models.DeviceSummary{} for _, device := range devices { - summaries[device.WWN] = &models.DeviceSummary{Device: device} + summaries[device.ScrutinyUUID] = &models.DeviceSummary{Device: device} } // 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") |> last() |> schema.fieldsAsCols() - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) weeklyData = from(bucket: bucketBaseName + "_weekly") |> 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") |> last() |> schema.fieldsAsCols() - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) monthlyData = from(bucket: bucketBaseName + "_monthly") |> 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") |> last() |> schema.fieldsAsCols() - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) yearlyData = from(bucket: bucketBaseName + "_yearly") |> 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") |> last() |> schema.fieldsAsCols() - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) union(tables: [dailyData, weeklyData, monthlyData, yearlyData]) |> sort(columns: ["_time"], desc: false) - |> group(columns: ["device_wwn"]) - |> last(column: "device_wwn") + |> group(columns: ["scrutiny_uuid"]) + |> last(column: "scrutiny_uuid") |> yield(name: "last") `, 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. //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 - if _, exists := summaries[deviceWWN.(string)]; !exists { - summaries[deviceWWN.(string)] = &models.DeviceSummary{} + //ensure summaries is intialized for this scrutiny_uuid + if _, exists := summaries[scrutinyUUID]; !exists { + summaries[scrutinyUUID] = &models.DeviceSummary{} } - summaries[deviceWWN.(string)].SmartResults = &models.SmartSummary{ + summaries[scrutinyUUID].SmartResults = &models.SmartSummary{ Temp: result.Record().Values()["temp"].(int64), PowerOnHours: result.Record().Values()["power_on_hours"].(int64), 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("Error: %v", err) } - for wwn, tempHistory := range deviceTempHistory { - summaries[wwn].TempHistory = tempHistory + for scutiny_uuid, tempHistory := range deviceTempHistory { + summaries[scutiny_uuid].TempHistory = tempHistory } return summaries, nil diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 399132a..0279106 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -8,6 +8,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" + "github.com/gofrs/uuid/v5" "gorm.io/gorm/clause" ) @@ -19,7 +20,7 @@ import ( // update device fields that may change: (DeviceType, HostID) func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error { 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"}), }).Create(&dev).Error; err != nil { return err @@ -38,9 +39,9 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device, } // 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 - 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) } @@ -53,9 +54,9 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll } // 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 - 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) } @@ -63,12 +64,12 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string 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 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 } @@ -76,17 +77,17 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) } // 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 - 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 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 { - if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil { +func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error { + if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Delete(&models.Device{}).Error; err != nil { return err } @@ -99,14 +100,14 @@ func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) erro } 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( ctx, sr.appConfig.GetString("web.influxdb.org"), bucket, time.Now().AddDate(-10, 0, 0), time.Now(), - fmt.Sprintf(`device_wwn="%s"`, wwn), + fmt.Sprintf(`scrutiny_uuid="%s"`, scrutiny_uuid.String()), ); err != nil { return err } diff --git a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go index b9db103..852b1b7 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go @@ -8,17 +8,18 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/gofrs/uuid/v5" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" log "github.com/sirupsen/logrus" ) -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 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{} - err := deviceSmartData.FromCollectorSmartInfo(wwn, collectorSmartData) + err := deviceSmartData.FromCollectorSmartInfo(scrutiny_uuid, collectorSmartData) if err != nil { sr.logger.Errorln("Could not process SMART metrics", 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. // 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) -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 //TODO: change the filter startrange to a real number. // Get parser flux query result //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) smartResults := []measurements.Smart{} @@ -100,7 +101,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking, 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") |> range(start: -1w, stop: now()) |> 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) |> schema.fieldsAsCols() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: -1w) |> 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) |> schema.fieldsAsCols() yearData = from(bucket: "metrics_monthly") |> range(start: -1y, stop: -1mo) |> 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) |> schema.fieldsAsCols() foreverData = from(bucket: "metrics_yearly") |> range(start: -10y, stop: -1y) |> 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) |> schema.fieldsAsCols() @@ -150,7 +151,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration if len(nestedDurationKeys) == 1 { //there's only one bucket being queried, no need to union, just aggregate the dataset and return 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]), `|> sort(columns: ["_time"], desc: true)`, `|> yield()`, @@ -165,9 +166,9 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration if selectEntries > 0 { // 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 - subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes)) + subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes)) } 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...) @@ -184,7 +185,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration 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) 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(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), `|> 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)`) diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index 6f39434..dd6bbe5 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -7,12 +7,14 @@ import ( "strconv" "time" + "github.com/analogj/scrutiny/collector/pkg/detect" "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/m20220503120000" "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/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/collector" "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 }, }, + { + 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 { @@ -473,6 +522,91 @@ func ignorePastRetentionPolicyError(err error) error { 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 func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) { //extract temperature data for every datapoint diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks.go b/webapp/backend/pkg/database/scrutiny_repository_tasks.go index 82b6040..ab66405 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks.go @@ -3,12 +3,13 @@ package database import ( "context" "fmt" + "github.com/influxdata/influxdb-client-go/v2/api" ) -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Tasks -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error { weeklyTaskName := "tsk-weekly-aggr" 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) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "smart" ) - |> group(columns: ["device_wwn", "_field"]) + |> group(columns: ["scrutiny_uuid", "_field"]) non_numeric_smart_data = smart_data |> 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) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "smart" ) -|> group(columns: ["device_wwn", "_field"]) +|> group(columns: ["scrutiny_uuid", "_field"]) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> set(key: "_measurement", value: "temp") |> set(key: "_field", value: "temp") -|> to(bucket: destBucket, org: destOrg) - `, +|> to(bucket: destBucket, org: destOrg)`, name, cron, sourceBucket, diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go index 941008f..a28cb11 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go @@ -43,20 +43,19 @@ destOrg = "scrutiny" from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "smart" ) -|> group(columns: ["device_wwn", "_field"]) +|> group(columns: ["scrutiny_uuid", "_field"]) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> set(key: "_measurement", value: "temp") |> set(key: "_field", value: "temp") -|> to(bucket: destBucket, org: destOrg) - `, influxDbScript) +|> to(bucket: destBucket, org: destOrg)`, influxDbScript) } func Test_DownsampleScript_Monthly(t *testing.T) { @@ -94,20 +93,19 @@ destOrg = "scrutiny" from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "smart" ) -|> group(columns: ["device_wwn", "_field"]) +|> group(columns: ["scrutiny_uuid", "_field"]) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> set(key: "_measurement", value: "temp") |> set(key: "_field", value: "temp") -|> to(bucket: destBucket, org: destOrg) - `, influxDbScript) +|> to(bucket: destBucket, org: destOrg)`, influxDbScript) } func Test_DownsampleScript_Yearly(t *testing.T) { @@ -145,18 +143,17 @@ destOrg = "scrutiny" from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "smart" ) -|> group(columns: ["device_wwn", "_field"]) +|> group(columns: ["scrutiny_uuid", "_field"]) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> set(key: "_measurement", value: "temp") |> set(key: "_field", value: "temp") -|> to(bucket: destBucket, org: destOrg) - `, influxDbScript) +|> to(bucket: destBucket, org: destOrg)`, influxDbScript) } diff --git a/webapp/backend/pkg/database/scrutiny_repository_temperature.go b/webapp/backend/pkg/database/scrutiny_repository_temperature.go index 0f72d96..e40e5f2 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_temperature.go +++ b/webapp/backend/pkg/database/scrutiny_repository_temperature.go @@ -8,13 +8,14 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/gofrs/uuid/v5" influxdb2 "github.com/influxdata/influxdb-client-go/v2" ) // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 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 { 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 - datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec - alignedDatapointTime := datapointTime - datapointTime % intervalSec + datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx)*intervalSec + alignedDatapointTime := datapointTime - datapointTime%intervalSec smartTemp := measurements.SmartTemperature{ Date: time.Unix(alignedDatapointTime, 0), Temp: temp, } tags, fields := smartTemp.Flatten() - tags["device_wwn"] = wwn + tags["scrutiny_uuid"] = scrutiny_uuid.String() p := influxdb2.NewPoint("temp", tags, 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 smartTemp := measurements.SmartTemperature{ 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["device_wwn"] = wwn + tags["scrutiny_uuid"] = scrutiny_uuid.String() p := influxdb2.NewPoint("temp", tags, fields, @@ -60,10 +60,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri 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" - deviceTempHistory := map[string][]measurements.SmartTemperature{} + deviceTempHistory := map[uuid.UUID][]measurements.SmartTemperature{} //TODO: change the query range to a variable. queryStr := sr.aggregateTempQuery(durationKey) @@ -73,14 +73,15 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du // Use Next() to iterate over query result lines 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 - if _, ok := deviceTempHistory[deviceWWN.(string)]; !ok { - deviceTempHistory[deviceWWN.(string)] = []measurements.SmartTemperature{} + //check if scrutinyUUID has been seen and initialized already + if _, ok := deviceTempHistory[scrutinyUUID]; !ok { + deviceTempHistory[scrutinyUUID] = []measurements.SmartTemperature{} } - currentTempHistory := deviceTempHistory[deviceWWN.(string)] + currentTempHistory := deviceTempHistory[scrutinyUUID] smartTemp := measurements.SmartTemperature{} 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) currentTempHistory = append(currentTempHistory, smartTemp) - deviceTempHistory[deviceWWN.(string)] = currentTempHistory + deviceTempHistory[scrutinyUUID] = currentTempHistory } } if result.Err() != nil { @@ -113,18 +114,18 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string { |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) |> toInt() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) |> toInt() union(tables: [weekData, monthData]) - |> group(columns: ["device_wwn"]) + |> group(columns: ["scrutiny_uuid"]) |> sort(columns: ["_time"], desc: false) |> schema.fieldsAsCols() @@ -148,7 +149,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string { fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]), `|> filter(fn: (r) => r["_measurement"] == "temp" )`, fmt.Sprintf(`|> aggregateWindow(every: %s, fn: mean, createEmpty: false)`, durationResolution), - `|> group(columns: ["device_wwn"])`, + `|> group(columns: ["scrutiny_uuid"])`, `|> toInt()`, "", }...) @@ -164,7 +165,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string { } else { partialQueryStr = append(partialQueryStr, []string{ fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")), - `|> group(columns: ["device_wwn"])`, + `|> group(columns: ["scrutiny_uuid"])`, `|> sort(columns: ["_time"], desc: false)`, "|> schema.fieldsAsCols()", }...) diff --git a/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go b/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go index c344479..5fd2133 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go +++ b/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go @@ -32,7 +32,7 @@ weekData = from(bucket: "metrics") |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() weekData @@ -64,18 +64,18 @@ weekData = from(bucket: "metrics") |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: -1w) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() union(tables: [weekData, monthData]) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> sort(columns: ["_time"], desc: false) |> schema.fieldsAsCols()`, influxDbScript) } @@ -104,25 +104,25 @@ weekData = from(bucket: "metrics") |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: -1w) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() yearData = from(bucket: "metrics_monthly") |> range(start: -1y, stop: -1mo) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() union(tables: [weekData, monthData, yearData]) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> sort(columns: ["_time"], desc: false) |> schema.fieldsAsCols()`, influxDbScript) } @@ -151,32 +151,32 @@ weekData = from(bucket: "metrics") |> range(start: -1w, stop: now()) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() monthData = from(bucket: "metrics_weekly") |> range(start: -1mo, stop: -1w) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() yearData = from(bucket: "metrics_monthly") |> range(start: -1y, stop: -1mo) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() foreverData = from(bucket: "metrics_yearly") |> range(start: -10y, stop: -1y) |> filter(fn: (r) => r["_measurement"] == "temp" ) |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> toInt() union(tables: [weekData, monthData, yearData, foreverData]) -|> group(columns: ["device_wwn"]) +|> group(columns: ["scrutiny_uuid"]) |> sort(columns: ["_time"], desc: false) |> schema.fieldsAsCols()`, influxDbScript) } diff --git a/webapp/backend/pkg/models/collector/smart.go b/webapp/backend/pkg/models/collector/smart.go index 8c640dc..4f383d2 100644 --- a/webapp/backend/pkg/models/collector/smart.go +++ b/webapp/backend/pkg/models/collector/smart.go @@ -207,10 +207,10 @@ type SmartInfo struct { ID int `json:"id"` SubsystemID int `json:"subsystem_id"` } `json:"nvme_pci_vendor"` - NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"` - NvmeTotalCapacity int64 `json:"nvme_total_capacity"` - NvmeControllerID int `json:"nvme_controller_id"` - NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"` + NvmeIeeeOuiIdentifier uint32 `json:"nvme_ieee_oui_identifier"` + NvmeTotalCapacity int64 `json:"nvme_total_capacity"` + NvmeControllerID int `json:"nvme_controller_id"` + NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"` NvmeNamespaces []struct { ID int `json:"id"` Size struct { @@ -226,6 +226,10 @@ type SmartInfo struct { Bytes int64 `json:"bytes"` } `json:"utilization"` FormattedLbaSize int `json:"formatted_lba_size"` + Eui64 struct { + Oui uint32 `json:"oui"` + ExtId uint64 `json:"ext_id"` + } `json:"eui64"` } `json:"nvme_namespaces"` NvmeSmartHealthInformationLog NvmeSmartHealthInformationLog `json:"nvme_smart_health_information_log"` diff --git a/webapp/backend/pkg/models/device.go b/webapp/backend/pkg/models/device.go index a891652..4028af9 100644 --- a/webapp/backend/pkg/models/device.go +++ b/webapp/backend/pkg/models/device.go @@ -1,9 +1,11 @@ package models import ( + "time" + "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" - "time" + "github.com/gofrs/uuid/v5" ) type DeviceWrapper struct { @@ -19,7 +21,7 @@ type Device struct { UpdatedAt time.Time DeletedAt *time.Time - WWN string `json:"wwn" gorm:"primary_key"` + WWN string `json:"wwn"` DeviceName string `json:"device_name"` DeviceUUID string `json:"device_uuid"` @@ -45,6 +47,7 @@ type Device struct { // Data set by Scrutiny DeviceStatus pkg.DeviceStatus `json:"device_status"` + ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"` } func (dv *Device) IsAta() bool { diff --git a/webapp/backend/pkg/models/device_summary.go b/webapp/backend/pkg/models/device_summary.go index f4ca323..98b21de 100644 --- a/webapp/backend/pkg/models/device_summary.go +++ b/webapp/backend/pkg/models/device_summary.go @@ -1,10 +1,12 @@ package models import ( - "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "time" + + "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" ) +// This is used in server_test.go type DeviceSummaryWrapper struct { Success bool `json:"success"` Errors []error `json:"errors"` diff --git a/webapp/backend/pkg/models/measurements/smart.go b/webapp/backend/pkg/models/measurements/smart.go index 8978e1a..88fdb3f 100644 --- a/webapp/backend/pkg/models/measurements/smart.go +++ b/webapp/backend/pkg/models/measurements/smart.go @@ -10,11 +10,13 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" + "github.com/gofrs/uuid/v5" ) type Smart struct { 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"` //Metrics (fields) @@ -31,7 +33,7 @@ type Smart struct { func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{}) { tags = map[string]string{ - "device_wwn": sm.DeviceWWN, + "scrutiny_uuid": sm.ScrutinyUUID.String(), "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) { //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{ //required fields Date: attrs["_time"].(time.Time), - DeviceWWN: attrs["device_wwn"].(string), + ScrutinyUUID: scrutiny_uuid, DeviceProtocol: attrs["device_protocol"].(string), 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 } // Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries) -func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error { - sm.DeviceWWN = wwn +func (sm *Smart) FromCollectorSmartInfo(scrutiny_uuid uuid.UUID, info collector.SmartInfo) error { + sm.ScrutinyUUID = scrutiny_uuid sm.Date = time.Unix(info.LocalTime.TimeT, 0) //smart metrics @@ -133,11 +140,12 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er sm.DeviceProtocol = info.Device.Protocol // process ATA/NVME/SCSI protocol data sm.Attributes = map[string]SmartAttribute{} - if sm.DeviceProtocol == pkg.DeviceProtocolAta { + switch sm.DeviceProtocol { + case pkg.DeviceProtocolAta: sm.ProcessAtaSmartInfo(info.AtaSmartAttributes.Table) - } else if sm.DeviceProtocol == pkg.DeviceProtocolNvme { + case pkg.DeviceProtocolNvme: sm.ProcessNvmeSmartInfo(info.NvmeSmartHealthInformationLog) - } else if sm.DeviceProtocol == pkg.DeviceProtocolScsi { + case pkg.DeviceProtocolScsi: sm.ProcessScsiSmartInfo(info.ScsiGrownDefectList, info.ScsiErrorCounterLog) } diff --git a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go index 4e251d0..7a43629 100644 --- a/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_nvme_attribute.go @@ -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 func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute { diff --git a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go index 347a3f6..2ba2305 100644 --- a/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go +++ b/webapp/backend/pkg/models/measurements/smart_scsci_attribute.go @@ -67,9 +67,8 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) { } } -// -//populate attribute status, using SMART Thresholds & Observed Metadata -//Chainable +// populate attribute status, using SMART Thresholds & Observed Metadata +// Chainable func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute { //-1 is a special number meaning no threshold. diff --git a/webapp/backend/pkg/models/measurements/smart_test.go b/webapp/backend/pkg/models/measurements/smart_test.go index 3935310..42134cd 100644 --- a/webapp/backend/pkg/models/measurements/smart_test.go +++ b/webapp/backend/pkg/models/measurements/smart_test.go @@ -10,15 +10,17 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" + "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/require" ) func TestSmart_Flatten(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) smart := measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: pkg.DeviceProtocolAta, Temp: 50, PowerOnHours: 10, @@ -31,16 +33,17 @@ func TestSmart_Flatten(t *testing.T) { tags, fields := smart.Flatten() //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) } func TestSmart_Flatten_ATA(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) smart := measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: pkg.DeviceProtocolAta, Temp: 50, PowerOnHours: 10, @@ -72,7 +75,7 @@ func TestSmart_Flatten_ATA(t *testing.T) { tags, fields := smart.Flatten() //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{}{ "attr.1.attribute_id": "1", "attr.1.failure_rate": float64(0), @@ -107,9 +110,10 @@ func TestSmart_Flatten_ATA(t *testing.T) { func TestSmart_Flatten_SCSI(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) smart := measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: pkg.DeviceProtocolScsi, Temp: 50, PowerOnHours: 10, @@ -127,7 +131,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) { tags, fields := smart.Flatten() //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{}{ "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "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) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) smart := measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: pkg.DeviceProtocolNvme, Temp: 50, PowerOnHours: 10, @@ -165,7 +170,7 @@ func TestSmart_Flatten_NVMe(t *testing.T) { tags, fields := smart.Flatten() //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{}{ "attr.available_spare.attribute_id": "available_spare", "attr.available_spare.failure_rate": float64(0), @@ -182,9 +187,10 @@ func TestSmart_Flatten_NVMe(t *testing.T) { func TestNewSmartFromInfluxDB_ATA(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) attrs := map[string]interface{}{ "_time": timeNow, - "device_wwn": "test-wwn", + "scrutiny_uuid": smartUUID.String(), "device_protocol": pkg.DeviceProtocolAta, "attr.1.attribute_id": "1", "attr.1.failure_rate": float64(0), @@ -209,7 +215,7 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) { require.NoError(t, err) require.Equal(t, &measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: "ATA", Temp: 50, PowerOnHours: 10, @@ -230,9 +236,10 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) { func TestNewSmartFromInfluxDB_NVMe(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) attrs := map[string]interface{}{ "_time": timeNow, - "device_wwn": "test-wwn", + "scrutiny_uuid": smartUUID.String(), "device_protocol": pkg.DeviceProtocolNvme, "attr.available_spare.attribute_id": "available_spare", "attr.available_spare.failure_rate": float64(0), @@ -253,7 +260,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) { require.NoError(t, err) require.Equal(t, &measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: "NVMe", Temp: 50, PowerOnHours: 10, @@ -269,9 +276,10 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) { func TestNewSmartFromInfluxDB_SCSI(t *testing.T) { //setup timeNow := time.Now() + smartUUID := uuid.Must(uuid.NewV4()) attrs := map[string]interface{}{ "_time": timeNow, - "device_wwn": "test-wwn", + "scrutiny_uuid": smartUUID.String(), "device_protocol": pkg.DeviceProtocolScsi, "attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast", "attr.read_errors_corrected_by_eccfast.failure_rate": float64(0), @@ -292,7 +300,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) { require.NoError(t, err) require.Equal(t, &measurements.Smart{ Date: timeNow, - DeviceWWN: "test-wwn", + ScrutinyUUID: smartUUID, DeviceProtocol: "SCSI", Temp: 50, PowerOnHours: 10, @@ -320,11 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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, 18, len(smartMdl.Attributes)) @@ -352,11 +361,12 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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, 0, len(smartMdl.Attributes)) } @@ -376,11 +386,12 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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, 17, len(smartMdl.Attributes)) } @@ -400,11 +411,12 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(), "scrutiny should detect that %d failed (status: %d, %s)", @@ -433,11 +445,12 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(), "scrutiny should detect that %s failed (status: %d, %s)", @@ -464,11 +477,12 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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, 16, len(smartMdl.Attributes)) @@ -491,11 +505,12 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) { //test smartMdl := measurements.Smart{} - err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson) + smartUUID := uuid.Must(uuid.NewV4()) + err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson) //assert 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, 13, len(smartMdl.Attributes)) diff --git a/webapp/backend/pkg/notify/notify.go b/webapp/backend/pkg/notify/notify.go index 9220a3c..e777e5e 100644 --- a/webapp/backend/pkg/notify/notify.go +++ b/webapp/backend/pkg/notify/notify.go @@ -22,6 +22,7 @@ import ( "github.com/gin-gonic/gin" "github.com/nicholas-fedor/shoutrrr" shoutrrrTypes "github.com/nicholas-fedor/shoutrrr/pkg/types" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) @@ -32,7 +33,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure" const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure" // 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 if device.DeviceStatus == pkg.DeviceStatusPassed { return false @@ -100,7 +101,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me var lastPoints []measurements.Smart var err error 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 { 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.") } diff --git a/webapp/backend/pkg/notify/notify_test.go b/webapp/backend/pkg/notify/notify_test.go index 02aac4f..578bc5d 100644 --- a/webapp/backend/pkg/notify/notify_test.go +++ b/webapp/backend/pkg/notify/notify_test.go @@ -12,6 +12,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -26,11 +27,12 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) { smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -42,10 +44,11 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -57,10 +60,11 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing. smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdSmart notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -72,10 +76,11 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi smartAttrs := measurements.Smart{} statusThreshold := pkg.MetricsStatusThresholdScrutiny notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -91,11 +96,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -114,11 +120,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -134,11 +141,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs( }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -154,11 +162,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { @@ -177,11 +186,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho }} statusThreshold := pkg.MetricsStatusThresholdSmart notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl) //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) { t.Parallel() @@ -196,12 +206,13 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) { }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) 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 - 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) { @@ -217,12 +228,13 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) { }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) 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 - 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) { t.Parallel() @@ -238,12 +250,13 @@ func TestShouldNotify_NoRepeat(t *testing.T) { }} statusThreshold := pkg.MetricsStatusThresholdBoth notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll + scrutinyUUID := uuid.Must(uuid.NewV4()) mockCtrl := gomock.NewController(t) 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 - 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) { diff --git a/webapp/backend/pkg/web/handler/archive_device.go b/webapp/backend/pkg/web/handler/archive_device.go index 494dd38..1edeb3a 100644 --- a/webapp/backend/pkg/web/handler/archive_device.go +++ b/webapp/backend/pkg/web/handler/archive_device.go @@ -1,17 +1,26 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" - "net/http" ) func ArchiveDevice(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) 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 { logger.Errorln("An error occurred while archiving device", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) diff --git a/webapp/backend/pkg/web/handler/delete_device.go b/webapp/backend/pkg/web/handler/delete_device.go index f8a507d..c954065 100644 --- a/webapp/backend/pkg/web/handler/delete_device.go +++ b/webapp/backend/pkg/web/handler/delete_device.go @@ -1,17 +1,24 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" - "net/http" ) func DeleteDevice(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) - - err := deviceRepo.DeleteDevice(c, c.Param("wwn")) + 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.DeleteDevice(c, scrutiny_uuid) if err != nil { logger.Errorln("An error occurred while deleting device", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) diff --git a/webapp/backend/pkg/web/handler/get_device_details.go b/webapp/backend/pkg/web/handler/get_device_details.go index 49e48a1..4f83c59 100644 --- a/webapp/backend/pkg/web/handler/get_device_details.go +++ b/webapp/backend/pkg/web/handler/get_device_details.go @@ -6,14 +6,20 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/thresholds" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" ) func GetDeviceDetails(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) - - device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn")) + 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 + } + device, err := deviceRepo.GetDeviceDetails(c, scrutiny_uuid) if err != nil { logger.Errorln("An error occurred while retrieving device details", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -25,7 +31,7 @@ func GetDeviceDetails(c *gin.Context) { 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 { logger.Errorln("An error occurred while retrieving device smart results", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) diff --git a/webapp/backend/pkg/web/handler/get_devices_summary.go b/webapp/backend/pkg/web/handler/get_devices_summary.go index a256f4b..b8a2485 100644 --- a/webapp/backend/pkg/web/handler/get_devices_summary.go +++ b/webapp/backend/pkg/web/handler/get_devices_summary.go @@ -1,10 +1,11 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "net/http" ) func GetDevicesSummary(c *gin.Context) { diff --git a/webapp/backend/pkg/web/handler/register_devices.go b/webapp/backend/pkg/web/handler/register_devices.go index 38132ad..8ef5234 100644 --- a/webapp/backend/pkg/web/handler/register_devices.go +++ b/webapp/backend/pkg/web/handler/register_devices.go @@ -1,12 +1,13 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/gin-gonic/gin" "github.com/samber/lo" "github.com/sirupsen/logrus" - "net/http" ) // register devices that are detected by various collectors. @@ -23,9 +24,9 @@ func RegisterDevices(c *gin.Context) { 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 { - return len(dev.WWN) > 0 + return !dev.ScrutinyUUID.IsNil() }) errs := []error{} diff --git a/webapp/backend/pkg/web/handler/unarchive_device.go b/webapp/backend/pkg/web/handler/unarchive_device.go index dea8781..322472b 100644 --- a/webapp/backend/pkg/web/handler/unarchive_device.go +++ b/webapp/backend/pkg/web/handler/unarchive_device.go @@ -1,17 +1,24 @@ package handler import ( + "net/http" + "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" - "net/http" ) func UnarchiveDevice(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) - - err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false) + 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, false) if err != nil { logger.Errorln("An error occurred while unarchiving device", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) diff --git a/webapp/backend/pkg/web/handler/upload_device_metrics.go b/webapp/backend/pkg/web/handler/upload_device_metrics.go index 7f4b0d7..68c1010 100644 --- a/webapp/backend/pkg/web/handler/upload_device_metrics.go +++ b/webapp/backend/pkg/web/handler/upload_device_metrics.go @@ -10,6 +10,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/notify" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" ) @@ -22,12 +23,15 @@ func UploadDeviceMetrics(c *gin.Context) { //appConfig := c.MustGet("CONFIG").(config.Interface) - if c.Param("wwn") == "" { - c.JSON(http.StatusBadRequest, gin.H{"success": false}) + 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 } var collectorSmartData collector.SmartInfo - err := c.BindJSON(&collectorSmartData) + err = c.BindJSON(&collectorSmartData) if err != nil { logger.Errorln("Cannot parse SMART data", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -35,7 +39,7 @@ func UploadDeviceMetrics(c *gin.Context) { } //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 { logger.Errorln("An error occurred while updating device data from smartctl metrics:", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -43,7 +47,7 @@ func UploadDeviceMetrics(c *gin.Context) { } // insert smart info - smartData, err := deviceRepo.SaveSmartAttributes(c, c.Param("wwn"), collectorSmartData) + smartData, err := deviceRepo.SaveSmartAttributes(c, scrutiny_uuid, collectorSmartData) if err != nil { logger.Errorln("An error occurred while saving smartctl metrics", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -52,7 +56,7 @@ func UploadDeviceMetrics(c *gin.Context) { if smartData.Status != pkg.DeviceStatusPassed { //there is a failure detected by Scrutiny, update the device status on the homepage. - updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status) + updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, scrutiny_uuid, smartData.Status) if err != nil { logger.Errorln("An error occurred while updating device status", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -61,7 +65,7 @@ func UploadDeviceMetrics(c *gin.Context) { } // 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 { logger.Errorln("An error occurred while saving smartctl temp data", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -73,6 +77,7 @@ func UploadDeviceMetrics(c *gin.Context) { logger, updatedDevice, smartData, + scrutiny_uuid, 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))), appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)), diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 38383c9..0c1baf3 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -2,6 +2,10 @@ package web import ( "fmt" + "net/http" + "path/filepath" + "strings" + "github.com/analogj/go-util/utils" "github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/errors" @@ -9,9 +13,6 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "net/http" - "path/filepath" - "strings" ) type AppEngine struct { @@ -37,15 +38,15 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine { api.GET("/health", handler.HealthCheck) 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.GET("/summary", handler.GetDevicesSummary) //used by Dashboard - 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/:wwn/selftest", handler.UploadDeviceSelfTests) - api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details - api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device - api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device - api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device + 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/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown) + api.POST("/device/:scrutiny_uuid/smart", handler.UploadDeviceMetrics) //used by Collector to upload data + api.POST("/device/:scrutiny_uuid/selftest", handler.UploadDeviceSelfTests) + api.GET("/device/:scrutiny_uuid/details", handler.GetDeviceDetails) //used by Details + api.POST("/device/:scrutiny_uuid/archive", handler.ArchiveDevice) //used by UI to archive device + api.POST("/device/:scrutiny_uuid/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device + api.DELETE("/device/:scrutiny_uuid", handler.DeleteDevice) //used by UI to delete device api.GET("/settings", handler.GetSettings) //used to get settings api.POST("/settings", handler.SaveSettings) //used to save settings diff --git a/webapp/backend/pkg/web/server_test.go b/webapp/backend/pkg/web/server_test.go index 7d9e2e3..9f33311 100644 --- a/webapp/backend/pkg/web/server_test.go +++ b/webapp/backend/pkg/web/server_test.go @@ -19,6 +19,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/web" + "github.com/gofrs/uuid/v5" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "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_BUCKET=metrics \ -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \ -influxdb:2.0 +influxdb:2.2 */ //func TestMain(m *testing.M) { @@ -216,7 +217,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() { require.Equal(suite.T(), 200, wr.Code) 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) require.Equal(suite.T(), 200, mr.Code) @@ -275,28 +276,31 @@ func (suite *ServerTestSuite) TestPopulateMultiple() { router.ServeHTTP(wr, req) 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() - 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) require.Equal(suite.T(), 200, mr.Code) 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) require.Equal(suite.T(), 200, fr.Code) 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) require.Equal(suite.T(), 200, nr.Code) 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) require.Equal(suite.T(), 200, sr.Code) 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) require.Equal(suite.T(), 200, s2r.Code) @@ -555,7 +559,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() { require.Equal(suite.T(), 200, wr.Code) 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) require.Equal(suite.T(), 200, mr.Code) @@ -568,6 +572,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() { require.NoError(suite.T(), err) //assert - require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN) - require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus) + deviceUUIDString := "bde1d2d2-7e5c-525a-8327-6adbfa382637" + 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) } diff --git a/webapp/backend/pkg/web/testdata/register-devices-req-2.json b/webapp/backend/pkg/web/testdata/register-devices-req-2.json index eb300b9..afa4aa4 100644 --- a/webapp/backend/pkg/web/testdata/register-devices-req-2.json +++ b/webapp/backend/pkg/web/testdata/register-devices-req-2.json @@ -14,7 +14,8 @@ "form_factor": "", "smart_support": false, "device_protocol": "NVMe", - "device_type": "nvme" + "device_type": "nvme", + "scrutiny_uuid": "bde1d2d2-7e5c-525a-8327-6adbfa382637" } ] } diff --git a/webapp/backend/pkg/web/testdata/register-devices-req.json b/webapp/backend/pkg/web/testdata/register-devices-req.json index 34d4b45..51197ca 100644 --- a/webapp/backend/pkg/web/testdata/register-devices-req.json +++ b/webapp/backend/pkg/web/testdata/register-devices-req.json @@ -12,27 +12,29 @@ "rotational_speed": 0, "capacity": 500107862016, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c" }, { "wwn": "0x5000cca264eb01d7", "device_name": "sdb", "manufacturer": "ATA", - "model_name": "WDC_WD140EDFZ-11A0VA0", + "model_name": "WDC WD140EDFZ-11A0VA0", "interface_type": "SCSI", "interface_speed": "", - "serial_number": "9RK1XXXXX", + "serial_number": "9RK1XXXX", "firmware": "", "rotational_speed": 0, "capacity": 14000519643136, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "3ea22b35-682b-49fb-a655-abffed108e48" }, { "wwn": "0x5000cca264ec3183", "device_name": "sdc", "manufacturer": "ATA", - "model_name": "WDC_WD140EDFZ-11A0VA0", + "model_name": "WDC WD140EDFZ-11A0VA0", "interface_type": "SCSI", "interface_speed": "", "serial_number": "9RK4XXXXX", @@ -40,7 +42,8 @@ "rotational_speed": 0, "capacity": 14000519643136, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "42caca8a-9b95-5c75-b059-305771a2a193" }, { "wwn": "0x5000cca252c859cc", @@ -54,7 +57,8 @@ "rotational_speed": 0, "capacity": 8001563222016, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "d8796fe7-2422-520c-8991-e970993dad3e" }, { "wwn": "0x5000cca264ebc248", @@ -68,7 +72,8 @@ "rotational_speed": 0, "capacity": 14000519643136, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "00328b73-9f8a-53ad-8f20-8d0b1be00f47" }, { "wwn": "0x50014ee20b2a72a9", @@ -82,7 +87,8 @@ "rotational_speed": 0, "capacity": 6001175126016, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "e5ccc378-24fc-5a9d-b1ce-8732096a9ea5" }, { "wwn": "0x5000c500673e6b5f", @@ -96,7 +102,8 @@ "rotational_speed": 0, "capacity": 6001175126016, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "acfbce7d-0e19-579b-895e-85809dab63fb" } ] } diff --git a/webapp/backend/pkg/web/testdata/register-devices-single-req.json b/webapp/backend/pkg/web/testdata/register-devices-single-req.json index 84aac6b..7102e3c 100644 --- a/webapp/backend/pkg/web/testdata/register-devices-single-req.json +++ b/webapp/backend/pkg/web/testdata/register-devices-single-req.json @@ -4,15 +4,16 @@ "wwn": "0x5000cca264eb01d7", "device_name": "sdb", "manufacturer": "ATA", - "model_name": "WDC_WD140EDFZ-11A0VA0", + "model_name": "WDC WD140EDFZ-11A0VA0", "interface_type": "SCSI", "interface_speed": "", - "serial_number": "9RK1XXXXX", + "serial_number": "9RK1XXXX", "firmware": "", "rotational_speed": 0, "capacity": 14000519643136, "form_factor": "", - "smart_support": false + "smart_support": false, + "scrutiny_uuid": "9a4d34b5-b2ee-51ef-8506-90eea09be417" } ] } diff --git a/webapp/frontend/src/app/app.routing.ts b/webapp/frontend/src/app/app.routing.ts index 2be8dd2..653ac10 100644 --- a/webapp/frontend/src/app/app.routing.ts +++ b/webapp/frontend/src/app/app.routing.ts @@ -38,7 +38,7 @@ export const appRoutes: Route[] = [ // Example {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 // {path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/errors/error-404/error-404.module').then(m => m.Error404Module)}, diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index bddb776..2819a2d 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -1,6 +1,7 @@ // maps to webapp/backend/pkg/models/device.go export interface DeviceModel { archived?: boolean; + scrutiny_uuid: string; wwn: string; device_name?: string; device_uuid?: string; diff --git a/webapp/frontend/src/app/core/models/measurements/smart-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-model.ts index d44d2e1..75b5783 100644 --- a/webapp/frontend/src/app/core/models/measurements/smart-model.ts +++ b/webapp/frontend/src/app/core/models/measurements/smart-model.ts @@ -4,6 +4,7 @@ import {SmartAttributeModel} from './smart-attribute-model'; export interface SmartModel { date: string; device_wwn: string; + scrutiny_uuid: string; device_protocol: string; temp: number; diff --git a/webapp/frontend/src/app/data/mock/device/details/index.ts b/webapp/frontend/src/app/data/mock/device/details/index.ts index 3958c1e..42c1de0 100644 --- a/webapp/frontend/src/app/data/mock/device/details/index.ts +++ b/webapp/frontend/src/app/data/mock/device/details/index.ts @@ -40,7 +40,7 @@ export class DetailsMockApi implements TreoMockApi register(): void { this._treoMockApiService - .onGet('/api/device/0x5002538e40a22954/details') + .onGet('/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/details') .reply(() => { return [ @@ -50,7 +50,7 @@ export class DetailsMockApi implements TreoMockApi }); this._treoMockApiService - .onGet('/api/device/0x5000cca264eb01d7/details') + .onGet('/api/device/3ea22b35-682b-49fb-a655-abffed108e48/details') .reply(() => { return [ @@ -60,7 +60,7 @@ export class DetailsMockApi implements TreoMockApi }); this._treoMockApiService - .onGet('/api/device/0x5000cca264ec3183/details') + .onGet('/api/device/42caca8a-9b95-5c75-b059-305771a2a193/details') .reply(() => { return [ @@ -70,7 +70,7 @@ export class DetailsMockApi implements TreoMockApi }); this._treoMockApiService - .onGet('/api/device/0x5000cca252c859cc/details') + .onGet('/api/device/d8796fe7-2422-520c-8991-e970993dad3e/details') .reply(() => { return [ @@ -80,7 +80,17 @@ export class DetailsMockApi implements TreoMockApi }); 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(() => { return [ diff --git a/webapp/frontend/src/app/data/mock/device/details/sda.ts b/webapp/frontend/src/app/data/mock/device/details/sda.ts index b0b0d4d..678695b 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sda.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sda.ts @@ -5,6 +5,7 @@ export const sda = { 'UpdatedAt': '2021-10-24T16:37:56.981833-07:00', 'DeletedAt': null, 'wwn': '0x5002538e40a22954', + 'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', 'device_name': 'sda', 'manufacturer': 'ATA', 'model_name': 'Samsung_SSD_860_EVO_500GB', @@ -26,6 +27,7 @@ export const sda = { 'smart_results': [{ 'date': '2021-10-24T23:20:44Z', 'device_wwn': '0x5002538e40a22954', + 'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', 'device_protocol': 'NVMe', 'temp': 36, 'power_on_hours': 2401, diff --git a/webapp/frontend/src/app/data/mock/device/details/sdb.ts b/webapp/frontend/src/app/data/mock/device/details/sdb.ts index 232fee5..e74123b 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sdb.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sdb.ts @@ -5,12 +5,13 @@ export const sdb = { 'UpdatedAt': '2021-10-24T17:06:39.436996-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264eb01d7', + 'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48', 'device_name': 'sdb', 'manufacturer': 'ATA', 'model_name': 'WDC_WD140EDFZ-11A0VA0', 'interface_type': 'SCSI', 'interface_speed': '', - 'serial_number': '9RK1XXXXX', + 'serial_number': '9RK1XXXX', 'firmware': '81.00A81', 'rotational_speed': 0, 'capacity': 14000519643136, @@ -25,6 +26,7 @@ export const sdb = { 'smart_results': [{ 'date': '2021-10-24T20:34:04Z', 'device_wwn': '0x5000cca264eb01d7', + 'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48', 'device_protocol': 'ATA', 'temp': 32, 'power_on_hours': 1730, @@ -245,6 +247,7 @@ export const sdb = { }, { 'date': '2021-10-24T23:20:44Z', 'device_wwn': '0x5000cca264eb01d7', + 'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48', 'device_protocol': 'ATA', 'temp': 32, 'power_on_hours': 1730, diff --git a/webapp/frontend/src/app/data/mock/device/details/sdc.ts b/webapp/frontend/src/app/data/mock/device/details/sdc.ts index a647ec0..d614652 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sdc.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sdc.ts @@ -5,6 +5,7 @@ export const sdc = { 'UpdatedAt': '2021-10-24T16:37:56.74865-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264ec3183', + 'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193', 'device_name': 'sdc', 'manufacturer': 'ATA', 'model_name': 'WDC_WD140EDFZ-11A0VA0', @@ -25,6 +26,7 @@ export const sdc = { 'smart_results': [{ 'date': '2021-10-24T23:20:44Z', 'device_wwn': '0x5000cca264ec3183', + 'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193', 'device_protocol': 'ATA', 'temp': 25, 'power_on_hours': 65592, diff --git a/webapp/frontend/src/app/data/mock/device/details/sdd.ts b/webapp/frontend/src/app/data/mock/device/details/sdd.ts index a35df88..34e4663 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sdd.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sdd.ts @@ -5,6 +5,7 @@ export const sdd = { 'UpdatedAt': '2021-10-24T16:37:57.013758-07:00', 'DeletedAt': null, 'wwn': '0x5000cca252c859cc', + 'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e', 'device_name': 'sdd', 'manufacturer': 'ATA', 'model_name': 'WDC_WD80EFAX-68LHPN0', @@ -25,6 +26,7 @@ export const sdd = { 'smart_results': [{ 'date': '2021-10-24T23:20:44Z', 'device_wwn': '0x5000cca252c859cc', + 'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e', 'device_protocol': 'SCSI', 'temp': 34, 'power_on_hours': 43549, diff --git a/webapp/frontend/src/app/data/mock/device/details/sde.ts b/webapp/frontend/src/app/data/mock/device/details/sde.ts index a9c5eae..7eb66fd 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sde.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sde.ts @@ -5,6 +5,7 @@ export const sde = { 'UpdatedAt': '2021-10-24T16:40:16.495248-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264ebc248', + 'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47', 'device_name': 'sde', 'manufacturer': 'ATA', 'model_name': 'WDC_WD140EDFZ-11A0VA0', @@ -25,6 +26,7 @@ export const sde = { 'smart_results': [{ 'date': '2021-10-24T23:20:44Z', 'device_wwn': '0x5000cca264ebc248', + 'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47', 'device_protocol': 'SCSI', 'temp': 31, 'power_on_hours': 5675, diff --git a/webapp/frontend/src/app/data/mock/device/details/sdf.ts b/webapp/frontend/src/app/data/mock/device/details/sdf.ts index e1a8871..c019453 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sdf.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sdf.ts @@ -5,6 +5,7 @@ export const sdf = { 'UpdatedAt': '2021-06-24T21:17:31.305246-07:00', 'DeletedAt': null, 'wwn': '0x50014ee20b2a72a9', + 'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5', 'device_name': 'sdf', 'manufacturer': 'ATA', 'model_name': 'WDC_WD60EFRX-68MYMN1', diff --git a/webapp/frontend/src/app/data/mock/summary/data.ts b/webapp/frontend/src/app/data/mock/summary/data.ts index 5156f9e..fd8525e 100644 --- a/webapp/frontend/src/app/data/mock/summary/data.ts +++ b/webapp/frontend/src/app/data/mock/summary/data.ts @@ -4,12 +4,13 @@ import * as moment from 'moment'; export const summary = { 'data': { 'summary': { - '0x5000c500673e6b5f': { + 'acfbce7d-0e19-579b-895e-85809dab63fb': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.155217-07:00', 'UpdatedAt': '2021-04-30T08:17:13.155217-07:00', 'DeletedAt': null, 'wwn': '0x5000c500673e6b5f', + 'scrutiny_uuid': 'acfbce7d-0e19-579b-895e-85809dab63fb', 'device_name': 'sdg', 'device_label': '14TB-WD-DRIVE2', 'device_uuid': '', @@ -32,12 +33,13 @@ export const summary = { 'archived': false } }, - '0x5000cca252c859cc': { + 'd8796fe7-2422-520c-8991-e970993dad3e': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.152705-07:00', 'UpdatedAt': '2021-05-02T14:22:50.357164-07:00', 'DeletedAt': null, 'wwn': '0x5000cca252c859cc', + 'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e', 'device_name': 'sdd', 'device_label': '14TB-WD-DRIVE1', 'device_uuid': '806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f', @@ -69,21 +71,22 @@ export const summary = { 'temp': 34 }] }, - '0x5000cca264eb01d7': { + '3ea22b35-682b-49fb-a655-abffed108e48': { 'device': { 'CreatedAt': '2021-04-28T20:52:49.047154-07:00', 'UpdatedAt': '2021-05-02T14:22:49.86136-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264eb01d7', + 'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48', 'device_name': 'sdb', 'device_label': '14TB-WD-DRIVE5', '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', 'model_name': 'WDC_WD140EDFZ-11A0VA0', 'interface_type': 'SCSI', 'interface_speed': '', - 'serial_number': '9RK1XXXXX', + 'serial_number': '9RK1XXXX', 'firmware': '81.00A81', 'rotational_speed': 0, 'capacity': 14000519643136, @@ -106,12 +109,13 @@ export const summary = { 'temp': 32 }] }, - '0x5000cca264ebc248': { + '00328b73-9f8a-53ad-8f20-8d0b1be00f47': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.153782-07:00', 'UpdatedAt': '2021-05-02T14:22:50.385282-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264ebc248', + 'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47', 'device_name': 'sde', 'device_label': '14TB-WD-DRIVE3', 'device_uuid': '9eb60cde-d6d0-4172-b520-b241a6a5477f', @@ -134,12 +138,13 @@ export const summary = { 'archived': false } }, - '0x5000cca264ec3183': { + '42caca8a-9b95-5c75-b059-305771a2a193': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.151906-07:00', 'UpdatedAt': '2021-05-02T14:49:51.645012-07:00', 'DeletedAt': null, 'wwn': '0x5000cca264ec3183', + 'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193', 'device_name': 'sdc', 'device_label': '14TB-WD-DRIVE6', 'device_uuid': 'e1378723-7861-49b9-8e01-0bd063f0ecdd', @@ -555,12 +560,13 @@ export const summary = { 'temp': 39 }] }, - '0x50014ee20b2a72a9': { + 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.15451-07:00', 'UpdatedAt': '2021-04-30T08:17:13.15451-07:00', 'DeletedAt': null, 'wwn': '0x50014ee20b2a72a9', + 'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5', 'device_name': 'sdf', 'device_label': '8.0TB-WD-4', 'device_uuid': 'fc684dcc-aa2f-44f3-a958-d302dc7dd46d', @@ -583,12 +589,13 @@ export const summary = { 'archived': false } }, - '0x5002538e40a22954': { + 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c': { 'device': { 'CreatedAt': '2021-04-30T08:17:13.150792-07:00', 'UpdatedAt': '2021-05-02T14:22:50.330706-07:00', 'DeletedAt': null, 'wwn': '0x5002538e40a22954', + 'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', 'device_name': 'sda', 'device_label': '', 'device_uuid': '', diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts index 3cffb95..b42e8f1 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts @@ -28,7 +28,7 @@ describe('DashboardDeviceArchiveDialogComponent', () => { ], providers: [ {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} ], declarations: [DashboardDeviceArchiveDialogComponent] @@ -56,7 +56,7 @@ describe('DashboardDeviceArchiveDialogComponent', () => { dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true})); component.onArchiveClick() - expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn'); + expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c'); expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count()) .withContext('one call') .toBe(1); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts index 979aeb8..3bc48e2 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts @@ -11,7 +11,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, + @Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string}, private _archiveService: DashboardDeviceArchiveDialogService, ) { } @@ -20,7 +20,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit { } onArchiveClick(): void { - this._archiveService.archiveDevice(this.data.wwn) + this._archiveService.archiveDevice(this.data.scrutiny_uuid) .subscribe((data) => { this.dialogRef.close(data); }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts index a8c1083..45b09d7 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts @@ -26,13 +26,13 @@ export class DashboardDeviceArchiveDialogService // ----------------------------------------------------------------------------------------------------- - archiveDevice(wwn: string): Observable + archiveDevice(scrutiny_uid: string): Observable { - return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {}); + return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/archive`, {}); } - unarchiveDevice(wwn: string): Observable + unarchiveDevice(scrutiny_uid: string): Observable { - return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {}); + return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/unarchive`, {}); } } diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts index 26248f1..4c1d62e 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts @@ -28,7 +28,7 @@ describe('DashboardDeviceDeleteDialogComponent', () => { ], providers: [ {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} ], declarations: [DashboardDeviceDeleteDialogComponent] @@ -56,7 +56,7 @@ describe('DashboardDeviceDeleteDialogComponent', () => { dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true})); component.onDeleteClick() - expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn'); + expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c'); expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count()) .withContext('one call') .toBe(1); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts index 5fdd8a0..c98ba12 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts @@ -11,7 +11,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, + @Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string}, private _deleteService: DashboardDeviceDeleteDialogService, ) { } @@ -20,7 +20,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit { } onDeleteClick(): void { - this._deleteService.deleteDevice(this.data.wwn) + this._deleteService.deleteDevice(this.data.scrutiny_uuid) .subscribe((data) => { this.dialogRef.close(data); }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts index e4c0f10..c6c45b2 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts @@ -27,8 +27,8 @@ export class DashboardDeviceDeleteDialogService // ----------------------------------------------------------------------------------------------------- - deleteDevice(wwn: string): Observable + deleteDevice(scrutiny_uuid: string): Observable { - return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {}); + return this._httpClient.delete( `${getBasePath()}/api/device/${scrutiny_uuid}`, {}); } } diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html index 01ffb5f..064f41e 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html @@ -16,7 +16,7 @@
- {{deviceSummary.device | deviceTitle:config.dashboard_display}}
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }} @@ -30,7 +30,7 @@ - + diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts index 4664be6..69af5ef 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts @@ -75,22 +75,22 @@ export class DashboardDeviceComponent implements OnInit { openArchiveDialog(): void { 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) { - this.deviceUnarchived.emit(this.deviceSummary.device.wwn) + this.deviceUnarchived.emit(this.deviceSummary.device.scrutiny_uuid) } }) return; } const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, { data: { - wwn: this.deviceSummary.device.wwn, + scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display) } }); dialogRef.afterClosed().subscribe(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, { // width: '250px', data: { - wwn: this.deviceSummary.device.wwn, + scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display) } }); @@ -107,7 +107,7 @@ export class DashboardDeviceComponent implements OnInit { dialogRef.afterClosed().subscribe(result => { console.log('The dialog was closed', result); if (result.success) { - this.deviceDeleted.emit(this.deviceSummary.device.wwn) + this.deviceDeleted.emit(this.deviceSummary.device.scrutiny_uuid) } }); } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 50aeed2..b5f2c30 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -100,10 +100,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy this.summaryData = data; // generate group data. - for (const wwn in this.summaryData) { - const hostid = this.summaryData[wwn].device.host_id + for (const scrutiny_uuid in this.summaryData) { + const hostid = this.summaryData[scrutiny_uuid].device.host_id const hostDeviceList = this.hostGroups[hostid] || [] - hostDeviceList.push(wwn) + hostDeviceList.push(scrutiny_uuid) this.hostGroups[hostid] = hostDeviceList } console.log(this.hostGroups) @@ -145,8 +145,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy console.log('DEVICE DATA SUMMARY', this.summaryData) - for (const wwn in this.summaryData) { - const deviceSummary = this.summaryData[wwn] + for (const scrutiny_uuid in this.summaryData) { + const deviceSummary = this.summaryData[scrutiny_uuid] if (!deviceSummary.temp_history) { continue } @@ -241,11 +241,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // @ Public methods // ----------------------------------------------------------------------------------------------------- - deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] { + deviceSummariesForHostGroup(hostGroupScrutinyUUIDs: string[]): DeviceSummaryModel[] { const deviceSummaries: DeviceSummaryModel[] = [] - for (const wwn of hostGroupWWNs) { - if (this.summaryData[wwn]) { - deviceSummaries.push(this.summaryData[wwn]) + for (const scrutiny_uuid of hostGroupScrutinyUUIDs) { + if (this.summaryData[scrutiny_uuid]) { + deviceSummaries.push(this.summaryData[scrutiny_uuid]) } } return deviceSummaries @@ -259,16 +259,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy }); } - onDeviceDeleted(wwn: string): void { - delete this.summaryData[wwn] // remove the device from the summary list. + onDeviceDeleted(scrutiny_uuid: string): void { + delete this.summaryData[scrutiny_uuid] // remove the device from the summary list. } - onDeviceArchived(wwn: string): void { - this.summaryData[wwn].device.archived = true; + onDeviceArchived(scrutiny_uuid: string): void { + this.summaryData[scrutiny_uuid].device.archived = true; } - onDeviceUnarchived(wwn: string): void { - this.summaryData[wwn].device.archived = false; + onDeviceUnarchived(scrutiny_uuid: string): void { + this.summaryData[scrutiny_uuid].device.archived = false; } /* @@ -286,9 +286,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy .subscribe((tempHistoryData) => { // given a list of device temp history, override the data in the "summary" object. - for (const wwn in this.summaryData) { - // console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`) - this.summaryData[wwn].temp_history = tempHistoryData[wwn] || [] + for (const scrutiny_uuid in this.summaryData) { + // console.log(`Updating ${scrutiny_uuid}, length: ${this.data.data.summary[scrutiny_uuid].temp_history.length}`) + this.summaryData[scrutiny_uuid].temp_history = tempHistoryData[scrutiny_uuid] || [] } // Prepare the chart series data diff --git a/webapp/frontend/src/app/modules/detail/detail.component.html b/webapp/frontend/src/app/modules/detail/detail.component.html index 13b19c2..93f5719 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.html +++ b/webapp/frontend/src/app/modules/detail/detail.component.html @@ -98,6 +98,10 @@
{{device?.serial_number}}
Serial Number
+
+
{{device?.scrutiny_uuid}}
+
Scrutiny UUID
+
{{device?.wwn}}
LU WWN Device Id
diff --git a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts index 221cad1..d6d9e92 100644 --- a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts +++ b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts @@ -30,6 +30,6 @@ export class DetailResolver implements Resolve { * @param state */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this._detailService.getData(route.params.wwn); + return this._detailService.getData(route.params.scrutiny_uuid); } } diff --git a/webapp/frontend/src/app/modules/detail/detail.service.ts b/webapp/frontend/src/app/modules/detail/detail.service.ts index e75cb8b..5ff629c 100644 --- a/webapp/frontend/src/app/modules/detail/detail.service.ts +++ b/webapp/frontend/src/app/modules/detail/detail.service.ts @@ -42,8 +42,8 @@ export class DetailService { /** * Get data */ - getData(wwn): Observable { - return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( + getData(scrutiny_uuid): Observable { + return this._httpClient.get(getBasePath() + `/api/device/${scrutiny_uuid}/details`).pipe( tap((response: DeviceDetailsResponseWrapper) => { this._data.next(response); }) diff --git a/webapp/frontend/src/app/shared/device-title.pipe.ts b/webapp/frontend/src/app/shared/device-title.pipe.ts index 3cabc0f..c370024 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.ts @@ -37,7 +37,7 @@ export class DeviceTitlePipe implements PipeTransform { } 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 = [] if (device.host_id) titleParts.push(device.host_id)