Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15cf5e4e3b |
@@ -15,7 +15,6 @@ 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"
|
||||
)
|
||||
@@ -65,9 +64,9 @@ func (mc *MetricsCollector) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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()
|
||||
//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
|
||||
})
|
||||
|
||||
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
||||
@@ -91,7 +90,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.ScrutinyUUID, device.DeviceName, device.DeviceType)
|
||||
mc.Collect(device.WWN, 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)
|
||||
@@ -118,10 +117,10 @@ func (mc *MetricsCollector) Validate() error {
|
||||
}
|
||||
|
||||
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
|
||||
func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string, deviceType string) {
|
||||
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
|
||||
//defer wg.Done()
|
||||
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)
|
||||
if len(deviceWWN) == 0 {
|
||||
mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName)
|
||||
return
|
||||
}
|
||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||
@@ -141,7 +140,7 @@ func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string,
|
||||
// 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(scrutiny_uuid, resultBytes)
|
||||
mc.Publish(deviceWWN, resultBytes)
|
||||
} else {
|
||||
mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName)
|
||||
mc.logger.Errorf("ERROR MESSAGE: %v", err)
|
||||
@@ -150,19 +149,19 @@ func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string,
|
||||
return
|
||||
} else {
|
||||
//successful run, pass the results directly to webapp backend for parsing and processing.
|
||||
mc.Publish(scrutiny_uuid, resultBytes)
|
||||
mc.Publish(deviceWWN, resultBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Publish(scrutinyUuid uuid.UUID, payload []byte) error {
|
||||
mc.logger.Infof("Publishing smartctl results for %s\n", scrutinyUuid)
|
||||
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
|
||||
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN)
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", scrutinyUuid.String()))
|
||||
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN)))
|
||||
|
||||
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", scrutinyUuid, err)
|
||||
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", deviceWWN, err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -101,11 +101,15 @@ 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.Debug("Using WWN Fallback")
|
||||
d.Logger.Info("Using WWN Fallback")
|
||||
d.wwnFallback(device)
|
||||
}
|
||||
|
||||
device.ScrutinyUUID = GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 {
|
||||
@@ -90,7 +89,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 {
|
||||
@@ -103,6 +102,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 {
|
||||
@@ -28,7 +27,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 {
|
||||
@@ -41,6 +40,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package detect
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -25,7 +26,14 @@ 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) {
|
||||
// No fallback on windows
|
||||
|
||||
//fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"`
|
||||
WWN string `json:"wwn"`
|
||||
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"`
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
|
||||
@@ -61,10 +61,6 @@ 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=
|
||||
@@ -178,7 +174,6 @@ 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=
|
||||
@@ -188,51 +183,30 @@ 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=
|
||||
|
||||
@@ -4,9 +4,8 @@ const DeviceProtocolAta = "ATA"
|
||||
const DeviceProtocolScsi = "SCSI"
|
||||
const DeviceProtocolNvme = "NVMe"
|
||||
|
||||
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
//
|
||||
//go:generate stringer -type=AttributeStatus
|
||||
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
type AttributeStatus uint8
|
||||
|
||||
const (
|
||||
@@ -24,9 +23,8 @@ 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 }
|
||||
|
||||
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
//
|
||||
//go:generate stringer -type=DeviceStatus
|
||||
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
type DeviceStatus uint8
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,7 +7,6 @@ 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:
|
||||
@@ -18,19 +17,19 @@ type DeviceRepo interface {
|
||||
|
||||
RegisterDevice(ctx context.Context, dev models.Device) error
|
||||
GetDevices(ctx context.Context) ([]models.Device, 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
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
||||
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
||||
|
||||
GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error)
|
||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||
|
||||
LoadSettings(ctx context.Context) (*models.Settings, error)
|
||||
SaveSettings(ctx context.Context, settings models.Settings) error
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package m20250221084400
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package m20260216155600
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
Archived bool `json:"archived"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
FormFactor string `json:"form_factor"`
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"`
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
// 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
|
||||
@@ -17,7 +12,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -25,7 +19,6 @@ import (
|
||||
type MockDeviceRepo struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDeviceRepoMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo.
|
||||
@@ -59,33 +52,47 @@ func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close))
|
||||
}
|
||||
|
||||
// DeleteDevice mocks base method.
|
||||
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error {
|
||||
// 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, "DeleteDevice", ctx, scrutiny_uuid)
|
||||
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 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteDevice indicates an expected call of DeleteDevice.
|
||||
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, scrutiny_uuid any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, scrutiny_uuid)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn)
|
||||
}
|
||||
|
||||
// GetDeviceDetails mocks base method.
|
||||
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, scrutiny_uuid)
|
||||
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn)
|
||||
ret0, _ := ret[0].(models.Device)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetDeviceDetails indicates an expected call of GetDeviceDetails.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, scrutiny_uuid any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, scrutiny_uuid)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn)
|
||||
}
|
||||
|
||||
// GetDevices mocks base method.
|
||||
@@ -98,52 +105,52 @@ func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error
|
||||
}
|
||||
|
||||
// GetDevices indicates an expected call of GetDevices.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *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, scrutiny_uuid uuid.UUID, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, 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, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
}
|
||||
|
||||
// GetSmartTemperatureHistory mocks base method.
|
||||
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
|
||||
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey)
|
||||
ret0, _ := ret[0].(map[uuid.UUID][]measurements.SmartTemperature)
|
||||
ret0, _ := ret[0].(map[string][]measurements.SmartTemperature)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *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[uuid.UUID]*models.DeviceSummary, error) {
|
||||
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSummary", ctx)
|
||||
ret0, _ := ret[0].(map[uuid.UUID]*models.DeviceSummary)
|
||||
ret0, _ := ret[0].(map[string]*models.DeviceSummary)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetSummary indicates an expected call of GetSummary.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx)
|
||||
}
|
||||
@@ -157,7 +164,7 @@ func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// HealthCheck indicates an expected call of HealthCheck.
|
||||
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx)
|
||||
}
|
||||
@@ -172,7 +179,7 @@ func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, er
|
||||
}
|
||||
|
||||
// LoadSettings indicates an expected call of LoadSettings.
|
||||
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx)
|
||||
}
|
||||
@@ -186,7 +193,7 @@ func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device)
|
||||
}
|
||||
|
||||
// RegisterDevice indicates an expected call of RegisterDevice.
|
||||
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev)
|
||||
}
|
||||
@@ -200,80 +207,66 @@ func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Setti
|
||||
}
|
||||
|
||||
// SaveSettings indicates an expected call of SaveSettings.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *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, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, scrutiny_uuid, collectorSmartData)
|
||||
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, 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, scrutiny_uuid, collectorSmartData any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, scrutiny_uuid, collectorSmartData)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData)
|
||||
}
|
||||
|
||||
// SaveSmartTemperature mocks base method.
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
}
|
||||
|
||||
// UpdateDevice mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDevice", ctx, scrutiny_uuid, collectorSmartData)
|
||||
ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, 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, scrutiny_uuid, collectorSmartData any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData)
|
||||
}
|
||||
|
||||
// UpdateDeviceStatus mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, scrutiny_uuid, status)
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, 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, scrutiny_uuid, status any) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, scrutiny_uuid, status)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,10 @@ 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"
|
||||
)
|
||||
|
||||
@@ -335,16 +333,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[uuid.UUID]*models.DeviceSummary, error) {
|
||||
func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
|
||||
devices, err := sr.GetDevices(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaries := map[uuid.UUID]*models.DeviceSummary{}
|
||||
summaries := map[string]*models.DeviceSummary{}
|
||||
|
||||
for _, device := range devices {
|
||||
summaries[device.ScrutinyUUID] = &models.DeviceSummary{Device: device}
|
||||
summaries[device.WWN] = &models.DeviceSummary{Device: device}
|
||||
}
|
||||
|
||||
// Get parser flux query result
|
||||
@@ -359,7 +357,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|
||||
weeklyData = from(bucket: bucketBaseName + "_weekly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -367,7 +365,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|
||||
monthlyData = from(bucket: bucketBaseName + "_monthly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -375,7 +373,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|
||||
yearlyData = from(bucket: bucketBaseName + "_yearly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -383,12 +381,12 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|
||||
union(tables: [dailyData, weeklyData, monthlyData, yearlyData])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> last(column: "scrutiny_uuid")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> last(column: "device_wwn")
|
||||
|> yield(name: "last")
|
||||
`,
|
||||
sr.appConfig.GetString("web.influxdb.bucket"),
|
||||
@@ -406,15 +404,14 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
|
||||
//get summary data from Influxdb.
|
||||
//result.Record().Values()
|
||||
if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
|
||||
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
|
||||
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok {
|
||||
|
||||
//ensure summaries is intialized for this scrutiny_uuid
|
||||
if _, exists := summaries[scrutinyUUID]; !exists {
|
||||
summaries[scrutinyUUID] = &models.DeviceSummary{}
|
||||
//ensure summaries is intialized for this wwn
|
||||
if _, exists := summaries[deviceWWN.(string)]; !exists {
|
||||
summaries[deviceWWN.(string)] = &models.DeviceSummary{}
|
||||
}
|
||||
|
||||
summaries[scrutinyUUID].SmartResults = &models.SmartSummary{
|
||||
summaries[deviceWWN.(string)].SmartResults = &models.SmartSummary{
|
||||
Temp: result.Record().Values()["temp"].(int64),
|
||||
PowerOnHours: result.Record().Values()["power_on_hours"].(int64),
|
||||
CollectorDate: result.Record().Values()["_time"].(time.Time),
|
||||
@@ -437,8 +434,8 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*mo
|
||||
sr.logger.Printf("========================>>>>>>>>======================")
|
||||
sr.logger.Printf("Error: %v", err)
|
||||
}
|
||||
for scutiny_uuid, tempHistory := range deviceTempHistory {
|
||||
summaries[scutiny_uuid].TempHistory = tempHistory
|
||||
for wwn, tempHistory := range deviceTempHistory {
|
||||
summaries[wwn].TempHistory = tempHistory
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
|
||||
@@ -8,7 +8,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,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: "scrutiny_uuid"}},
|
||||
Columns: []clause.Column{{Name: "wwn"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
|
||||
}).Create(&dev).Error; err != nil {
|
||||
return err
|
||||
@@ -39,9 +38,9 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
|
||||
}
|
||||
|
||||
// update device (only metadata) from collector
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
@@ -54,9 +53,9 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, scrutiny_uuid uu
|
||||
}
|
||||
|
||||
// Update Device Status
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
@@ -64,12 +63,12 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, scrutiny_u
|
||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
|
||||
var device models.Device
|
||||
|
||||
fmt.Println("GetDeviceDetails from GORM")
|
||||
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return models.Device{}, err
|
||||
}
|
||||
|
||||
@@ -77,17 +76,17 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, scrutiny_uui
|
||||
}
|
||||
|
||||
// Update Device Archived State
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error {
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
return sr.gormClient.Model(&device).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Update("archived", archived).Error
|
||||
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -100,14 +99,14 @@ func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, scrutiny_uuid uu
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", scrutiny_uuid.String(), bucket)
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, 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(`scrutiny_uuid="%s"`, scrutiny_uuid.String()),
|
||||
fmt.Sprintf(`device_wwn="%s"`, wwn),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,18 +8,17 @@ 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, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
deviceSmartData := measurements.Smart{}
|
||||
err := deviceSmartData.FromCollectorSmartInfo(scrutiny_uuid, collectorSmartData)
|
||||
err := deviceSmartData.FromCollectorSmartInfo(wwn, collectorSmartData)
|
||||
if err != nil {
|
||||
sr.logger.Errorln("Could not process SMART metrics", err)
|
||||
return measurements.Smart{}, err
|
||||
@@ -35,14 +34,14 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, scrutiny_
|
||||
// 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, scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, 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(scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
log.Infoln(queryStr)
|
||||
|
||||
smartResults := []measurements.Smart{}
|
||||
@@ -101,7 +100,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking,
|
||||
return influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
|
||||
/*
|
||||
|
||||
@@ -109,28 +108,28 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.U
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> 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["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> 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["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> 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["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> tail(n: 10, offset: 0)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
@@ -151,7 +150,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.U
|
||||
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(scrutiny_uuid, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
|
||||
sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
|
||||
fmt.Sprintf(`%sData`, nestedDurationKeys[0]),
|
||||
`|> sort(columns: ["_time"], desc: true)`,
|
||||
`|> yield()`,
|
||||
@@ -166,9 +165,9 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.U
|
||||
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(scrutiny_uuid, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
|
||||
} else {
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, 0, 0, attributes))
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes))
|
||||
}
|
||||
}
|
||||
partialQueryStr = append(partialQueryStr, subQueries...)
|
||||
@@ -185,7 +184,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.U
|
||||
return strings.Join(partialQueryStr, "\n")
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) generateSmartAttributesSubquery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
bucketName := sr.lookupBucketName(durationKey)
|
||||
durationRange := sr.lookupDuration(durationKey)
|
||||
|
||||
@@ -193,7 +192,7 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(scrutiny_uuid uuid
|
||||
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["scrutiny_uuid"] == "%s" )`, scrutiny_uuid.String()),
|
||||
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
|
||||
}
|
||||
|
||||
partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`)
|
||||
|
||||
@@ -7,14 +7,12 @@ 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"
|
||||
@@ -426,53 +424,6 @@ 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 {
|
||||
@@ -522,91 +473,6 @@ 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
|
||||
|
||||
@@ -3,13 +3,12 @@ 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")
|
||||
@@ -109,7 +108,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: ["scrutiny_uuid", "_field"])
|
||||
|> group(columns: ["device_wwn", "_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"))
|
||||
@@ -140,19 +139,20 @@ destOrg = "%s"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> group(columns: ["device_wwn", "_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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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,
|
||||
|
||||
@@ -43,19 +43,20 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> group(columns: ["device_wwn", "_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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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) {
|
||||
@@ -93,19 +94,20 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> group(columns: ["device_wwn", "_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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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) {
|
||||
@@ -143,17 +145,18 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> group(columns: ["device_wwn", "_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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ 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, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
|
||||
|
||||
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
||||
@@ -25,15 +24,15 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, scrutiny
|
||||
}
|
||||
|
||||
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["scrutiny_uuid"] = scrutiny_uuid.String()
|
||||
tags["device_wwn"] = wwn
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
@@ -45,6 +44,7 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, scrutiny
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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, scrutiny
|
||||
}
|
||||
|
||||
tags, fields := smartTemp.Flatten()
|
||||
tags["scrutiny_uuid"] = scrutiny_uuid.String()
|
||||
tags["device_wwn"] = wwn
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
@@ -60,10 +60,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, scrutiny
|
||||
return sr.influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
|
||||
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||
//we can get temp history for "week", "month", DURATION_KEY_YEAR, "forever"
|
||||
|
||||
deviceTempHistory := map[uuid.UUID][]measurements.SmartTemperature{}
|
||||
deviceTempHistory := map[string][]measurements.SmartTemperature{}
|
||||
|
||||
//TODO: change the query range to a variable.
|
||||
queryStr := sr.aggregateTempQuery(durationKey)
|
||||
@@ -73,15 +73,14 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
|
||||
// Use Next() to iterate over query result lines
|
||||
for result.Next() {
|
||||
|
||||
if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
|
||||
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
|
||||
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok {
|
||||
|
||||
//check if scrutinyUUID has been seen and initialized already
|
||||
if _, ok := deviceTempHistory[scrutinyUUID]; !ok {
|
||||
deviceTempHistory[scrutinyUUID] = []measurements.SmartTemperature{}
|
||||
//check if deviceWWN has been seen and initialized already
|
||||
if _, ok := deviceTempHistory[deviceWWN.(string)]; !ok {
|
||||
deviceTempHistory[deviceWWN.(string)] = []measurements.SmartTemperature{}
|
||||
}
|
||||
|
||||
currentTempHistory := deviceTempHistory[scrutinyUUID]
|
||||
currentTempHistory := deviceTempHistory[deviceWWN.(string)]
|
||||
smartTemp := measurements.SmartTemperature{}
|
||||
|
||||
for key, val := range result.Record().Values() {
|
||||
@@ -89,7 +88,7 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
|
||||
}
|
||||
smartTemp.Date = result.Record().Values()["_time"].(time.Time)
|
||||
currentTempHistory = append(currentTempHistory, smartTemp)
|
||||
deviceTempHistory[scrutinyUUID] = currentTempHistory
|
||||
deviceTempHistory[deviceWWN.(string)] = currentTempHistory
|
||||
}
|
||||
}
|
||||
if result.Err() != nil {
|
||||
@@ -114,18 +113,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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
@@ -149,7 +148,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: ["scrutiny_uuid"])`,
|
||||
`|> group(columns: ["device_wwn"])`,
|
||||
`|> toInt()`,
|
||||
"",
|
||||
}...)
|
||||
@@ -165,7 +164,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
|
||||
} else {
|
||||
partialQueryStr = append(partialQueryStr, []string{
|
||||
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
|
||||
`|> group(columns: ["scrutiny_uuid"])`,
|
||||
`|> group(columns: ["device_wwn"])`,
|
||||
`|> sort(columns: ["_time"], desc: false)`,
|
||||
"|> schema.fieldsAsCols()",
|
||||
}...)
|
||||
|
||||
@@ -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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> 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: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData, foreverData])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
|
||||
@@ -207,10 +207,10 @@ type SmartInfo struct {
|
||||
ID int `json:"id"`
|
||||
SubsystemID int `json:"subsystem_id"`
|
||||
} `json:"nvme_pci_vendor"`
|
||||
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"`
|
||||
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"`
|
||||
NvmeNamespaces []struct {
|
||||
ID int `json:"id"`
|
||||
Size struct {
|
||||
@@ -226,10 +226,6 @@ 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"`
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceWrapper struct {
|
||||
@@ -21,7 +19,7 @@ type Device struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn"`
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
@@ -47,7 +45,6 @@ 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 {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This is used in server_test.go
|
||||
type DeviceSummaryWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
|
||||
@@ -10,13 +10,11 @@ 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` // deprecated
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"` //(tag)
|
||||
DeviceWWN string `json:"device_wwn"` //(tag)
|
||||
DeviceProtocol string `json:"device_protocol"`
|
||||
|
||||
//Metrics (fields)
|
||||
@@ -33,7 +31,7 @@ type Smart struct {
|
||||
|
||||
func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{}) {
|
||||
tags = map[string]string{
|
||||
"scrutiny_uuid": sm.ScrutinyUUID.String(),
|
||||
"device_wwn": sm.DeviceWWN,
|
||||
"device_protocol": sm.DeviceProtocol,
|
||||
}
|
||||
|
||||
@@ -55,15 +53,10 @@ 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),
|
||||
ScrutinyUUID: scrutiny_uuid,
|
||||
DeviceWWN: attrs["device_wwn"].(string),
|
||||
DeviceProtocol: attrs["device_protocol"].(string),
|
||||
|
||||
Attributes: map[string]SmartAttribute{},
|
||||
@@ -119,14 +112,14 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
|
||||
}
|
||||
|
||||
log.Printf("Found Smart Device (%s) Attributes (%v)", sm.ScrutinyUUID, len(sm.Attributes))
|
||||
log.Printf("Found Smart Device (%s) Attributes (%v)", sm.DeviceWWN, len(sm.Attributes))
|
||||
|
||||
return &sm, nil
|
||||
}
|
||||
|
||||
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
func (sm *Smart) FromCollectorSmartInfo(scrutiny_uuid uuid.UUID, info collector.SmartInfo) error {
|
||||
sm.ScrutinyUUID = scrutiny_uuid
|
||||
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
|
||||
sm.DeviceWWN = wwn
|
||||
sm.Date = time.Unix(info.LocalTime.TimeT, 0)
|
||||
|
||||
//smart metrics
|
||||
@@ -140,12 +133,11 @@ func (sm *Smart) FromCollectorSmartInfo(scrutiny_uuid uuid.UUID, info collector.
|
||||
sm.DeviceProtocol = info.Device.Protocol
|
||||
// process ATA/NVME/SCSI protocol data
|
||||
sm.Attributes = map[string]SmartAttribute{}
|
||||
switch sm.DeviceProtocol {
|
||||
case pkg.DeviceProtocolAta:
|
||||
if sm.DeviceProtocol == pkg.DeviceProtocolAta {
|
||||
sm.ProcessAtaSmartInfo(info.AtaSmartAttributes.Table)
|
||||
case pkg.DeviceProtocolNvme:
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolNvme {
|
||||
sm.ProcessNvmeSmartInfo(info.NvmeSmartHealthInformationLog)
|
||||
case pkg.DeviceProtocolScsi:
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi {
|
||||
sm.ProcessScsiSmartInfo(info.ScsiGrownDefectList, info.ScsiErrorCounterLog)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ 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.
|
||||
|
||||
@@ -10,17 +10,15 @@ 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,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: pkg.DeviceProtocolAta,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -33,17 +31,16 @@ func TestSmart_Flatten(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, 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,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: pkg.DeviceProtocolAta,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -75,7 +72,7 @@ func TestSmart_Flatten_ATA(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"attr.1.attribute_id": "1",
|
||||
"attr.1.failure_rate": float64(0),
|
||||
@@ -110,10 +107,9 @@ 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,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: pkg.DeviceProtocolScsi,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -131,7 +127,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "SCSI", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "SCSI", "device_wwn": "test-wwn"}, 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),
|
||||
@@ -149,10 +145,9 @@ 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,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: pkg.DeviceProtocolNvme,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -170,7 +165,7 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "NVMe", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "NVMe", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"attr.available_spare.attribute_id": "available_spare",
|
||||
"attr.available_spare.failure_rate": float64(0),
|
||||
@@ -187,10 +182,9 @@ 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,
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_wwn": "test-wwn",
|
||||
"device_protocol": pkg.DeviceProtocolAta,
|
||||
"attr.1.attribute_id": "1",
|
||||
"attr.1.failure_rate": float64(0),
|
||||
@@ -215,7 +209,7 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: "ATA",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -236,10 +230,9 @@ 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,
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_wwn": "test-wwn",
|
||||
"device_protocol": pkg.DeviceProtocolNvme,
|
||||
"attr.available_spare.attribute_id": "available_spare",
|
||||
"attr.available_spare.failure_rate": float64(0),
|
||||
@@ -260,7 +253,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: "NVMe",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -276,10 +269,9 @@ 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,
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_wwn": "test-wwn",
|
||||
"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),
|
||||
@@ -300,7 +292,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceWWN: "test-wwn",
|
||||
DeviceProtocol: "SCSI",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -328,12 +320,11 @@ func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 18, len(smartMdl.Attributes))
|
||||
|
||||
@@ -361,12 +352,11 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedSmart, smartMdl.Status)
|
||||
require.Equal(t, 0, len(smartMdl.Attributes))
|
||||
}
|
||||
@@ -386,12 +376,11 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny|pkg.DeviceStatusFailedSmart, smartMdl.Status)
|
||||
require.Equal(t, 17, len(smartMdl.Attributes))
|
||||
}
|
||||
@@ -411,12 +400,11 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
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)",
|
||||
@@ -445,12 +433,11 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
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)",
|
||||
@@ -477,12 +464,11 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 16, len(smartMdl.Attributes))
|
||||
|
||||
@@ -505,12 +491,11 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 13, len(smartMdl.Attributes))
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ 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"
|
||||
)
|
||||
@@ -33,7 +32,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, scrutiny_uuid uuid.UUID, 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, 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
|
||||
@@ -101,7 +100,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, scrutiny_uuid, database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
|
||||
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), 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.")
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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"
|
||||
@@ -27,12 +26,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
|
||||
@@ -44,11 +42,10 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
|
||||
@@ -60,11 +57,10 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
|
||||
@@ -76,11 +72,10 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||
@@ -96,12 +91,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||
@@ -120,12 +114,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||
@@ -141,12 +134,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||
@@ -162,12 +154,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||
@@ -186,12 +177,11 @@ 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, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -206,13 +196,12 @@ 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{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
||||
@@ -228,13 +217,12 @@ 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{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
func TestShouldNotify_NoRepeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -250,13 +238,12 @@ 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{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestNewPayload(t *testing.T) {
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while archiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
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)
|
||||
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)
|
||||
|
||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while deleting device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -6,20 +6,14 @@ 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)
|
||||
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)
|
||||
|
||||
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving device details", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -31,7 +25,7 @@ func GetDeviceDetails(c *gin.Context) {
|
||||
durationKey = "forever"
|
||||
}
|
||||
|
||||
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, scrutiny_uuid, durationKey, 0, 0, nil)
|
||||
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), 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})
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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.
|
||||
@@ -24,9 +23,9 @@ func RegisterDevices(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter any device without a scrutiny UUID. This should never happen...
|
||||
//filter any device with empty wwn (they are invalid)
|
||||
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
|
||||
return !dev.ScrutinyUUID.IsNil()
|
||||
return len(dev.WWN) > 0
|
||||
})
|
||||
|
||||
errs := []error{}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
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)
|
||||
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)
|
||||
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while unarchiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -10,7 +10,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -23,15 +22,12 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
|
||||
//appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
|
||||
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
|
||||
if c.Param("wwn") == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
}
|
||||
|
||||
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})
|
||||
@@ -39,7 +35,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
//update the device information if necessary
|
||||
updatedDevice, err := deviceRepo.UpdateDevice(c, scrutiny_uuid, collectorSmartData)
|
||||
updatedDevice, err := deviceRepo.UpdateDevice(c, c.Param("wwn"), 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})
|
||||
@@ -47,7 +43,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
// insert smart info
|
||||
smartData, err := deviceRepo.SaveSmartAttributes(c, scrutiny_uuid, collectorSmartData)
|
||||
smartData, err := deviceRepo.SaveSmartAttributes(c, c.Param("wwn"), collectorSmartData)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving smartctl metrics", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -56,7 +52,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, scrutiny_uuid, smartData.Status)
|
||||
updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while updating device status", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -65,7 +61,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
// save smart temperature data (ignore failures)
|
||||
err = deviceRepo.SaveSmartTemperature(c, scrutiny_uuid, updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
|
||||
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)))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving smartctl temp data", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -77,7 +73,6 @@ 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)),
|
||||
|
||||
@@ -2,10 +2,6 @@ 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"
|
||||
@@ -13,6 +9,9 @@ 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 {
|
||||
@@ -38,15 +37,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/: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.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.GET("/settings", handler.GetSettings) //used to get settings
|
||||
api.POST("/settings", handler.SaveSettings) //used to save settings
|
||||
|
||||
@@ -19,7 +19,6 @@ 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"
|
||||
@@ -36,7 +35,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.2
|
||||
influxdb:2.0
|
||||
*/
|
||||
|
||||
//func TestMain(m *testing.M) {
|
||||
@@ -217,7 +216,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
require.Equal(suite.T(), 200, wr.Code)
|
||||
|
||||
mr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/9a4d34b5-b2ee-51ef-8506-90eea09be417/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
@@ -276,31 +275,28 @@ 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/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
fr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/3ea22b35-682b-49fb-a655-abffed108e48/smart", failfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ec3183/smart", failfile)
|
||||
router.ServeHTTP(fr, req)
|
||||
require.Equal(suite.T(), 200, fr.Code)
|
||||
|
||||
nr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/d8796fe7-2422-520c-8991-e970993dad3e/smart", nvmefile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5002538e40a22954/smart", nvmefile)
|
||||
router.ServeHTTP(nr, req)
|
||||
require.Equal(suite.T(), 200, nr.Code)
|
||||
|
||||
sr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/00328b73-9f8a-53ad-8f20-8d0b1be00f47/smart", scsifile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca252c859cc/smart", scsifile)
|
||||
router.ServeHTTP(sr, req)
|
||||
require.Equal(suite.T(), 200, sr.Code)
|
||||
|
||||
s2r := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/e5ccc378-24fc-5a9d-b1ce-8732096a9ea5/smart", scsi2file)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ebc248/smart", scsi2file)
|
||||
router.ServeHTTP(s2r, req)
|
||||
require.Equal(suite.T(), 200, s2r.Code)
|
||||
|
||||
@@ -559,7 +555,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
require.Equal(suite.T(), 200, wr.Code)
|
||||
|
||||
mr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/bde1d2d2-7e5c-525a-8327-6adbfa382637/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/a4c8e8ed-11a0-4c97-9bba-306440f1b944/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
@@ -572,8 +568,6 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
//assert
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "NVMe",
|
||||
"device_type": "nvme",
|
||||
"scrutiny_uuid": "bde1d2d2-7e5c-525a-8327-6adbfa382637"
|
||||
"device_type": "nvme"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+10
-17
@@ -12,29 +12,27 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 500107862016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK1XXXX",
|
||||
"serial_number": "9RK1XXXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "3ea22b35-682b-49fb-a655-abffed108e48"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -42,8 +40,7 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "42caca8a-9b95-5c75-b059-305771a2a193"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca252c859cc",
|
||||
@@ -57,8 +54,7 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 8001563222016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "d8796fe7-2422-520c-8991-e970993dad3e"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca264ebc248",
|
||||
@@ -72,8 +68,7 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "00328b73-9f8a-53ad-8f20-8d0b1be00f47"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"wwn": "0x50014ee20b2a72a9",
|
||||
@@ -87,8 +82,7 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 6001175126016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "e5ccc378-24fc-5a9d-b1ce-8732096a9ea5"
|
||||
"smart_support": false
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000c500673e6b5f",
|
||||
@@ -102,8 +96,7 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 6001175126016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "acfbce7d-0e19-579b-895e-85809dab63fb"
|
||||
"smart_support": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK1XXXX",
|
||||
"serial_number": "9RK1XXXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "9a4d34b5-b2ee-51ef-8506-90eea09be417"
|
||||
"smart_support": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+17
-45
@@ -363,7 +363,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-13.3.12.tgz",
|
||||
"integrity": "sha512-dc2JDokKJuuNxzzZa9FvuQU71kYC/e0xCLjGxEgX48sGKwajHRGBuzYFb8EmvLeA24SshYGmrxN0vGG9GhLK6g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -378,7 +377,6 @@
|
||||
"version": "13.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.9.tgz",
|
||||
"integrity": "sha512-XCuCbeuxWFyo3EYrgEYx7eHzwl76vaWcxtWXl00ka8d+WAOtMQ6Tf1D98ybYT5uwF9889fFpXAPw98mVnlo3MA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -431,7 +429,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-13.3.12.tgz",
|
||||
"integrity": "sha512-Nk4zNKfda92aFe+cucHRv2keyryR7C1ZnsurwZW9WZSobpY3z2tTT81F+yy35lGoMt5BDBAIpfh1b4j9Ue/vMg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -447,7 +444,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-13.3.12.tgz",
|
||||
"integrity": "sha512-F5vJYrjbNvEWoVz9J/CqiT3Iod6g9bV0dGI5EeURcW4yHXHZ12ioQpfU3+bE7qXcTlnofbdDhK8cGxGx01SzBA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -460,7 +456,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-13.3.12.tgz",
|
||||
"integrity": "sha512-6jrdVwexPihWlyitopc3rn2ReEkhAaMI8UWR0SOTnt3NaqNYWeio4bpeWlumgNPElDyY5rmyrmJgeaY8ICa8qA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.17.2",
|
||||
"chokidar": "^3.0.0",
|
||||
@@ -584,7 +579,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-13.3.12.tgz",
|
||||
"integrity": "sha512-jx0YC+NbPMbxGr5bXECkCEQv2RdVxR8AJNnabkPk8ZjwCpDzROrbELwwS1kunrZUhffcD15IhWGBvf1EGHAYDw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -600,7 +594,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-13.3.12.tgz",
|
||||
"integrity": "sha512-auow1TKZx44ha1ia8Jwg2xp2Q7BbpShG5Ft8tewL3T44aTmJY7svWOE/m+DkZ/SXHmTFnbZFobGU5aEfe0+pNw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -627,7 +620,6 @@
|
||||
"version": "13.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.9.tgz",
|
||||
"integrity": "sha512-FU8lcMgo+AL8ckd27B4V097ZPoIZNRHiCe3wpgkImT1qC0YwcyXZVn0MqQTTFSdC9a/aI8wPm3AbTClJEVw5Vw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -658,7 +650,6 @@
|
||||
"version": "13.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-13.3.12.tgz",
|
||||
"integrity": "sha512-sfhQqU4xjTJCjkH62TQeH5/gkay/KzvNDF95J6NHi/Q6p2dbtzZdXuLJKR/sHxtF2kc505z5v9RNm6XMSXM1KA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -742,7 +733,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz",
|
||||
"integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.16.8",
|
||||
@@ -3104,8 +3094,7 @@
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
@@ -3374,7 +3363,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3480,7 +3468,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
|
||||
"integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
@@ -4102,7 +4089,6 @@
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001449",
|
||||
"electron-to-chromium": "^1.4.284",
|
||||
@@ -4569,7 +4555,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
@@ -4590,15 +4575,13 @@
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/codelyzer/node_modules/zone.js": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz",
|
||||
"integrity": "sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -8077,8 +8060,7 @@
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz",
|
||||
"integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jasmine-spec-reporter": {
|
||||
"version": "7.0.0",
|
||||
@@ -8311,7 +8293,6 @@
|
||||
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz",
|
||||
"integrity": "sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"body-parser": "^1.19.0",
|
||||
@@ -8399,7 +8380,6 @@
|
||||
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
|
||||
"integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"jasmine-core": "^4.1.0"
|
||||
},
|
||||
@@ -9263,7 +9243,6 @@
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -10399,10 +10378,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -10515,7 +10495,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
|
||||
"integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.1.30",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -11148,7 +11127,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
|
||||
"integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -11214,7 +11192,6 @@
|
||||
"integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==",
|
||||
"deprecated": "We have news to share - Protractor is deprecated and will reach end-of-life by Summer 2023. To learn more and find out about other options please refer to this post on the Angular blog. Thank you for using and contributing to Protractor. https://goo.gle/state-of-e2e-in-angular",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/q": "^0.0.32",
|
||||
"@types/selenium-webdriver": "^3.0.0",
|
||||
@@ -12020,7 +11997,6 @@
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -12056,7 +12032,6 @@
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
@@ -12178,7 +12153,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -12982,7 +12956,6 @@
|
||||
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.56.0.tgz",
|
||||
"integrity": "sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"css": "^3.0.0",
|
||||
"debug": "^4.3.2",
|
||||
@@ -13098,6 +13071,7 @@
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.0.1"
|
||||
},
|
||||
@@ -13109,6 +13083,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": ">=2.3.x"
|
||||
},
|
||||
@@ -13120,6 +13095,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.2.5"
|
||||
},
|
||||
@@ -13130,12 +13106,14 @@
|
||||
"node_modules/svg.js": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/svg.pathmorphing.js": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.4.0"
|
||||
},
|
||||
@@ -13147,6 +13125,7 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.6.5",
|
||||
"svg.select.js": "^2.1.2"
|
||||
@@ -13159,6 +13138,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.2.5"
|
||||
},
|
||||
@@ -13170,6 +13150,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"svg.js": "^2.6.5"
|
||||
},
|
||||
@@ -13191,7 +13172,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
|
||||
"integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -13262,7 +13242,6 @@
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -13423,7 +13402,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -13614,7 +13592,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -13670,7 +13647,6 @@
|
||||
"integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
|
||||
"deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"builtin-modules": "^1.1.1",
|
||||
@@ -13795,7 +13771,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
|
||||
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14166,7 +14141,6 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
|
||||
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^0.0.51",
|
||||
@@ -14256,7 +14230,6 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz",
|
||||
"integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.9",
|
||||
"@types/connect-history-api-fallback": "^1.3.5",
|
||||
@@ -14475,7 +14448,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
||||
@@ -38,7 +38,7 @@ export const appRoutes: Route[] = [
|
||||
|
||||
// Example
|
||||
{path: 'dashboard', loadChildren: () => import('app/modules/dashboard/dashboard.module').then(m => m.DashboardModule)},
|
||||
{path: 'device/:scrutiny_uuid', loadChildren: () => import('app/modules/detail/detail.module').then(m => m.DetailModule)}
|
||||
{path: 'device/:wwn', 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)},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// maps to webapp/backend/pkg/models/device.go
|
||||
export interface DeviceModel {
|
||||
archived?: boolean;
|
||||
scrutiny_uuid: string;
|
||||
wwn: string;
|
||||
device_name?: string;
|
||||
device_uuid?: string;
|
||||
|
||||
@@ -4,7 +4,6 @@ import {SmartAttributeModel} from './smart-attribute-model';
|
||||
export interface SmartModel {
|
||||
date: string;
|
||||
device_wwn: string;
|
||||
scrutiny_uuid: string;
|
||||
device_protocol: string;
|
||||
|
||||
temp: number;
|
||||
|
||||
@@ -40,7 +40,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
register(): void
|
||||
{
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/details')
|
||||
.onGet('/api/device/0x5002538e40a22954/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -50,7 +50,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/3ea22b35-682b-49fb-a655-abffed108e48/details')
|
||||
.onGet('/api/device/0x5000cca264eb01d7/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -60,7 +60,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/42caca8a-9b95-5c75-b059-305771a2a193/details')
|
||||
.onGet('/api/device/0x5000cca264ec3183/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -70,7 +70,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/d8796fe7-2422-520c-8991-e970993dad3e/details')
|
||||
.onGet('/api/device/0x5000cca252c859cc/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -80,17 +80,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.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')
|
||||
.onGet('/api/device/0x5000cca264ebc248/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
|
||||
@@ -5,7 +5,6 @@ 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',
|
||||
@@ -27,7 +26,6 @@ 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,
|
||||
|
||||
@@ -5,13 +5,12 @@ 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': '9RK1XXXX',
|
||||
'serial_number': '9RK1XXXXX',
|
||||
'firmware': '81.00A81',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 14000519643136,
|
||||
@@ -26,7 +25,6 @@ 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,
|
||||
@@ -247,7 +245,6 @@ 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,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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',
|
||||
@@ -26,7 +25,6 @@ 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,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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',
|
||||
@@ -26,7 +25,6 @@ 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,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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',
|
||||
@@ -26,7 +25,6 @@ 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,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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',
|
||||
|
||||
@@ -4,13 +4,12 @@ import * as moment from 'moment';
|
||||
export const summary = {
|
||||
'data': {
|
||||
'summary': {
|
||||
'acfbce7d-0e19-579b-895e-85809dab63fb': {
|
||||
'0x5000c500673e6b5f': {
|
||||
'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': '',
|
||||
@@ -33,13 +32,12 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'd8796fe7-2422-520c-8991-e970993dad3e': {
|
||||
'0x5000cca252c859cc': {
|
||||
'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',
|
||||
@@ -71,22 +69,21 @@ export const summary = {
|
||||
'temp': 34
|
||||
}]
|
||||
},
|
||||
'3ea22b35-682b-49fb-a655-abffed108e48': {
|
||||
'0x5000cca264eb01d7': {
|
||||
'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-9RK1XXXX',
|
||||
'device_serial_id': 'ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': '9RK1XXXX',
|
||||
'serial_number': '9RK1XXXXX',
|
||||
'firmware': '81.00A81',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 14000519643136,
|
||||
@@ -109,13 +106,12 @@ export const summary = {
|
||||
'temp': 32
|
||||
}]
|
||||
},
|
||||
'00328b73-9f8a-53ad-8f20-8d0b1be00f47': {
|
||||
'0x5000cca264ebc248': {
|
||||
'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',
|
||||
@@ -138,13 +134,12 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'42caca8a-9b95-5c75-b059-305771a2a193': {
|
||||
'0x5000cca264ec3183': {
|
||||
'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',
|
||||
@@ -560,13 +555,12 @@ export const summary = {
|
||||
'temp': 39
|
||||
}]
|
||||
},
|
||||
'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5': {
|
||||
'0x50014ee20b2a72a9': {
|
||||
'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',
|
||||
@@ -589,13 +583,12 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c': {
|
||||
'0x5002538e40a22954': {
|
||||
'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': '',
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ describe('DashboardDeviceArchiveDialogComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{provide: MatDialogRef, useValue: matDialogRefSpy},
|
||||
{provide: MAT_DIALOG_DATA, useValue: {scrutiny_uuid: 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', title: 'my-test-device-title'}},
|
||||
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', 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('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c');
|
||||
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn');
|
||||
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count())
|
||||
.withContext('one call')
|
||||
.toBe(1);
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DashboardDeviceArchiveDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string},
|
||||
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
|
||||
private _archiveService: DashboardDeviceArchiveDialogService,
|
||||
) {
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export class DashboardDeviceArchiveDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
onArchiveClick(): void {
|
||||
this._archiveService.archiveDevice(this.data.scrutiny_uuid)
|
||||
this._archiveService.archiveDevice(this.data.wwn)
|
||||
.subscribe((data) => {
|
||||
this.dialogRef.close(data);
|
||||
});
|
||||
|
||||
+4
-4
@@ -26,13 +26,13 @@ export class DashboardDeviceArchiveDialogService
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
archiveDevice(scrutiny_uid: string): Observable<any>
|
||||
archiveDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/archive`, {});
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {});
|
||||
}
|
||||
|
||||
unarchiveDevice(scrutiny_uid: string): Observable<any>
|
||||
unarchiveDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${scrutiny_uid}/unarchive`, {});
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ describe('DashboardDeviceDeleteDialogComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{provide: MatDialogRef, useValue: matDialogRefSpy},
|
||||
{provide: MAT_DIALOG_DATA, useValue: {scrutiny_uuid: 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c', title: 'my-test-device-title'}},
|
||||
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', 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('ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c');
|
||||
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn');
|
||||
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count())
|
||||
.withContext('one call')
|
||||
.toBe(1);
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {scrutiny_uuid: string, title: string},
|
||||
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
|
||||
private _deleteService: DashboardDeviceDeleteDialogService,
|
||||
) {
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export class DashboardDeviceDeleteDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
onDeleteClick(): void {
|
||||
this._deleteService.deleteDevice(this.data.scrutiny_uuid)
|
||||
this._deleteService.deleteDevice(this.data.wwn)
|
||||
.subscribe((data) => {
|
||||
this.dialogRef.close(data);
|
||||
});
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ export class DashboardDeviceDeleteDialogService
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
deleteDevice(scrutiny_uuid: string): Observable<any>
|
||||
deleteDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.delete( `${getBasePath()}/api/device/${scrutiny_uuid}`, {});
|
||||
return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ deviceSummary.device.scrutiny_uuid"
|
||||
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
|
||||
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
@@ -30,7 +30,7 @@
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.scrutiny_uuid">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'assessment'"></mat-icon>
|
||||
|
||||
+6
-6
@@ -75,22 +75,22 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
|
||||
openArchiveDialog(): void {
|
||||
if(this.deviceSummary.device.archived){
|
||||
this._archiveService.unarchiveDevice(this.deviceSummary.device.scrutiny_uuid).subscribe((result) => {
|
||||
this._archiveService.unarchiveDevice(this.deviceSummary.device.wwn).subscribe((result) => {
|
||||
if(result) {
|
||||
this.deviceUnarchived.emit(this.deviceSummary.device.scrutiny_uuid)
|
||||
this.deviceUnarchived.emit(this.deviceSummary.device.wwn)
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, {
|
||||
data: {
|
||||
scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid,
|
||||
wwn: this.deviceSummary.device.wwn,
|
||||
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if(result) {
|
||||
this.deviceArchived.emit(this.deviceSummary.device.scrutiny_uuid);
|
||||
this.deviceArchived.emit(this.deviceSummary.device.wwn);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
||||
// width: '250px',
|
||||
data: {
|
||||
scrutiny_uuid: this.deviceSummary.device.scrutiny_uuid,
|
||||
wwn: this.deviceSummary.device.wwn,
|
||||
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.scrutiny_uuid)
|
||||
this.deviceDeleted.emit(this.deviceSummary.device.wwn)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
this.summaryData = data;
|
||||
|
||||
// generate group data.
|
||||
for (const scrutiny_uuid in this.summaryData) {
|
||||
const hostid = this.summaryData[scrutiny_uuid].device.host_id
|
||||
for (const wwn in this.summaryData) {
|
||||
const hostid = this.summaryData[wwn].device.host_id
|
||||
const hostDeviceList = this.hostGroups[hostid] || []
|
||||
hostDeviceList.push(scrutiny_uuid)
|
||||
hostDeviceList.push(wwn)
|
||||
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 scrutiny_uuid in this.summaryData) {
|
||||
const deviceSummary = this.summaryData[scrutiny_uuid]
|
||||
for (const wwn in this.summaryData) {
|
||||
const deviceSummary = this.summaryData[wwn]
|
||||
if (!deviceSummary.temp_history) {
|
||||
continue
|
||||
}
|
||||
@@ -241,11 +241,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
deviceSummariesForHostGroup(hostGroupScrutinyUUIDs: string[]): DeviceSummaryModel[] {
|
||||
deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] {
|
||||
const deviceSummaries: DeviceSummaryModel[] = []
|
||||
for (const scrutiny_uuid of hostGroupScrutinyUUIDs) {
|
||||
if (this.summaryData[scrutiny_uuid]) {
|
||||
deviceSummaries.push(this.summaryData[scrutiny_uuid])
|
||||
for (const wwn of hostGroupWWNs) {
|
||||
if (this.summaryData[wwn]) {
|
||||
deviceSummaries.push(this.summaryData[wwn])
|
||||
}
|
||||
}
|
||||
return deviceSummaries
|
||||
@@ -259,16 +259,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
});
|
||||
}
|
||||
|
||||
onDeviceDeleted(scrutiny_uuid: string): void {
|
||||
delete this.summaryData[scrutiny_uuid] // remove the device from the summary list.
|
||||
onDeviceDeleted(wwn: string): void {
|
||||
delete this.summaryData[wwn] // remove the device from the summary list.
|
||||
}
|
||||
|
||||
onDeviceArchived(scrutiny_uuid: string): void {
|
||||
this.summaryData[scrutiny_uuid].device.archived = true;
|
||||
onDeviceArchived(wwn: string): void {
|
||||
this.summaryData[wwn].device.archived = true;
|
||||
}
|
||||
|
||||
onDeviceUnarchived(scrutiny_uuid: string): void {
|
||||
this.summaryData[scrutiny_uuid].device.archived = false;
|
||||
onDeviceUnarchived(wwn: string): void {
|
||||
this.summaryData[wwn].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 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] || []
|
||||
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] || []
|
||||
}
|
||||
|
||||
// Prepare the chart series data
|
||||
|
||||
@@ -98,10 +98,6 @@
|
||||
<div>{{device?.serial_number}}</div>
|
||||
<div class="text-secondary text-md">Serial Number</div>
|
||||
</div>
|
||||
<div class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{device?.scrutiny_uuid}}</div>
|
||||
<div class="text-secondary text-md">Scrutiny UUID</div>
|
||||
</div>
|
||||
<div class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{device?.wwn}}</div>
|
||||
<div class="text-secondary text-md">LU WWN Device Id</div>
|
||||
|
||||
@@ -30,6 +30,6 @@ export class DetailResolver implements Resolve<any> {
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> {
|
||||
return this._detailService.getData(route.params.scrutiny_uuid);
|
||||
return this._detailService.getData(route.params.wwn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export class DetailService {
|
||||
/**
|
||||
* Get data
|
||||
*/
|
||||
getData(scrutiny_uuid): Observable<DeviceDetailsResponseWrapper> {
|
||||
return this._httpClient.get(getBasePath() + `/api/device/${scrutiny_uuid}/details`).pipe(
|
||||
getData(wwn): Observable<DeviceDetailsResponseWrapper> {
|
||||
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
|
||||
tap((response: DeviceDetailsResponseWrapper) => {
|
||||
this._data.next(response);
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ export class DeviceTitlePipe implements PipeTransform {
|
||||
}
|
||||
|
||||
static deviceTitleWithFallback(device: DeviceModel, titleType: string): string {
|
||||
console.log(`Displaying Device ${device.scrutiny_uuid} with: ${titleType}`)
|
||||
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
|
||||
const titleParts = []
|
||||
if (device.host_id) titleParts.push(device.host_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user