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

* Generate a UUIDv5 from a random namespace  based on WWN, model name, and serial number
* Migrate sqlite and influxdb data accordingly
* Update frontend API routes and components
* Fixes #923
This commit is contained in:
Aram Akhavan
2026-03-25 20:16:17 -07:00
committed by GitHub
parent e4c40f7e80
commit c3b2eb2b4f
69 changed files with 815 additions and 402 deletions
@@ -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)),