Files
mesh-catalog/modules/mounts/cmd/mounts/health.go
T
jochen bbb3161ade
mesh/merge-gate fail: builds mounts → shanks; no bus step; a manifest the change touches fails the module check: modules/mounts/module.json: this manifest …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 24
mesh/delivery superseded: a newer head of the same pull request
mounts: take an SMB source's username as a setting and its password as its own secret, and wait for them
The one INI secret smb-credentials could not be given through the desk
prompt, which takes one hidden line, and a missing login was said as a
fault: the first-node gate put the build back and the operator was told
nothing was theirs to do. The username is now the setting smb-users and
each password the one-line secret smb-password-<source>; a source missing
either answers waits through mesh-sdk's health answer (hq ADR 0283,
issue 386, issue 379).
2026-10-10 21:11:16 +02:00

109 lines
4.2 KiB
Go

package main
// The module's health (novox/hq ADR 0240, ADR 0263 rule 8), judged from what the process last found.
//
// Unhealthy when a share that should always be there has been unreachable or failed for five minutes,
// when an automount is not armed, when something the settings name cannot be set up, or when the
// process has not looked for two minutes. An occasional source that is absent is healthy: it is said as
// not present since when, and raises nothing. A source waiting for the operator's username or password is
// not a fault: when nothing else is found the answer is *waits*, naming each (novox/hq ADR 0283).
import (
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/health"
)
// Health is the answer the node-engine reads from a tool check: mesh-sdk's answer (state, healthy, why,
// waits), with what was found and what was said beside it.
type Health struct {
health.Answer
Found []string `json:"found,omitempty"`
Said []string `json:"said,omitempty"`
}
// Grace is how long a share may be away before it is a fault; Stale how old the process's look may be.
const (
Grace = 5 * time.Minute
Stale = 2 * time.Minute
)
func clock(ts string) string {
t, err := time.Parse(time.RFC3339, ts)
if err != nil {
return ts
}
return t.Local().Format("15:04")
}
// Judge is the health of one status at a moment.
func Judge(st Status, readErr error, now time.Time) Health {
if readErr != nil {
return Health{Answer: health.Unhealthy("The module's process has left no account of this machine's mounts (" + readErr.Error() + ")."),
Found: []string{"no status"}}
}
var found, said, waiting []string
var waits []health.Wait
if looked, err := time.Parse(time.RFC3339, st.Looked); err != nil || now.Sub(looked) > Stale {
found = append(found, fmt.Sprintf("The module's process last looked at %s, more than two minutes ago.", clock(st.Looked)))
}
for _, p := range st.Problems {
found = append(found, "The module's settings or secret: "+p+".")
}
from := ""
if st.Server != "" {
from = " of " + st.Server
}
for _, s := range st.Shares {
since, _ := time.Parse(time.RFC3339, s.Since)
long := now.Sub(since) >= Grace
switch s.State {
case StateMounted, StateArmed:
said = append(said, fmt.Sprintf("%s at %s: %s since %s.", s.Name, s.Mountpoint, s.State, clock(s.Since)))
case StateWaiting:
waiting = append(waiting, fmt.Sprintf("The source %s at %s is not mounted since %s: %s.", s.Name, s.Mountpoint, clock(s.Since),
strings.TrimSuffix(s.Why, ".")))
waits = append(waits, s.Waits...)
case StateAbsent:
said = append(said, fmt.Sprintf("%s at %s: not present since %s.", s.Name, s.Mountpoint, clock(s.Since)))
case StateUnreachable:
line := fmt.Sprintf("The share %s%s is not reachable from this machine since %s (%s).", s.Name, from, clock(s.Since), s.Why)
if s.Kind != "nfs" {
line = fmt.Sprintf("The source %s is not reachable from this machine since %s (%s).", s.Name, clock(s.Since), s.Why)
}
if long {
found = append(found, line)
} else {
said = append(said, line+" Raised after five minutes.")
}
default:
line := fmt.Sprintf("The %s %s at %s cannot be mounted since %s: %s.", map[bool]string{true: "source", false: "share"}[s.Occasional],
s.Name, s.Mountpoint, clock(s.Since), strings.TrimSuffix(s.Why, "."))
if long || strings.Contains(s.Why, "not armed") || strings.Contains(s.Why, "not the module's") {
found = append(found, line)
} else {
said = append(said, line+" Raised after five minutes.")
}
}
}
if len(found) > 0 {
return Health{Answer: health.Unhealthy(strings.Join(append(found, waiting...), " "), waits...), Found: found, Said: append(said, waiting...)}
}
if len(waits) > 0 {
return Health{Answer: health.Waiting(strings.Join(waiting, " "), waits...), Said: append(said, waiting...)}
}
why := "Every share this machine is asked to mount is armed or mounted, and every occasional source is armed, mounted or not present."
if len(st.Shares) == 0 {
why = "This machine is asked to mount nothing."
}
return Health{Answer: health.Healthy(why), Said: said}
}
// Health reads the process's last look and judges it.
func (m *Machine) Health(now time.Time) Health {
st, err := readJSON[Status](m, StatusFile)
return Judge(st, err, now)
}