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.
94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// PropertiesArgv is every property of one dataset, the name validated against the list first.
|
|
func PropertiesArgv(dataset string) []string {
|
|
return []string{"get", "-Hp", "-o", "property,value,source", "all", "--", dataset}
|
|
}
|
|
|
|
// Withheld are properties whose value the module never answers: where an encryption key is kept can
|
|
// itself be a secret (a URL with a token). ZFS never gives a key itself.
|
|
var Withheld = map[string]bool{"keylocation": true}
|
|
|
|
// Property is one property, with where its value comes from.
|
|
type Property struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
// ParseProperties reads `zfs get -Hp -o property,value,source`.
|
|
func ParseProperties(out string) ([]Property, error) {
|
|
var ps []Property
|
|
for _, line := range lines(out) {
|
|
f := strings.Split(line, "\t")
|
|
if len(f) != 3 {
|
|
return nil, fmt.Errorf("zfs get answered a line with %d columns, not 3: %q", len(f), line)
|
|
}
|
|
p := Property{Name: f[0], Value: f[1], Source: f[2]}
|
|
if Withheld[p.Name] && p.Value != "none" && p.Value != "-" {
|
|
p.Value = "(withheld: where the key is kept is not answered)"
|
|
}
|
|
ps = append(ps, p)
|
|
}
|
|
return ps, nil
|
|
}
|
|
|
|
// Highlights are the properties zfs_properties says in lines first.
|
|
var Highlights = []string{"type", "mountpoint", "mounted", "compression", "compressratio", "recordsize", "volblocksize", "atime",
|
|
"relatime", "sync", "xattr", "acltype", "dedup", "copies", "checksum", "sharenfs", "sharesmb", "readonly", "canmount",
|
|
"encryption", "keystatus", "keyformat", "encryptionroot", "snapdir", "primarycache", "secondarycache", "logbias",
|
|
"special_small_blocks", "quota", "refquota", "reservation", "refreservation"}
|
|
|
|
// PropertiesAnswer is what zfs_properties answers.
|
|
type PropertiesAnswer struct {
|
|
Applicable bool `json:"applicable"`
|
|
Dataset string `json:"dataset"`
|
|
Properties []Property `json:"properties"`
|
|
Said []string `json:"said"`
|
|
}
|
|
|
|
// PropertiesOf is every property of one dataset that exists.
|
|
func (m *Machine) PropertiesOf(ctx context.Context, dataset string) (any, error) {
|
|
if dataset == "" {
|
|
return nil, fmt.Errorf("name the dataset; zfs_datasets lists them")
|
|
}
|
|
ds, err := m.Datasets(ctx)
|
|
if err != nil {
|
|
return notHere(err)
|
|
}
|
|
if find(ds, dataset) == nil {
|
|
return nil, fmt.Errorf("there is no dataset %q on this machine; zfs_datasets lists them", dataset)
|
|
}
|
|
out, err := m.ask(ctx, "zfs", PropertiesArgv(dataset)...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ps, err := ParseProperties(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a := PropertiesAnswer{Applicable: true, Dataset: dataset, Properties: ps, Said: []string{}}
|
|
by := map[string]Property{}
|
|
for _, p := range ps {
|
|
by[p.Name] = p
|
|
}
|
|
for _, name := range Highlights {
|
|
p, ok := by[name]
|
|
if !ok || p.Value == "-" {
|
|
continue
|
|
}
|
|
line := fmt.Sprintf("%s = %s", name, p.Value)
|
|
if p.Source != "-" && p.Source != "" {
|
|
line += " (" + p.Source + ")"
|
|
}
|
|
a.Said = append(a.Said, line)
|
|
}
|
|
return a, nil
|
|
}
|