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.
224 lines
10 KiB
Go
224 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Tools are the module's tools over one machine. Every one but zfs_snapshot only reads; zfs_snapshot only
|
|
// adds a snapshot. Nothing here destroys, rolls back, sets a property or sends: those change the
|
|
// operator's data and need a decision of their own.
|
|
func Tools(m *Machine) []stdio.Tool {
|
|
ctx := context.Background()
|
|
dataset := func(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "zfs_datasets",
|
|
Description: "Every ZFS filesystem and volume on this machine: name, mountpoint, used, available and referenced " +
|
|
"(bytes), compression ratio, sharenfs, how many snapshots it has and the space they hold together " +
|
|
"(usedbysnapshots: what destroying all of them would free), each in a line under `said`. A machine " +
|
|
"without ZFS answers applicable false, with why. Replaces zfs list. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.DatasetsNow(ctx) },
|
|
},
|
|
{
|
|
Name: "zfs_snapshots",
|
|
Description: "The snapshots of one dataset (exactly that one, not its children), or of every dataset when none is " +
|
|
"named, oldest first: name, creation time, used (the space only that snapshot holds: what destroying it " +
|
|
"alone would free) and referenced. Replaces zfs list -t snapshot. (r)",
|
|
Input: map[string]any{"dataset": dataset("the dataset, as zfs_datasets names it (optional: every snapshot when left out)")},
|
|
Run: func(a map[string]any) (any, error) { return m.SnapshotsOf(ctx, text(a, "dataset")) },
|
|
},
|
|
{
|
|
Name: "zfs_snapshot",
|
|
Description: "Take a snapshot dataset@name of one dataset, a person's act with why: refused for a dataset that is not " +
|
|
"on this machine, a name with anything but letters, digits, '.', '_' and '-', a snapshot that already exists, " +
|
|
"or no why. Only that dataset unless recursive is true, which snapshots every child at the same instant too. " +
|
|
"Runs zfs snapshot -- dataset@name through sudo -n, then answers what zfs list shows: the snapshot made " +
|
|
"and its creation time, and any other snapshot that came or went meanwhile. Recorded as the event " +
|
|
"snapshot.taken; ZFS keeps it in the pool's history too. It adds a snapshot and changes nothing else. " +
|
|
"Replaces zfs snapshot. (a)",
|
|
Input: map[string]any{
|
|
"dataset": dataset("the dataset, as zfs_datasets names it"),
|
|
"name": map[string]any{"type": "string", "description": "the snapshot's name, after the @: letters, digits, '.', '_' and '-'"},
|
|
"why": map[string]any{"type": "string", "description": "why, in one line; kept with the act"},
|
|
"recursive": map[string]any{"type": "boolean", "description": "true to snapshot every child dataset too (default false)"},
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
recursive, err := flag(a, "recursive")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ans, err := m.TakeSnapshot(ctx, text(a, "dataset"), text(a, "name"), text(a, "why"), recursive)
|
|
if err != nil {
|
|
if _, absentHere := err.(errAbsent); absentHere {
|
|
return NotApplicable{Applicable: false, Said: err.Error()}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ans, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_pools",
|
|
Description: "Every imported pool's health in a line: its state (ONLINE, DEGRADED, FAULTED, …) with status's own " +
|
|
"explanation and action, size, allocated and free (bytes), capacity and fragmentation %, the error count of " +
|
|
"its devices, its data errors and its last scan. Replaces zpool list and zpool status -x. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.PoolsNow(ctx) },
|
|
},
|
|
{
|
|
Name: "zfs_space",
|
|
Description: "Where the space goes: each pool's size, allocated, free, capacity % and fragmentation %; then every " +
|
|
"dataset, biggest first, with what uses it (usedbydataset, usedbysnapshots, usedbychildren, " +
|
|
"usedbyrefreservation), its quota, refquota, reservation and refreservation where set, the compression and " +
|
|
"its ratio, the logical size against the physical and what compression saves. Replaces zfs list -o space " +
|
|
"and zpool list -v. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.Space(ctx) },
|
|
},
|
|
{
|
|
Name: "zfs_pool_layout",
|
|
Description: "Each pool's layout as status gives it: its vdevs (mirror, raidz, draid) and their disks by the name the " +
|
|
"pool knows them by (by-id where it was imported so), each with its state and its read, write and checksum " +
|
|
"error counts and status's note (resilvering, was /dev/…); then the log, cache, special and dedup devices " +
|
|
"and the spares. Replaces zpool status and zpool status -v. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.Layout(ctx) },
|
|
},
|
|
{
|
|
Name: "zfs_scrub",
|
|
Description: "Each pool's last scan, as status says it (ZFS keeps only the latest, a scrub's or a resilver's): when it " +
|
|
"started and ended, how long it took, what it repaired and the errors it found, or, while one runs, its " +
|
|
"progress and the time to go; and how many days since the last finished scrub, with a WARNING past 35. " +
|
|
"With history true, also each pool's history (zpool history -i, as root through sudo -n) for the last " +
|
|
"finished scrub and resilver status no longer shows; it can take some seconds on a pool with a long life. " +
|
|
"Replaces zpool history -i. (r)",
|
|
Input: map[string]any{"history": map[string]any{"type": "boolean", "description": "true to read each pool's history too (default false)"}},
|
|
Run: func(a map[string]any) (any, error) {
|
|
h, err := flag(a, "history")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Scrubs(ctx, h)
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_io",
|
|
Description: "A sample of every pool's I/O over a few seconds (zpool iostat -v, the report since boot left out): " +
|
|
"read and write operations per second and bytes per second per pool, vdev and device, with each " +
|
|
"vdev's allocated and free. Replaces zpool iostat and zpool iostat -v. (r)",
|
|
Input: map[string]any{"seconds": map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 3, 1 to 10)"}},
|
|
Run: func(a map[string]any) (any, error) {
|
|
n, err := whole(a, "seconds", 3, 1, 10)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.IO(ctx, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_arc",
|
|
Description: "The ARC, ZFS's cache in memory, since the machine started, from /proc/spl/kstat/zfs/arcstats: its " +
|
|
"size, target, minimum and maximum (bytes), hits, misses and hit ratio %, the ratio for what was asked " +
|
|
"for alone (not read ahead), and the L2ARC on a cache device where there is one. Replaces arcstat, " +
|
|
"arc_summary and cat /proc/spl/kstat/zfs/arcstats. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.Arc() },
|
|
},
|
|
{
|
|
Name: "zfs_events",
|
|
Description: "ZFS's recent events, newest first, as root through sudo -n: by default only errors (I/O, checksum and " +
|
|
"device errors) and changes of state (a device's state, a scrub or resilver started or finished, a pool " +
|
|
"imported), each with its time, class, pool, device and state; all true gives every event. ZFS keeps " +
|
|
"a bounded number since its module loaded. Replaces zpool events and zpool events -v. (r)",
|
|
Input: map[string]any{
|
|
"all": map[string]any{"type": "boolean", "description": "true for every event, not only errors and changes of state (default false)"},
|
|
"limit": map[string]any{"type": "integer", "description": "how many to show (default 50, at most 500)"},
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
all, err := flag(a, "all")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(a, "limit", 50, 1, 500)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Events(ctx, all, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_properties",
|
|
Description: "Every property of one dataset with where its value comes from (local, default, inherited from …): " +
|
|
"compression, recordsize, atime, sync, xattr, sharenfs, sharesmb, mountpoint, readonly, encryption, " +
|
|
"keystatus, keyformat and encryptionroot and the rest, the usual ones said first in lines. Never a key: " +
|
|
"keylocation's value is withheld. Replaces zfs get all. (r)",
|
|
Input: map[string]any{"dataset": dataset("the dataset, as zfs_datasets names it")},
|
|
Run: func(a map[string]any) (any, error) { return m.PropertiesOf(ctx, text(a, "dataset")) },
|
|
},
|
|
{
|
|
Name: "zfs_health",
|
|
Description: "Whether this machine's ZFS is healthy, as the module's health check (novox/hq ADR 0240): unhealthy " +
|
|
"when a pool is not ONLINE (with status's reason and action), a device has read, write or checksum " +
|
|
"errors or is not online, a pool reports data errors, a pool is more than 90 % full, no pool is " +
|
|
"imported, or ZFS is not on this machine; a pool more than 80 % full and a last scrub more than 35 " +
|
|
"days ago are warnings, which leave it healthy. With why, one sentence per finding. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.Health(ctx), nil },
|
|
},
|
|
}
|
|
}
|
|
|
|
func text(a map[string]any, key string) string {
|
|
s, _ := a[key].(string)
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// flag is a yes-or-no argument: true only when given as true; anything that is not a yes or a no is
|
|
// refused rather than read as no.
|
|
func flag(a map[string]any, key string) (bool, error) {
|
|
switch v := a[key].(type) {
|
|
case nil:
|
|
return false, nil
|
|
case bool:
|
|
return v, nil
|
|
case string:
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "true":
|
|
return true, nil
|
|
case "false", "":
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("%s is true or false, not %q", key, v)
|
|
}
|
|
return false, fmt.Errorf("%s is true or false", key)
|
|
}
|
|
|
|
// whole is a whole-number argument, defaulted, refused outside least..most.
|
|
func whole(a map[string]any, key string, fallback, least, most int) (int, error) {
|
|
v, given := a[key]
|
|
if !given || v == nil {
|
|
return fallback, nil
|
|
}
|
|
var n int
|
|
switch x := v.(type) {
|
|
case float64:
|
|
if x != math.Trunc(x) {
|
|
return 0, fmt.Errorf("%s must be a whole number, not %v", key, x)
|
|
}
|
|
n = int(x)
|
|
case string:
|
|
i, err := strconv.Atoi(strings.TrimSpace(x))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be a whole number, not %q", key, x)
|
|
}
|
|
n = i
|
|
default:
|
|
return 0, fmt.Errorf("%s must be a whole number", key)
|
|
}
|
|
if n < least || n > most {
|
|
return 0, fmt.Errorf("%s must be from %d to %d, not %d", key, least, most, n)
|
|
}
|
|
return n, nil
|
|
}
|