The safety review of the first version found it safe for the pool's data and asked for these fixes before it goes on the home server: - zfs_snapshot: no recursive. Every answer gives the dataset's free space and the pool's capacity, and says that the snapshot holds every block deleted or overwritten until it is destroyed by hand. - A command that runs out of time is ended with its whole process group. An act that timed out is answered from the list, or as "outcome unknown", never as "NOT DONE". - The watch runs as the operator account. - The event follower reads ZFS's queue from its start, with a cursor per boot, so the pool's import and any downtime reach the journal once. It writes "dropped N events", kills zpool before waiting for it, and exits non-zero after five failed follows in a row. - Past error counts are a warning, and counts that rise are unhealthy. - The guard test catches .Run(, exec.Command(, exec.CommandContext( and process starts. - The fixtures and the parser follow OpenZFS 2.4.4's own printers: human scan sizes under -p, no iostat section lines, error scrubs, per-vdev rebuild resilvers, paused scrubs. The operator also asked the module to own the scrub. A timer and a service of the module's own run zpool scrub -w -- <pool> from the scrub-pool, scrub-cadence, scrub-day and scrub-time settings. In the same declaration, the distribution's weekly and monthly timers for that pool are held stopped and disabled, so exactly one schedule scrubs it. zfs_scrub shows the schedule and the next run, with any resilver's progress. zfs_scrub_start, zfs_scrub_pause and zfs_scrub_stop act with why. The health check warns of a late or failed scrub and of a running resilver, and is unhealthy for a resilver that finished with errors.
135 lines
4.3 KiB
Go
135 lines
4.3 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, "eid": true,
|
|
"resilver_type": 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 droppedLine.MatchString(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
|
|
}
|