package main import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "strings" "time" ) // The machine as the module sees it. // // **It runs fixed commands, never a shell.** Every command is a program and an argument list built here; // nothing an argument says is ever read by a shell. The tools run as the operator account (novox/hq ADR // 0175 §4). What ZFS lets any account read — `zfs list`, `zfs get`, `zpool list`, `zpool status`, // `zpool iostat` — runs as that account. What ZFS keeps for root — `zpool events`, `zpool history` and // `zfs snapshot` — goes through `sudo -n`, as the packet filter's, the service manager's and nfs-server's // tools do: without a prompt, so a refusal is said rather than waited on. // CommandTimeout bounds every command but a sample. const CommandTimeout = 30 * time.Second // Ran is what one command did. type Ran struct { Out string Err string Code int } // Runner runs one command; injected, so every tool is tested over recorded answers. type Runner func(ctx context.Context, name string, args ...string) Ran // ExecRunner runs a command on the machine, bounded by the context (CommandTimeout unless the caller set // one). func ExecRunner(ctx context.Context, name string, args ...string) Ran { if _, has := ctx.Deadline(); !has { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, CommandTimeout) defer cancel() } cmd := exec.CommandContext(ctx, name, args...) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr err := cmd.Run() r := Ran{Out: stdout.String(), Err: strings.TrimSpace(stderr.String())} if err != nil { var exit *exec.ExitError if errors.As(err, &exit) { r.Code = exit.ExitCode() } else { r.Code = -1 if r.Err == "" { r.Err = err.Error() } } if ctx.Err() != nil { r.Code = -1 r.Err = fmt.Sprintf("did not answer within its time (%v)", ctx.Err()) } } return r } // Machine is what the module reads and the one thing it does. type Machine struct { Run Runner // UID is this process's account: root runs a command as it is, any other account through sudo -n. UID int Now func() time.Time // ReadFile reads a kernel file (the ARC's counters). ReadFile func(path string) ([]byte, error) // Emit publishes an event as this module (the record of an act); nil where nothing can be published. Emit func(eventType string, body any) error } // Here is the machine this process runs on. func Here() *Machine { return &Machine{Run: ExecRunner, UID: os.Getuid(), Now: time.Now, ReadFile: os.ReadFile} } // NotApplicable is the answer of a machine without ZFS: the module does not apply here. type NotApplicable struct { Applicable bool `json:"applicable"` Said string `json:"said"` } // absent says why ZFS cannot be asked on this machine, from a command's failure; "" when the failure is // something else. func absent(r Ran) string { switch { case r.Code == -1 && (strings.Contains(r.Err, "executable file not found") || strings.Contains(r.Err, "no such file or directory")): return "ZFS is not installed on this machine (there is no zfs or zpool command), so this module does not apply here" case strings.Contains(r.Err, "/dev/zfs") || strings.Contains(r.Err, "ZFS modules cannot be auto-loaded") || strings.Contains(r.Err, "modprobe zfs"): return "ZFS's kernel module is not loaded on this machine (" + firstLine(r.Err) + "), so there is no pool to read" } return "" } // errAbsent is the error a tool answers when ZFS is not here. type errAbsent struct{ why string } func (e errAbsent) Error() string { return e.why } // ask runs a read-only command as this account and answers its output, or why it could not. func (m *Machine) ask(ctx context.Context, name string, args ...string) (string, error) { r := m.Run(ctx, name, args...) if r.Code != 0 { if why := absent(r); why != "" { return "", errAbsent{why} } return "", fmt.Errorf("%s %s: exit %d: %s", name, strings.Join(args, " "), r.Code, firstLine(orOut(r))) } return r.Out, nil } // Escalated is a command as it is run: as given when this process is root, else through sudo without a // prompt. func (m *Machine) Escalated(name string, args ...string) (string, []string) { if m.UID == 0 { return name, args } return "sudo", append([]string{"-n", name}, args...) } // AsRoot runs what only root may do, and names a refusal by how it failed. func (m *Machine) AsRoot(ctx context.Context, name string, args ...string) (Ran, error) { program, argv := m.Escalated(name, args...) r := m.Run(ctx, program, argv...) if r.Code != 0 { why := orOut(r) if why2 := absent(r); why2 != "" && program != "sudo" { return r, errAbsent{why2} } if program == "sudo" && (strings.Contains(why, "a password is required") || strings.Contains(why, "not allowed") || strings.Contains(why, "may not run sudo")) { return r, fmt.Errorf("%s needs root, and sudo refused the operator account without a prompt: %s", name, firstLine(why)) } if program == "sudo" && strings.Contains(why, "command not found") { return r, errAbsent{"ZFS is not installed on this machine (sudo found no " + name + " command), so this module does not apply here"} } return r, fmt.Errorf("%s %s: exit %d: %s", name, strings.Join(args, " "), r.Code, firstLine(why)) } return r, nil } func orOut(r Ran) string { if strings.TrimSpace(r.Err) != "" { return r.Err } return strings.TrimSpace(r.Out) } func firstLine(s string) string { s = strings.TrimSpace(s) if i := strings.IndexByte(s, '\n'); i >= 0 { return s[:i] } return s } // bytesSaid says a size for a person: 1.2 TiB, 512 MiB, 900 B. func bytesSaid(n uint64) string { units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB"} f := float64(n) i := 0 for f >= 1024 && i < len(units)-1 { f /= 1024 i++ } if i == 0 { return fmt.Sprintf("%d B", n) } return fmt.Sprintf("%.1f %s", f, units[i]) } // round1 rounds to one decimal, for what a person reads. func round1(f float64) float64 { if f < 0 { return -float64(int64(-f*10+0.5)) / 10 } return float64(int64(f*10+0.5)) / 10 }