package measurements
import (
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
)
type Smart struct {
Date time . Time ` json:"date" `
DeviceWWN string ` json:"device_wwn" ` //(tag)
DeviceProtocol string ` json:"device_protocol" `
// Metrics (fields)
Temp int64 ` json:"temp" `
PowerOnHours int64 ` json:"power_on_hours" `
PowerCycleCount int64 ` json:"power_cycle_count" `
// Attributes (fields)
Attributes map [ string ] SmartAttribute ` json:"attrs" `
// status
Status pkg . DeviceStatus
}
func ( sm * Smart ) Flatten ( ) ( tags map [ string ] string , fields map [ string ] interface { } ) {
tags = map [ string ] string {
"device_wwn" : sm . DeviceWWN ,
"device_protocol" : sm . DeviceProtocol ,
}
fields = map [ string ] interface { } {
"temp" : sm . Temp ,
"power_on_hours" : sm . PowerOnHours ,
"power_cycle_count" : sm . PowerCycleCount ,
}
for _ , attr := range sm . Attributes {
for attrKey , attrVal := range attr . Flatten ( ) {
fields [ attrKey ] = attrVal
}
}
return tags , fields
}
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.
sm := Smart {
// required fields
Date : attrs [ "_time" ] . ( time . Time ) ,
DeviceWWN : attrs [ "device_wwn" ] . ( string ) ,
DeviceProtocol : attrs [ "device_protocol" ] . ( string ) ,
Attributes : map [ string ] SmartAttribute { } ,
}
for key , val := range attrs {
switch key {
case "temp" :
sm . Temp = val . ( int64 )
case "power_on_hours" :
sm . PowerOnHours = val . ( int64 )
case "power_cycle_count" :
sm . PowerCycleCount = val . ( int64 )
default :
// this key is unknown.
if ! strings . HasPrefix ( key , "attr." ) {
continue
}
// this is a attribute, lets group it with its related "siblings", populating a SmartAttribute object
keyParts := strings . Split ( key , "." )
attributeId := keyParts [ 1 ]
if _ , ok := sm . Attributes [ attributeId ] ; ! ok {
// init the attribute group
if sm . DeviceProtocol == pkg . DeviceProtocolAta {
sm . Attributes [ attributeId ] = & SmartAtaAttribute { }
} else if sm . DeviceProtocol == pkg . DeviceProtocolNvme {
sm . Attributes [ attributeId ] = & SmartNvmeAttribute { }
} else if sm . DeviceProtocol == pkg . DeviceProtocolScsi {
sm . Attributes [ attributeId ] = & SmartScsiAttribute { }
} else {
return nil , fmt . Errorf ( "Unknown Device Protocol: %s" , sm . DeviceProtocol )
}
}
sm . Attributes [ attributeId ] . Inflate ( key , val )
}
}
log . Printf ( "Found Smart Device (%s) Attributes (%v)" , sm . DeviceWWN , len ( sm . Attributes ) )
return & sm , nil
}
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
func ( sm * Smart ) FromCollectorSmartInfo ( wwn string , info collector . SmartInfo ) error {
sm . DeviceWWN = wwn
sm . Date = time . Unix ( info . LocalTime . TimeT , 0 )
// smart metrics
sm . Temp = info . Temperature . Current
sm . PowerCycleCount = info . PowerCycleCount
sm . PowerOnHours = info . PowerOnTime . Hours
if ! info . SmartStatus . Passed {
sm . Status = pkg . DeviceStatusSet ( sm . Status , pkg . DeviceStatusFailedSmart )
}
sm . DeviceProtocol = info . Device . Protocol
// process ATA/NVME/SCSI protocol data
sm . Attributes = map [ string ] SmartAttribute { }
if sm . DeviceProtocol == pkg . DeviceProtocolAta {
sm . ProcessAtaSmartInfo ( info . AtaSmartAttributes . Table )
} else if sm . DeviceProtocol == pkg . DeviceProtocolNvme {
sm . ProcessNvmeSmartInfo ( info . NvmeSmartHealthInformationLog )
} else if sm . DeviceProtocol == pkg . DeviceProtocolScsi {
sm . ProcessScsiSmartInfo ( info . ScsiGrownDefectList , info . ScsiErrorCounterLog )
}
return nil
}
// generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
func ( sm * Smart ) ProcessAtaSmartInfo ( tableItems [ ] collector . AtaSmartAttributesTableItem ) {
for _ , collectorAttr := range tableItems {
attrModel := SmartAtaAttribute {
AttributeId : collectorAttr . ID ,
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 := thresholds . AtaMetadata [ collectorAttr . ID ] ; ok {
if smartMetadata . Transform != nil {
attrModel . TransformedValue = smartMetadata . Transform ( attrModel . Value , attrModel . RawValue , attrModel . RawString )
}
}
attrModel . PopulateAttributeStatus ( )
sm . Attributes [ strconv . Itoa ( collectorAttr . ID ) ] = & attrModel
if pkg . AttributeStatusHas ( attrModel . Status , pkg . AttributeStatusFailedScrutiny ) {
sm . Status = pkg . DeviceStatusSet ( sm . Status , pkg . DeviceStatusFailedScrutiny )
}
}
}
// generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
func ( sm * Smart ) ProcessNvmeSmartInfo ( nvmeSmartHealthInformationLog collector . NvmeSmartHealthInformationLog ) {
sm . Attributes = map [ string ] SmartAttribute {
"critical_warning" : ( & SmartNvmeAttribute { AttributeId : "critical_warning" , Value : nvmeSmartHealthInformationLog . CriticalWarning , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"temperature" : ( & SmartNvmeAttribute { AttributeId : "temperature" , Value : nvmeSmartHealthInformationLog . Temperature , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"available_spare" : ( & SmartNvmeAttribute { AttributeId : "available_spare" , Value : nvmeSmartHealthInformationLog . AvailableSpare , Threshold : nvmeSmartHealthInformationLog . AvailableSpareThreshold } ) . PopulateAttributeStatus ( ) ,
"percentage_used" : ( & SmartNvmeAttribute { AttributeId : "percentage_used" , Value : nvmeSmartHealthInformationLog . PercentageUsed , Threshold : 100 } ) . PopulateAttributeStatus ( ) ,
"data_units_read" : ( & SmartNvmeAttribute { AttributeId : "data_units_read" , Value : nvmeSmartHealthInformationLog . DataUnitsRead , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"data_units_written" : ( & SmartNvmeAttribute { AttributeId : "data_units_written" , Value : nvmeSmartHealthInformationLog . DataUnitsWritten , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"host_reads" : ( & SmartNvmeAttribute { AttributeId : "host_reads" , Value : nvmeSmartHealthInformationLog . HostReads , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"host_writes" : ( & SmartNvmeAttribute { AttributeId : "host_writes" , Value : nvmeSmartHealthInformationLog . HostWrites , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"controller_busy_time" : ( & SmartNvmeAttribute { AttributeId : "controller_busy_time" , Value : nvmeSmartHealthInformationLog . ControllerBusyTime , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"power_cycles" : ( & SmartNvmeAttribute { AttributeId : "power_cycles" , Value : nvmeSmartHealthInformationLog . PowerCycles , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"power_on_hours" : ( & SmartNvmeAttribute { AttributeId : "power_on_hours" , Value : nvmeSmartHealthInformationLog . PowerOnHours , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"unsafe_shutdowns" : ( & SmartNvmeAttribute { AttributeId : "unsafe_shutdowns" , Value : nvmeSmartHealthInformationLog . UnsafeShutdowns , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"media_errors" : ( & SmartNvmeAttribute { AttributeId : "media_errors" , Value : nvmeSmartHealthInformationLog . MediaErrors , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"num_err_log_entries" : ( & SmartNvmeAttribute { AttributeId : "num_err_log_entries" , Value : nvmeSmartHealthInformationLog . NumErrLogEntries , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"warning_temp_time" : ( & SmartNvmeAttribute { AttributeId : "warning_temp_time" , Value : nvmeSmartHealthInformationLog . WarningTempTime , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"critical_comp_time" : ( & SmartNvmeAttribute { AttributeId : "critical_comp_time" , Value : nvmeSmartHealthInformationLog . CriticalCompTime , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
}
// find analyzed attribute status
for _ , val := range sm . Attributes {
if pkg . AttributeStatusHas ( val . GetStatus ( ) , pkg . AttributeStatusFailedScrutiny ) {
sm . Status = pkg . DeviceStatusSet ( sm . Status , pkg . DeviceStatusFailedScrutiny )
}
}
}
// generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
func ( sm * Smart ) ProcessScsiSmartInfo ( defectGrownList int64 , scsiErrorCounterLog collector . ScsiErrorCounterLog ) {
sm . Attributes = map [ string ] SmartAttribute {
"scsi_grown_defect_list" : ( & SmartScsiAttribute { AttributeId : "scsi_grown_defect_list" , Value : defectGrownList , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"read_errors_corrected_by_eccfast" : ( & SmartScsiAttribute { AttributeId : "read_errors_corrected_by_eccfast" , Value : scsiErrorCounterLog . Read . ErrorsCorrectedByEccfast , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"read_errors_corrected_by_eccdelayed" : ( & SmartScsiAttribute { AttributeId : "read_errors_corrected_by_eccdelayed" , Value : scsiErrorCounterLog . Read . ErrorsCorrectedByEccdelayed , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"read_errors_corrected_by_rereads_rewrites" : ( & SmartScsiAttribute { AttributeId : "read_errors_corrected_by_rereads_rewrites" , Value : scsiErrorCounterLog . Read . ErrorsCorrectedByRereadsRewrites , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"read_total_errors_corrected" : ( & SmartScsiAttribute { AttributeId : "read_total_errors_corrected" , Value : scsiErrorCounterLog . Read . TotalErrorsCorrected , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"read_correction_algorithm_invocations" : ( & SmartScsiAttribute { AttributeId : "read_correction_algorithm_invocations" , Value : scsiErrorCounterLog . Read . CorrectionAlgorithmInvocations , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"read_total_uncorrected_errors" : ( & SmartScsiAttribute { AttributeId : "read_total_uncorrected_errors" , Value : scsiErrorCounterLog . Read . TotalUncorrectedErrors , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"write_errors_corrected_by_eccfast" : ( & SmartScsiAttribute { AttributeId : "write_errors_corrected_by_eccfast" , Value : scsiErrorCounterLog . Write . ErrorsCorrectedByEccfast , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"write_errors_corrected_by_eccdelayed" : ( & SmartScsiAttribute { AttributeId : "write_errors_corrected_by_eccdelayed" , Value : scsiErrorCounterLog . Write . ErrorsCorrectedByEccdelayed , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"write_errors_corrected_by_rereads_rewrites" : ( & SmartScsiAttribute { AttributeId : "write_errors_corrected_by_rereads_rewrites" , Value : scsiErrorCounterLog . Write . ErrorsCorrectedByRereadsRewrites , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
"write_total_errors_corrected" : ( & SmartScsiAttribute { AttributeId : "write_total_errors_corrected" , Value : scsiErrorCounterLog . Write . TotalErrorsCorrected , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"write_correction_algorithm_invocations" : ( & SmartScsiAttribute { AttributeId : "write_correction_algorithm_invocations" , Value : scsiErrorCounterLog . Write . CorrectionAlgorithmInvocations , Threshold : - 1 } ) . PopulateAttributeStatus ( ) ,
"write_total_uncorrected_errors" : ( & SmartScsiAttribute { AttributeId : "write_total_uncorrected_errors" , Value : scsiErrorCounterLog . Write . TotalUncorrectedErrors , Threshold : 0 } ) . PopulateAttributeStatus ( ) ,
}
// find analyzed attribute status
for _ , val := range sm . Attributes {
if pkg . AttributeStatusHas ( val . GetStatus ( ) , pkg . AttributeStatusFailedScrutiny ) {
sm . Status = pkg . DeviceStatusSet ( sm . Status , pkg . DeviceStatusFailedScrutiny )
}
}
}