mesh/merge-gate pass: builds new: modules/zfs, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestASnapshotIsAnsweredFromTheListAfterwardsAndRecorded (0.00s)
mesh/delivery superseded: a newer head of the same pull request
The home server's pool holds the operator's media library, and no mesh tool could say whether it is healthy, how full it is, when it was last scrubbed or what snapshots exist; the only way was a login. This module reads all of it with fixed zfs and zpool commands (list, get, status, iostat, events, history) and has one act, zfs_snapshot, which only adds a snapshot: refused for an unknown dataset, a name outside [A-Za-z0-9._-], an existing snapshot or no why; run as sudo -n zfs snapshot -- ds@name; answered from zfs list afterwards, with any other snapshot that came or went meanwhile; recorded as the event snapshot.taken. No destroy, rollback, set or send exists, and a test holds every command to its verb. Health (ADR 0240): zfs_health on the watch process, unhealthy for a pool not ONLINE, device or data errors, a pool past 90 % full or no ZFS at all; past 80 % and a scrub older than 35 days are warnings. Beside it, the event follower (zpool events -f, as root) keeps ZFS's errors in its journal past a reboot and is judged by its unit, the check the module does not run itself. No seat (ADR 0121), no package: ZFS is the machine's.
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The module's second long-running process (`zfs-tools events`), run by the node-engine as root. ZFS keeps
|
|
// its events in memory only, so a reboot or a reload of its kernel module loses them; the follower writes
|
|
// each error and change of state to its journal as ZFS emits it, where it outlives both. It writes no file
|
|
// and changes nothing. It is judged by its unit (novox/hq ADR 0240), the check beside zfs_health that the
|
|
// module does not run itself.
|
|
|
|
// FollowArgv follows ZFS's events as they come, with their details.
|
|
func FollowArgv() []string { return []string{"events", "-f", "-v"} }
|
|
|
|
// FollowRetry is how long the follower waits before following again after zpool exits.
|
|
const FollowRetry = 30 * time.Second
|
|
|
|
// eventTime reads an event's time as `zpool events` gives it: "Oct 8 2026 21:01:55.889900112".
|
|
func eventTime(s string) (time.Time, bool) {
|
|
t, err := time.ParseInLocation("Jan 2 2006 15:04:05.999999999", strings.Join(strings.Fields(s), " "), time.Local)
|
|
return t, err == nil
|
|
}
|
|
|
|
// EventLine is an event in one journal line.
|
|
func EventLine(e Event) string {
|
|
line := e.Time + " " + e.Class
|
|
keys := make([]string, 0, len(e.Details))
|
|
for k := range e.Details {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
line += fmt.Sprintf(" %s=%s", k, e.Details[k])
|
|
}
|
|
return line
|
|
}
|
|
|
|
// Follow reads `zpool events -f -v` and writes each notable event at or after since in a line. `-f` first
|
|
// gives every event ZFS still keeps; those from before since were written by an earlier follower, or are
|
|
// in zfs_events, and are not written again.
|
|
func Follow(r io.Reader, out io.Writer, since time.Time) error {
|
|
sc := bufio.NewScanner(r)
|
|
sc.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
var block []string
|
|
flush := func() {
|
|
for _, e := range ParseEvents(strings.Join(block, "\n")) {
|
|
if !Notable(e.Class) {
|
|
continue
|
|
}
|
|
if t, ok := eventTime(e.Time); ok && t.Before(since) {
|
|
continue
|
|
}
|
|
fmt.Fprintln(out, EventLine(e))
|
|
}
|
|
block = block[:0]
|
|
}
|
|
for sc.Scan() {
|
|
l := sc.Text()
|
|
if strings.TrimSpace(l) == "" {
|
|
flush()
|
|
continue
|
|
}
|
|
block = append(block, l)
|
|
}
|
|
flush()
|
|
return sc.Err()
|
|
}
|
|
|
|
// Follower runs zpool events -f until the context ends, again after FollowRetry whenever it exits.
|
|
func Follower(ctx context.Context, out io.Writer) {
|
|
since := time.Now()
|
|
for ctx.Err() == nil {
|
|
cmd := exec.CommandContext(ctx, "zpool", FollowArgv()...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err == nil {
|
|
err = cmd.Start()
|
|
}
|
|
if err == nil {
|
|
err = Follow(stdout, out, since)
|
|
if werr := cmd.Wait(); err == nil {
|
|
err = werr
|
|
}
|
|
}
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
fmt.Fprintf(out, "zpool events -f stopped (%v); following again in %s\n", err, FollowRetry)
|
|
// What comes while it waits is still in ZFS's own list, and the next follow writes it from here.
|
|
since = time.Now()
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(FollowRetry):
|
|
}
|
|
}
|
|
}
|