Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3b2eb2b4f | |||
| e4c40f7e80 | |||
| 6cc9ff7fc5 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+5626
-9477
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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': '',
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
});
|
||||
|
||||
+4
-4
@@ -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`, {});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
});
|
||||
|
||||
+2
-2
@@ -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}`, {});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+6
-6
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user