Compare commits

..

3 Commits

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