init
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceRespWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
Data []Device `json:"data"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
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"`
|
||||
|
||||
SmartResults []Smart `gorm:"foreignkey:DeviceWWN" json:"smart_results"`
|
||||
}
|
||||
|
||||
//This method requires a device with an array of SmartResults.
|
||||
//It will remove all SmartResults other than the first (the latest one)
|
||||
//All removed SmartResults, will be processed, grouping SmartAttribute by attribute_id
|
||||
// and adding theme to an array called History.
|
||||
func (dv *Device) SquashHistory() error {
|
||||
if len(dv.SmartResults) <= 1 {
|
||||
return nil //no history found. ignore
|
||||
}
|
||||
|
||||
latestSmartResultSlice := dv.SmartResults[0:1]
|
||||
historicalSmartResultSlice := dv.SmartResults[1:]
|
||||
|
||||
//re-assign the latest slice to the SmartResults field
|
||||
dv.SmartResults = latestSmartResultSlice
|
||||
|
||||
//process the historical slice
|
||||
history := map[int][]SmartAttribute{}
|
||||
for _, smartResult := range historicalSmartResultSlice {
|
||||
for _, smartAttribute := range smartResult.SmartAttributes {
|
||||
if _, ok := history[smartAttribute.AttributeId]; !ok {
|
||||
history[smartAttribute.AttributeId] = []SmartAttribute{}
|
||||
}
|
||||
history[smartAttribute.AttributeId] = append(history[smartAttribute.AttributeId], smartAttribute)
|
||||
}
|
||||
}
|
||||
|
||||
//now assign the historical slices to the SmartAttributes in the latest SmartResults
|
||||
for sandx, smartAttribute := range dv.SmartResults[0].SmartAttributes {
|
||||
if attributeHistory, ok := history[smartAttribute.AttributeId]; ok {
|
||||
dv.SmartResults[0].SmartAttributes[sandx].History = attributeHistory
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dv *Device) ApplyMetadataRules() error {
|
||||
//embed metadata in the latest smart attributes object
|
||||
if len(dv.SmartResults) > 0 {
|
||||
for ndx, attr := range dv.SmartResults[0].SmartAttributes {
|
||||
if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedFailingNow {
|
||||
//this attribute has previously failed
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusFailed
|
||||
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute is failing manufacturer SMART threshold"
|
||||
|
||||
} else if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedInThePast {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusWarning
|
||||
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute has previously failed manufacturer SMART threshold"
|
||||
}
|
||||
|
||||
if smartMetadata, ok := metadata.AtaSmartAttributes[attr.AttributeId]; ok {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].MetadataObservedThresholdStatus(smartMetadata)
|
||||
}
|
||||
|
||||
//check if status is blank, set to "passed"
|
||||
if len(dv.SmartResults[0].SmartAttributes[ndx].Status) == 0 {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusPassed
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
||||
dv.InterfaceSpeed = info.InterfaceSpeed.Current.String
|
||||
dv.Firmware = info.FirmwareVersion
|
||||
dv.RotationSpeed = info.RotationRate
|
||||
dv.Capacity = info.UserCapacity.Bytes
|
||||
dv.FormFactor = info.FormFactor.Name
|
||||
//dv.SmartSupport =
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
|
||||
type SelfTest struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
DeviceWWN string
|
||||
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
|
||||
Date time.Time
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/jinzhu/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
||||
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
||||
|
||||
type Smart struct {
|
||||
gorm.Model
|
||||
|
||||
DeviceWWN string `json:"device_wwn"`
|
||||
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
|
||||
TestDate time.Time `json:"date"`
|
||||
SmartStatus string `json:"smart_status"`
|
||||
|
||||
//Metrics
|
||||
Temp int64 `json:"temp"`
|
||||
PowerOnHours int64 `json:"power_on_hours"`
|
||||
PowerCycleCount int64 `json:"power_cycle_count"`
|
||||
|
||||
SmartAttributes []SmartAttribute `json:"smart_attributes" gorm:"foreignkey:SmartId"`
|
||||
}
|
||||
|
||||
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
|
||||
sm.DeviceWWN = wwn
|
||||
sm.TestDate = time.Unix(info.LocalTime.TimeT, 0)
|
||||
|
||||
//smart metrics
|
||||
sm.Temp = info.Temperature.Current
|
||||
sm.PowerCycleCount = info.PowerCycleCount
|
||||
sm.PowerOnHours = info.PowerOnTime.Hours
|
||||
|
||||
sm.SmartAttributes = []SmartAttribute{}
|
||||
for _, collectorAttr := range info.AtaSmartAttributes.Table {
|
||||
attrModel := SmartAttribute{
|
||||
AttributeId: collectorAttr.ID,
|
||||
Name: collectorAttr.Name,
|
||||
Value: collectorAttr.Value,
|
||||
Worst: collectorAttr.Worst,
|
||||
Threshold: collectorAttr.Thresh,
|
||||
RawValue: collectorAttr.Raw.Value,
|
||||
RawString: collectorAttr.Raw.String,
|
||||
WhenFailed: collectorAttr.WhenFailed,
|
||||
}
|
||||
|
||||
//now that we've parsed the data from the smartctl response, lets match it against our metadata rules and add additional Scrutiny specific data.
|
||||
if smartMetadata, ok := metadata.AtaSmartAttributes[collectorAttr.ID]; ok {
|
||||
attrModel.Name = smartMetadata.DisplayName
|
||||
if smartMetadata.Transform != nil {
|
||||
attrModel.TransformedValue = smartMetadata.Transform(attrModel.Value, attrModel.RawValue, attrModel.RawString)
|
||||
}
|
||||
}
|
||||
sm.SmartAttributes = append(sm.SmartAttributes, attrModel)
|
||||
}
|
||||
|
||||
if info.SmartStatus.Passed {
|
||||
sm.SmartStatus = "passed"
|
||||
} else {
|
||||
sm.SmartStatus = "failed"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const SmartAttributeStatusPassed = "passed"
|
||||
const SmartAttributeStatusFailed = "failed"
|
||||
const SmartAttributeStatusWarning = "warn"
|
||||
|
||||
type SmartAttribute struct {
|
||||
gorm.Model
|
||||
|
||||
SmartId int `json:"smart_id"`
|
||||
Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key
|
||||
|
||||
AttributeId int `json:"attribute_id"`
|
||||
Name string `json:"name"`
|
||||
Value int `json:"value"`
|
||||
Worst int `json:"worst"`
|
||||
Threshold int `json:"thresh"`
|
||||
RawValue int64 `json:"raw_value"`
|
||||
RawString string `json:"raw_string"`
|
||||
WhenFailed string `json:"when_failed"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status string `gorm:"-" json:"status,omitempty"`
|
||||
StatusReason string `gorm:"-" json:"status_reason,omitempty"`
|
||||
FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"`
|
||||
History []SmartAttribute `gorm:"-" json:"history,omitempty"`
|
||||
}
|
||||
|
||||
// compare the attribute (raw, normalized, transformed) value to observed thresholds, and update status if necessary
|
||||
func (sa *SmartAttribute) MetadataObservedThresholdStatus(smartMetadata metadata.AtaSmartAttribute) {
|
||||
//TODO: multiple rules
|
||||
// try to predict the failure rates for observed thresholds that have 0 failure rate and error bars.
|
||||
// - if the attribute is critical
|
||||
// - the failure rate is over 10 - set to failed
|
||||
// - the attribute does not match any threshold, set to warn
|
||||
// - if the attribute is not critical
|
||||
// - if failure rate is above 20 - set to failed
|
||||
// - if failure rate is above 10 but below 20 - set to warn
|
||||
|
||||
//update the smart attribute status based on Observed thresholds.
|
||||
var value int64
|
||||
if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeNormalized {
|
||||
value = int64(sa.Value)
|
||||
} else if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeTransformed {
|
||||
value = sa.TransformedValue
|
||||
} else {
|
||||
value = sa.RawValue
|
||||
}
|
||||
|
||||
for _, obsThresh := range smartMetadata.ObservedThresholds {
|
||||
|
||||
//check if "value" is in this bucket
|
||||
if ((obsThresh.Low == obsThresh.High) && value == obsThresh.Low) ||
|
||||
(obsThresh.Low < value && value <= obsThresh.High) {
|
||||
sa.FailureRate = obsThresh.AnnualFailureRate
|
||||
|
||||
if smartMetadata.Critical {
|
||||
if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
}
|
||||
} else {
|
||||
if obsThresh.AnnualFailureRate >= 0.20 {
|
||||
sa.Status = SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
|
||||
} else if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
|
||||
}
|
||||
}
|
||||
|
||||
//we've found the correct bucket, we can drop out of this loop
|
||||
return
|
||||
}
|
||||
}
|
||||
// no bucket found
|
||||
if smartMetadata.Critical {
|
||||
sa.Status = SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
//setup
|
||||
smartDataFile, err := os.Open("../testdata/smart.json")
|
||||
require.NoError(t, err)
|
||||
defer smartDataFile.Close()
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
//test
|
||||
smartMdl := db.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartMdl.DeviceWWN, "WWN-test")
|
||||
require.Equal(t, smartMdl.SmartStatus, "PASSED")
|
||||
|
||||
//check that temperature was correctly parsed
|
||||
for _, attr := range smartMdl.SmartAttributes {
|
||||
if attr.AttributeId == 194 {
|
||||
require.Equal(t, int64(163210330144), attr.RawValue)
|
||||
require.Equal(t, int64(32), attr.TransformedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user