// Package config holds nasmon settings, persisted as a JSON file that is // editable both by hand and through the web UI. package config import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "sync" ) type SMTP struct { Host string `json:"host"` Port int `json:"port"` Security string `json:"security"` // starttls | tls | none User string `json:"user"` Pass string `json:"pass"` From string `json:"from"` To []string `json:"to"` SubjectPrefix string `json:"subject_prefix"` } type Thresholds struct { DiskTempC int `json:"disk_temp_c"` CPUTempC int `json:"cpu_temp_c"` PoolCapacityPct int `json:"pool_capacity_pct"` ScrubMaxAgeDays int `json:"scrub_max_age_days"` RootFSFreePct int `json:"rootfs_free_pct"` NVMeUsedPct int `json:"nvme_used_pct"` NVMeSparePct int `json:"nvme_spare_pct"` } type SelfTests struct { Enabled bool `json:"enabled"` ShortDays int `json:"short_days"` LongDays int `json:"long_days"` Hour int `json:"hour"` // local hour of day when tests may be started } type Settings struct { SMTP SMTP `json:"smtp"` AlertsEnabled bool `json:"alerts_enabled"` NotifyResolved bool `json:"notify_resolved"` RemindHours int `json:"remind_hours"` SmartIntervalMin int `json:"smart_interval_min"` SmartSkipStandby bool `json:"smart_skip_standby"` Thresholds Thresholds `json:"thresholds"` SelfTests SelfTests `json:"self_tests"` } func Defaults() Settings { return Settings{ SMTP: SMTP{Port: 587, Security: "starttls", SubjectPrefix: "[nasmon]"}, AlertsEnabled: true, NotifyResolved: true, RemindHours: 24, SmartIntervalMin: 30, SmartSkipStandby: true, Thresholds: Thresholds{ DiskTempC: 50, CPUTempC: 80, PoolCapacityPct: 85, ScrubMaxAgeDays: 45, RootFSFreePct: 10, NVMeUsedPct: 90, NVMeSparePct: 10, }, SelfTests: SelfTests{Enabled: false, ShortDays: 7, LongDays: 30, Hour: 3}, } } // Validate normalises and checks values coming from the web form. func (s *Settings) Validate() error { s.SMTP.Security = strings.ToLower(strings.TrimSpace(s.SMTP.Security)) switch s.SMTP.Security { case "starttls", "tls", "none": default: return fmt.Errorf("smtp security must be starttls, tls or none") } if s.SMTP.Port <= 0 || s.SMTP.Port > 65535 { return errors.New("smtp port out of range") } var to []string for _, a := range s.SMTP.To { if a = strings.TrimSpace(a); a != "" { to = append(to, a) } } s.SMTP.To = to if s.RemindHours < 0 { return errors.New("remind hours must be >= 0") } if s.SmartIntervalMin < 1 { return errors.New("SMART interval must be >= 1 minute") } if s.SelfTests.Hour < 0 || s.SelfTests.Hour > 23 { return errors.New("self-test hour must be 0..23") } if s.SelfTests.ShortDays < 0 || s.SelfTests.LongDays < 0 { return errors.New("self-test periods must be >= 0") } return nil } // Store is a thread-safe holder for the current settings backed by a file. type Store struct { path string mu sync.RWMutex cur Settings } func Load(path string) (*Store, error) { st := &Store{path: path, cur: Defaults()} b, err := os.ReadFile(path) switch { case errors.Is(err, os.ErrNotExist): return st, nil case err != nil: return nil, err } if err := json.Unmarshal(b, &st.cur); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } return st, nil } func (st *Store) Get() Settings { st.mu.RLock() defer st.mu.RUnlock() s := st.cur s.SMTP.To = append([]string(nil), s.SMTP.To...) return s } func (st *Store) Save(s Settings) error { if err := s.Validate(); err != nil { return err } b, err := json.MarshalIndent(s, "", " ") if err != nil { return err } st.mu.Lock() defer st.mu.Unlock() if err := os.MkdirAll(filepath.Dir(st.path), 0o755); err != nil { return err } tmp := st.path + ".tmp" if err := os.WriteFile(tmp, append(b, '\n'), 0o600); err != nil { return err } if err := os.Rename(tmp, st.path); err != nil { return err } st.cur = s return nil }