Removes streamHear, HandPoll, HandWait and the stream mark; keeps toolEmit. Builds: design 42 zfs-owns-zfs §24, §25, lead answers A-43, A-44
346 lines
19 KiB
Go
346 lines
19 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, never its children. Runs zfs snapshot -- dataset@name through sudo -n, then " +
|
|
"answers what zfs list shows: the snapshot made, its creation time, any other snapshot that came or went " +
|
|
"meanwhile, the dataset's free space and the pool's capacity. From then on the snapshot holds every block " +
|
|
"of the dataset deleted or overwritten, until it is destroyed by hand. A command that ran out of time is " +
|
|
"answered from the list, or as outcome unknown. A name beginning mesh- is refused (the scheduled run's). Recorded as the event snapshot.taken.person; 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"},
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
if _, given := a["recursive"]; given {
|
|
return nil, fmt.Errorf("recursive does not exist in this version: zfs_snapshot snapshots one dataset, never its children")
|
|
}
|
|
ans, err := m.TakeSnapshot(ctx, text(a, "dataset"), text(a, "name"), text(a, "why"), stdio.CallerOf(a))
|
|
if err != nil {
|
|
if _, absentHere := err.(errAbsent); absentHere {
|
|
return NotApplicable{Applicable: false, Said: err.Error()}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ans, nil
|
|
},
|
|
},
|
|
handTool(m, "zfs_snapshots_run_now", ActSnapshots, "Take the scheduled ZFS snapshots now, as the scheduled run does (a person's act with why): writes the act into the state `schedule` and starts zfs-mesh-run.service through the service manager; the run takes it under the same lock and guards. Does not wait: answers started at once and says zfs_module_events reads its outcome. (a)"),
|
|
handTool(m, "zfs_snapshots_prune_now", ActPrune, "Remove the scheduled ZFS snapshots past their keep count now, as the scheduled run does, only those its own ledger holds (a person's act with why). Does not wait: answers started at once and says zfs_module_events reads its outcome. (a)"),
|
|
handTool(m, "zfs_config_apply", ActApply, "Converge the node to its config file now, as the apply does (a person's act with why): the same lock and guards as the apply. Does not wait: answers started at once and says zfs_module_events reads its outcome. (a)"),
|
|
{
|
|
Name: "zfs_snapshots_pause",
|
|
Description: "Pause the scheduled ZFS snapshots: writes `paused` {by, why, at} in the state `schedule`; the run then takes and removes nothing until zfs_snapshots_resume. (a)",
|
|
Input: map[string]any{"why": map[string]any{"type": "string", "description": "why, in one line; kept with the act"}},
|
|
Run: func(a map[string]any) (any, error) {
|
|
if err := m.Pause(stdio.CallerOf(a), text(a, "why")); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"paused": true}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_snapshots_resume",
|
|
Description: "Resume the scheduled ZFS snapshots: deletes `paused` from the state `schedule`. (a)",
|
|
Run: func(map[string]any) (any, error) {
|
|
if err := m.Resume(); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"paused": false}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "zfs_snapshots_record",
|
|
Description: "Record the scheduled run's `taken` or `removed` ZFS snapshot in the ledger `snapshots`: the run is a process, " +
|
|
"which cannot write a ledger, so it calls this tool. Refused for a snapshot not named mesh-<kind>-<UTC stamp>: the ledger " +
|
|
"holds only the run's. (a)",
|
|
Input: map[string]any{
|
|
"operation": map[string]any{"type": "string", "description": "taken or removed"},
|
|
"dataset": map[string]any{"type": "string", "description": "the dataset"},
|
|
"name": map[string]any{"type": "string", "description": "the snapshot's name after the @"},
|
|
"kind": map[string]any{"type": "string", "description": "hourly, daily, weekly or monthly"},
|
|
"at": map[string]any{"type": "string", "description": "when it was taken, RFC 3339 in UTC"},
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
e := LedgerEntry{Dataset: text(a, "dataset"), Name: text(a, "name"), Kind: text(a, "kind"), At: text(a, "at")}
|
|
if err := m.Record(text(a, "operation"), e, stdio.CallIDOf(a)); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"recorded": true}, nil
|
|
},
|
|
},
|
|
readTool("zfs_config", "Every value the module owns with its config value, what runs on the node, and how it stands: not managed, as set, "+
|
|
"not applied yet, waiting (on what, since when), refused (why), failed, or taking effect at the next load of the kernel module; the "+
|
|
"package's files the module never writes (with a .pacnew beside them) and what the module says before it leaves a node: the next load of "+
|
|
"the kernel module uses ZFS's defaults, the three units stay, /etc/hostid stays. Changes nothing. (r)", nil,
|
|
func(map[string]any) (any, error) { return m.ConfigNow(ctx) }),
|
|
readTool("zfs_snapshots_schedule", "The schedule of the scheduled ZFS snapshots: the keep counts, when each kind next falls due, whether it is paused, "+
|
|
"the entries past their keep count the next run would remove, and the ledger `{\"ledger\": [...]}` of every scheduled ZFS snapshot the run took "+
|
|
"and did not remove. The scheduled run reads the ledger from here. Changes nothing. (r)", nil,
|
|
func(map[string]any) (any, error) { return m.SnapshotSchedule(ctx) }),
|
|
readTool("zfs_module_events", "The module's own events, newest first, from its events stream (config.applied, config.refused, config.waiting, "+
|
|
"snapshot.taken.person and .schedule, snapshot.removed.person and .schedule, snapshot.skipped.person and .schedule, run.skipped, scrub.requested), "+
|
|
"the last N of one kind or of all, with how many of each kind. Not ZFS's own events: zfs_events reads those. Changes nothing. (r)",
|
|
map[string]any{"last": map[string]any{"type": "integer", "description": "how many to show (default 20, 1 to 500)"},
|
|
"kind": map[string]any{"type": "string", "description": "one kind of event (optional: every kind when left out)"}},
|
|
func(a map[string]any) (any, error) {
|
|
n, err := whole(a, "last", 20, 1, 500)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.ModuleEvents(n, text(a, "kind"))
|
|
}),
|
|
readTool("zfs_snapshots_check", "Compares the ledger with what ZFS holds: names each ledger entry whose ZFS snapshot is gone and each ZFS snapshot "+
|
|
"named mesh- that the ledger does not hold. Changes nothing. (r)", nil,
|
|
func(map[string]any) (any, error) { return m.CheckSnapshots(ctx) }),
|
|
readTool("zfs_findings", "The event follower's findings: one key for each pool or device that is not ONLINE, deleted when it is ONLINE again or "+
|
|
"leaves the pool. Changes nothing. (r)", nil,
|
|
func(map[string]any) (any, error) { return m.FindingsNow() }),
|
|
{
|
|
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 scans as status gives them: its scrub (or error scrub) and its resilver, the pool-wide one or " +
|
|
"each vdev's rebuild; when each 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 what the schedule allows. Then the module's scrub schedule: the pool, the " +
|
|
"cadence, the day and time, its timer's state and the next run, and how the last run ended. 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. 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)
|
|
},
|
|
},
|
|
scrubAct("start", "Start a scrub of one pool, or resume a paused one: zpool scrub -- pool through sudo -n, a person's "+
|
|
"act with why. A scrub reads every block and rewrites from redundancy only a block it finds damaged; it adds "+
|
|
"I/O load for its whole length (hours to days on a large pool) and never changes what a file holds. "+
|
|
"Answers what status says afterwards; recorded as the event scrub.requested. Replaces zpool scrub. (a)", m),
|
|
scrubAct("pause", "Pause one pool's running scrub, keeping its place: zpool scrub -p -- pool through sudo -n, with why; "+
|
|
"zfs_scrub_start resumes it. Answers what status says afterwards; recorded as the event scrub.requested. "+
|
|
"Replaces zpool scrub -p. (a)", m),
|
|
scrubAct("stop", "Stop one pool's scrub, losing its place: zpool scrub -s -- pool through sudo -n, with why. Answers "+
|
|
"what status says afterwards; recorded as the event scrub.requested. Replaces zpool scrub -s. (a)", m),
|
|
{
|
|
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 " +
|
|
"for a fault now: a pool not ONLINE (with status's reason and action), a device not online, a device's " +
|
|
"error counts that rose since the last look (unhealthy for 15 minutes after the rise), data errors, a " +
|
|
"resilver that finished with errors, a pool more than 90 % full, no pool imported, or ZFS not on this " +
|
|
"machine. Warnings leave it healthy: error counts from before, to clear by hand once understood; a pool " +
|
|
"more than 80 % full; a resilver running; a last scrub later than its schedule allows (or 35 days without " +
|
|
"one); a scheduled scrub that failed, or a schedule whose timer does not run. With why, one sentence per " +
|
|
"finding. (r)",
|
|
Run: func(map[string]any) (any, error) { return m.Health(ctx), nil },
|
|
},
|
|
}
|
|
}
|
|
|
|
// scrubAct is one of the scrub's acts as a tool.
|
|
func scrubAct(act, description string, m *Machine) stdio.Tool {
|
|
return stdio.Tool{
|
|
Name: "zfs_scrub_" + act,
|
|
Description: description,
|
|
Input: map[string]any{
|
|
"pool": map[string]any{"type": "string", "description": "the pool, as zfs_pools names it"},
|
|
"why": map[string]any{"type": "string", "description": "why, in one line; kept with the act"},
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
ans, err := m.ScrubAct(context.Background(), act, text(a, "pool"), text(a, "why"))
|
|
if err != nil {
|
|
if _, absentHere := err.(errAbsent); absentHere {
|
|
return NotApplicable{Applicable: false, Said: err.Error()}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ans, 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
|
|
}
|
|
|
|
// handTool is one hand act as a tool.
|
|
func handTool(m *Machine, name, act, description string) stdio.Tool {
|
|
return stdio.Tool{
|
|
Name: name,
|
|
Description: description,
|
|
Input: map[string]any{"why": map[string]any{"type": "string", "description": "why, in one line; kept with the act"}},
|
|
Run: func(a map[string]any) (any, error) {
|
|
return m.HandAct(context.Background(), act, stdio.CallerOf(a), text(a, "why"))
|
|
},
|
|
}
|
|
}
|
|
|
|
// readTool is one of the module's own read tools.
|
|
func readTool(name, description string, input map[string]any, run func(map[string]any) (any, error)) stdio.Tool {
|
|
return stdio.Tool{Name: name, Description: description, Input: input, Run: run}
|
|
}
|