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. 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"}, }, 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")) 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 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 }