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) }