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 }