Public source validation / validate (push) Failing after 3m8s
188 lines
6.2 KiB
Go
188 lines
6.2 KiB
Go
package disk
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type RawPerformanceSample struct {
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
ReadBytesPerSecond *float64 `json:"readBytesPerSecond,omitempty"`
|
|
WriteBytesPerSecond *float64 `json:"writeBytesPerSecond,omitempty"`
|
|
ReadIOPS *float64 `json:"readIops,omitempty"`
|
|
WriteIOPS *float64 `json:"writeIops,omitempty"`
|
|
ReadLatencyMs *float64 `json:"readLatencyMs,omitempty"`
|
|
WriteLatencyMs *float64 `json:"writeLatencyMs,omitempty"`
|
|
}
|
|
type PerformanceSample struct {
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
ReadBytesPerSecond *float64 `json:"readBytesPerSecond,omitempty"`
|
|
WriteBytesPerSecond *float64 `json:"writeBytesPerSecond,omitempty"`
|
|
ReadIOPS *float64 `json:"readIops,omitempty"`
|
|
WriteIOPS *float64 `json:"writeIops,omitempty"`
|
|
ReadLatencyMs *float64 `json:"readLatencyMs,omitempty"`
|
|
WriteLatencyMs *float64 `json:"writeLatencyMs,omitempty"`
|
|
}
|
|
type RawPerformance struct {
|
|
Available bool `json:"available"`
|
|
Current RawPerformanceSample `json:"current"`
|
|
History []RawPerformanceSample `json:"history,omitempty"`
|
|
}
|
|
type Performance struct {
|
|
State string `json:"state"`
|
|
Current PerformanceSample `json:"current"`
|
|
History []PerformanceSample `json:"history,omitempty"`
|
|
}
|
|
type RawTemperature struct {
|
|
Available bool `json:"available"`
|
|
Celsius float64 `json:"celsius"`
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
}
|
|
type Temperature struct {
|
|
State string `json:"state"`
|
|
Celsius *float64 `json:"celsius,omitempty"`
|
|
Status string `json:"status"`
|
|
ObservedAt *time.Time `json:"observedAt,omitempty"`
|
|
}
|
|
type RawSpin struct {
|
|
Supported bool `json:"supported"`
|
|
State string `json:"state"`
|
|
}
|
|
type Spin struct {
|
|
State string `json:"state"`
|
|
}
|
|
type PerformancePolicy struct {
|
|
MaxHistory int
|
|
TemperatureWarningCelsius float64
|
|
TemperatureCriticalCelsius float64
|
|
TemperatureRecoveryCelsius float64
|
|
}
|
|
|
|
func (p PerformancePolicy) withDefaults() PerformancePolicy {
|
|
if p.MaxHistory == 0 {
|
|
p.MaxHistory = 120
|
|
}
|
|
if p.TemperatureWarningCelsius == 0 {
|
|
p.TemperatureWarningCelsius = 50
|
|
}
|
|
if p.TemperatureCriticalCelsius == 0 {
|
|
p.TemperatureCriticalCelsius = 55
|
|
}
|
|
if p.TemperatureRecoveryCelsius == 0 {
|
|
p.TemperatureRecoveryCelsius = 45
|
|
}
|
|
return p
|
|
}
|
|
func (p PerformancePolicy) Validate() error {
|
|
if p.MaxHistory < 1 || p.MaxHistory > 1000 || p.TemperatureRecoveryCelsius >= p.TemperatureWarningCelsius || p.TemperatureWarningCelsius >= p.TemperatureCriticalCelsius {
|
|
return errors.New("disk performance policy is outside safe bounds")
|
|
}
|
|
return nil
|
|
}
|
|
func normalizePerformance(raw *RawPerformance, now time.Time, policy PerformancePolicy) (*Performance, error) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
policy = policy.withDefaults()
|
|
if err := policy.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
result := &Performance{State: "unknown", History: []PerformanceSample{}}
|
|
if !raw.Available {
|
|
result.State = "unsupported"
|
|
return result, nil
|
|
}
|
|
if len(raw.History) > policy.MaxHistory {
|
|
return nil, errors.New("disk performance history exceeds bounds")
|
|
}
|
|
current, err := normalizeSample(raw.Current, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.State = "available"
|
|
result.Current = current
|
|
for _, item := range raw.History {
|
|
sample, sampleErr := normalizeSample(item, now)
|
|
if sampleErr != nil {
|
|
return nil, sampleErr
|
|
}
|
|
result.History = append(result.History, sample)
|
|
}
|
|
sort.Slice(result.History, func(i, j int) bool { return result.History[i].ObservedAt.After(result.History[j].ObservedAt) })
|
|
return result, nil
|
|
}
|
|
func normalizeSample(raw RawPerformanceSample, now time.Time) (PerformanceSample, error) {
|
|
observed := raw.ObservedAt
|
|
if observed.IsZero() {
|
|
observed = now
|
|
}
|
|
if observed.After(now.Add(time.Minute)) {
|
|
return PerformanceSample{}, errors.New("disk performance observation is materially in the future")
|
|
}
|
|
for _, value := range []*float64{raw.ReadBytesPerSecond, raw.WriteBytesPerSecond, raw.ReadIOPS, raw.WriteIOPS, raw.ReadLatencyMs, raw.WriteLatencyMs} {
|
|
if value != nil && (*value < 0 || math.IsNaN(*value) || math.IsInf(*value, 0)) {
|
|
return PerformanceSample{}, errors.New("disk performance value is invalid")
|
|
}
|
|
}
|
|
return PerformanceSample{ObservedAt: observed.UTC(), ReadBytesPerSecond: raw.ReadBytesPerSecond, WriteBytesPerSecond: raw.WriteBytesPerSecond, ReadIOPS: raw.ReadIOPS, WriteIOPS: raw.WriteIOPS, ReadLatencyMs: raw.ReadLatencyMs, WriteLatencyMs: raw.WriteLatencyMs}, nil
|
|
}
|
|
func normalizeTemperature(raw *RawTemperature, now time.Time, policy PerformancePolicy) (*Temperature, error) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
result := &Temperature{State: "unknown", Status: "unknown"}
|
|
if !raw.Available {
|
|
return result, nil
|
|
}
|
|
if raw.Celsius < -50 || raw.Celsius > 150 || math.IsNaN(raw.Celsius) || math.IsInf(raw.Celsius, 0) {
|
|
return nil, errors.New("disk temperature is invalid")
|
|
}
|
|
observed := raw.ObservedAt
|
|
if observed.IsZero() {
|
|
observed = now
|
|
}
|
|
if observed.After(now.Add(time.Minute)) {
|
|
return nil, errors.New("disk temperature observation is materially in the future")
|
|
}
|
|
result.State = "available"
|
|
result.Celsius = &raw.Celsius
|
|
value := observed.UTC()
|
|
result.ObservedAt = &value
|
|
result.Status = temperatureStatus("normal", raw.Celsius, policy)
|
|
return result, nil
|
|
}
|
|
func temperatureStatus(previous string, celsius float64, policy PerformancePolicy) string {
|
|
policy = policy.withDefaults()
|
|
if celsius >= policy.TemperatureCriticalCelsius {
|
|
return "critical"
|
|
}
|
|
if celsius >= policy.TemperatureWarningCelsius {
|
|
return "attention"
|
|
}
|
|
if (previous == "attention" || previous == "critical") && celsius >= policy.TemperatureRecoveryCelsius {
|
|
return previous
|
|
}
|
|
return "normal"
|
|
}
|
|
func TemperatureStatus(previous string, celsius float64, policy PerformancePolicy) string {
|
|
return temperatureStatus(strings.ToLower(previous), celsius, policy)
|
|
}
|
|
func normalizeSpin(raw *RawSpin) (*Spin, error) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
if !raw.Supported {
|
|
return &Spin{State: "unsupported"}, nil
|
|
}
|
|
state := strings.ToLower(strings.TrimSpace(raw.State))
|
|
switch state {
|
|
case "spinning", "idle", "standby", "unknown":
|
|
return &Spin{State: state}, nil
|
|
default:
|
|
return &Spin{State: "unknown"}, nil
|
|
}
|
|
}
|