Go service (nasmon/) plus nfpm packaging (packaging/), carried over from the home_nas monorepo. Adds rpm packaging alongside the existing deb build (Fedora/RHEL/Rocky dependency names via nfpm overrides, per-format systemd unit path, dpkg+rpm-compatible maintainer scripts) and a Gitea Actions workflow that builds both formats on every push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
// 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
|
|
}
|