128 lines
3.8 KiB
Go
128 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"scm.bstein.dev/bstein/ananke/internal/config"
|
|
"scm.bstein.dev/bstein/ananke/internal/service"
|
|
"scm.bstein.dev/bstein/ananke/internal/ups"
|
|
)
|
|
|
|
// buildUPSTargets runs one orchestration or CLI step.
|
|
// Signature: buildUPSTargets(cfg config.Config) ([]service.Target, error).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func buildUPSTargets(cfg config.Config) ([]service.Target, error) {
|
|
targets := make([]service.Target, 0, len(cfg.UPS.Targets)+1)
|
|
switch cfg.UPS.Provider {
|
|
case "nut":
|
|
if len(cfg.UPS.Targets) == 0 {
|
|
target := cfg.UPS.Target
|
|
if target == "" {
|
|
return nil, fmt.Errorf("ups target must be set")
|
|
}
|
|
targets = append(targets, service.Target{
|
|
Name: "primary",
|
|
Target: target,
|
|
Provider: ups.NewNUTProvider(target),
|
|
})
|
|
} else {
|
|
for idx, t := range cfg.UPS.Targets {
|
|
name := t.Name
|
|
if name == "" {
|
|
name = fmt.Sprintf("target-%d", idx+1)
|
|
}
|
|
targets = append(targets, service.Target{
|
|
Name: name,
|
|
Target: t.Target,
|
|
Provider: ups.NewNUTProvider(t.Target),
|
|
})
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported UPS provider: %s", cfg.UPS.Provider)
|
|
}
|
|
return targets, nil
|
|
}
|
|
|
|
// ensureStartupPowerSafe runs one orchestration or CLI step.
|
|
// Signature: ensureStartupPowerSafe(ctx context.Context, targets []service.Target, minimumBatteryPercent float64) error.
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func ensureStartupPowerSafe(ctx context.Context, targets []service.Target, minimumBatteryPercent float64) error {
|
|
type targetState struct {
|
|
seenGood bool
|
|
lastErr error
|
|
}
|
|
states := make(map[string]*targetState, len(targets))
|
|
for _, t := range targets {
|
|
key := t.Name + "|" + t.Target
|
|
states[key] = &targetState{}
|
|
}
|
|
const pollInterval = 3 * time.Second
|
|
for {
|
|
onBatteryTargets := []string{}
|
|
lowChargeTargets := []string{}
|
|
allSeen := true
|
|
for _, t := range targets {
|
|
key := t.Name + "|" + t.Target
|
|
st := states[key]
|
|
sample, err := t.Provider.Read(ctx)
|
|
if err != nil {
|
|
st.lastErr = err
|
|
if !st.seenGood {
|
|
allSeen = false
|
|
}
|
|
continue
|
|
}
|
|
st.seenGood = true
|
|
st.lastErr = nil
|
|
if sample.OnBattery {
|
|
onBatteryTargets = append(onBatteryTargets, fmt.Sprintf("%s(status=%s runtime_s=%d)", t.Name, sample.RawStatus, sample.RuntimeSeconds))
|
|
}
|
|
if minimumBatteryPercent > 0 && sample.BatteryCharge > 0 && sample.BatteryCharge < minimumBatteryPercent {
|
|
lowChargeTargets = append(
|
|
lowChargeTargets,
|
|
fmt.Sprintf(
|
|
"%s(charge=%.1f%%<%.1f%% status=%s)",
|
|
t.Name,
|
|
sample.BatteryCharge,
|
|
minimumBatteryPercent,
|
|
sample.RawStatus,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
if len(onBatteryTargets) > 0 {
|
|
return fmt.Errorf("startup blocked: UPS is on battery for %s", strings.Join(onBatteryTargets, ", "))
|
|
}
|
|
if len(lowChargeTargets) > 0 {
|
|
return fmt.Errorf("startup blocked: UPS battery charge below minimum for %s", strings.Join(lowChargeTargets, ", "))
|
|
}
|
|
if allSeen {
|
|
return nil
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
unverified := make([]string, 0, len(targets))
|
|
for _, t := range targets {
|
|
key := t.Name + "|" + t.Target
|
|
st := states[key]
|
|
if st.seenGood {
|
|
continue
|
|
}
|
|
if st.lastErr != nil {
|
|
unverified = append(unverified, fmt.Sprintf("%s(%s): %v", t.Name, t.Target, st.lastErr))
|
|
} else {
|
|
unverified = append(unverified, fmt.Sprintf("%s(%s): no telemetry sample yet", t.Name, t.Target))
|
|
}
|
|
}
|
|
roundedMin := math.Round(minimumBatteryPercent*10) / 10
|
|
return fmt.Errorf("startup blocked: unable to verify UPS telemetry before timeout (minimum_battery_percent=%.1f): %s", roundedMin, strings.Join(unverified, " | "))
|
|
case <-time.After(pollInterval):
|
|
}
|
|
}
|
|
}
|