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.
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// EventsArgv is ZFS's recent events with their details. ZFS keeps them in memory since the module loaded,
|
|
// a bounded number, and gives them to root only.
|
|
func EventsArgv() []string { return []string{"events", "-v"} }
|
|
|
|
// Event is one ZFS event.
|
|
type Event struct {
|
|
Time string `json:"time"`
|
|
Class string `json:"class"`
|
|
// Details are the event's fields that say what and where: pool, vdev, state, error.
|
|
Details map[string]string `json:"details,omitempty"`
|
|
}
|
|
|
|
// eventKeys are the details an event is answered with; the rest are internal identifiers and checksums.
|
|
var eventKeys = map[string]bool{
|
|
"pool": true, "pool_state": true, "vdev_path": true, "vdev_type": true, "vdev_state": true, "vdev_laststate": true,
|
|
"vdev_read_errors": true, "vdev_write_errors": true, "vdev_cksum_errors": true, "zio_err": true, "zio_objset": true,
|
|
"zio_object": true, "history_internal_name": true, "history_internal_str": true, "vdev_ashift": true,
|
|
}
|
|
|
|
// vdevStates are ZFS's vdev states as numbers, as events give them.
|
|
var vdevStates = map[string]string{
|
|
"0x0": "UNKNOWN", "0x1": "CLOSED", "0x2": "OFFLINE", "0x3": "REMOVED", "0x4": "CANT_OPEN", "0x5": "FAULTED",
|
|
"0x6": "DEGRADED", "0x7": "ONLINE",
|
|
}
|
|
|
|
// ParseEvents reads `zpool events -v`: a header line, then for each event a line of its time and class and
|
|
// its fields indented under it, `name = value`.
|
|
func ParseEvents(out string) []Event {
|
|
var evs []Event
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimRight(line, "\r")
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
if line[0] != ' ' && line[0] != '\t' {
|
|
f := strings.Fields(line)
|
|
if len(f) < 2 || (f[0] == "TIME" && f[len(f)-1] == "CLASS") {
|
|
continue
|
|
}
|
|
evs = append(evs, Event{Time: strings.Join(f[:len(f)-1], " "), Class: f[len(f)-1]})
|
|
continue
|
|
}
|
|
if len(evs) == 0 {
|
|
continue
|
|
}
|
|
k, v, ok := strings.Cut(strings.TrimSpace(line), " = ")
|
|
if !ok || !eventKeys[k] {
|
|
continue
|
|
}
|
|
v = strings.Trim(strings.TrimSpace(v), `"`)
|
|
if k == "vdev_state" || k == "vdev_laststate" {
|
|
if s, known := vdevStates[v]; known {
|
|
v = s
|
|
}
|
|
}
|
|
e := &evs[len(evs)-1]
|
|
if e.Details == nil {
|
|
e.Details = map[string]string{}
|
|
}
|
|
e.Details[k] = v
|
|
}
|
|
return evs
|
|
}
|
|
|
|
// Notable says whether an event is an error or a change of state: every ereport (an I/O, checksum or
|
|
// device error), every resource event (a device's state changed) and ZFS's own events but the history
|
|
// it writes for every command and the configuration it syncs.
|
|
func Notable(class string) bool {
|
|
switch {
|
|
case strings.HasPrefix(class, "ereport."), strings.HasPrefix(class, "resource."):
|
|
return true
|
|
case class == "sysevent.fs.zfs.history_event", class == "sysevent.fs.zfs.config_sync":
|
|
return false
|
|
case strings.HasPrefix(class, "sysevent.fs.zfs."):
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EventsAnswer is what zfs_events answers.
|
|
type EventsAnswer struct {
|
|
Applicable bool `json:"applicable"`
|
|
Events []Event `json:"events"`
|
|
Shown int `json:"shown"`
|
|
Kept int `json:"kept"`
|
|
Said []string `json:"said"`
|
|
}
|
|
|
|
// Events are ZFS's recent errors and changes of state, newest first; with all, every event.
|
|
func (m *Machine) Events(ctx context.Context, all bool, limit int) (any, error) {
|
|
r, err := m.AsRoot(ctx, "zpool", EventsArgv()...)
|
|
if err != nil {
|
|
return notHere(err)
|
|
}
|
|
evs := ParseEvents(r.Out)
|
|
a := EventsAnswer{Applicable: true, Events: []Event{}, Kept: len(evs), Said: []string{}}
|
|
for i := len(evs) - 1; i >= 0 && len(a.Events) < limit; i-- {
|
|
if all || Notable(evs[i].Class) {
|
|
a.Events = append(a.Events, evs[i])
|
|
}
|
|
}
|
|
a.Shown = len(a.Events)
|
|
for _, e := range a.Events {
|
|
line := e.Time + " " + strings.TrimPrefix(strings.TrimPrefix(e.Class, "sysevent.fs.zfs."), "ereport.fs.zfs.")
|
|
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])
|
|
}
|
|
a.Said = append(a.Said, line)
|
|
}
|
|
which := "errors and changes of state"
|
|
if all {
|
|
which = "events"
|
|
}
|
|
a.Said = append([]string{fmt.Sprintf("%d %s shown, newest first, of the %d events ZFS keeps since its module loaded", a.Shown, which, a.Kept)}, a.Said...)
|
|
return a, nil
|
|
}
|