The safety review of the first version found it safe for the pool's data and asked for these fixes before it goes on the home server: - zfs_snapshot: no recursive. Every answer gives the dataset's free space and the pool's capacity, and says that the snapshot holds every block deleted or overwritten until it is destroyed by hand. - A command that runs out of time is ended with its whole process group. An act that timed out is answered from the list, or as "outcome unknown", never as "NOT DONE". - The watch runs as the operator account. - The event follower reads ZFS's queue from its start, with a cursor per boot, so the pool's import and any downtime reach the journal once. It writes "dropped N events", kills zpool before waiting for it, and exits non-zero after five failed follows in a row. - Past error counts are a warning, and counts that rise are unhealthy. - The guard test catches .Run(, exec.Command(, exec.CommandContext( and process starts. - The fixtures and the parser follow OpenZFS 2.4.4's own printers: human scan sizes under -p, no iostat section lines, error scrubs, per-vdev rebuild resilvers, paused scrubs. The operator also asked the module to own the scrub. A timer and a service of the module's own run zpool scrub -w -- <pool> from the scrub-pool, scrub-cadence, scrub-day and scrub-time settings. In the same declaration, the distribution's weekly and monthly timers for that pool are held stopped and disabled, so exactly one schedule scrubs it. zfs_scrub shows the schedule and the next run, with any resilver's progress. zfs_scrub_start, zfs_scrub_pause and zfs_scrub_stop act with why. The health check warns of a late or failed scrub and of a running resilver, and is unhealthy for a resilver that finished with errors.
636 lines
19 KiB
Go
636 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// PoolColumns are the properties `zpool list` is asked for, in order.
|
|
var PoolColumns = []string{"name", "size", "allocated", "free", "capacity", "fragmentation", "health", "dedupratio"}
|
|
|
|
// PoolsArgv lists every imported pool.
|
|
func PoolsArgv() []string {
|
|
return []string{"list", "-Hp", "-o", strings.Join(PoolColumns, ",")}
|
|
}
|
|
|
|
// StatusArgv is every imported pool's status, numbers exact.
|
|
func StatusArgv() []string { return []string{"status", "-p"} }
|
|
|
|
// Pool is one pool as `zpool list` answers it.
|
|
type Pool struct {
|
|
Name string `json:"name"`
|
|
Size uint64 `json:"size"`
|
|
Allocated uint64 `json:"allocated"`
|
|
Free uint64 `json:"free"`
|
|
// Capacity is the share of the pool allocated, in %.
|
|
Capacity int `json:"capacity_pct"`
|
|
// Fragmentation is the free space's fragmentation in %, nil when the pool cannot say.
|
|
Fragmentation *int `json:"fragmentation_pct,omitempty"`
|
|
Health string `json:"health"`
|
|
DedupRatio float64 `json:"dedupratio"`
|
|
}
|
|
|
|
// ParsePools reads `zpool list -Hp -o <PoolColumns>`.
|
|
func ParsePools(out string) ([]Pool, error) {
|
|
var ps []Pool
|
|
for _, line := range lines(out) {
|
|
f := strings.Split(line, "\t")
|
|
if len(f) != len(PoolColumns) {
|
|
return nil, fmt.Errorf("zpool list answered a line with %d columns, not %d: %q", len(f), len(PoolColumns), line)
|
|
}
|
|
p := Pool{Name: f[0], Health: f[6]}
|
|
var err error
|
|
if p.Size, err = number(f[1]); err != nil {
|
|
return nil, fmt.Errorf("zpool list: size of %s: %v", p.Name, err)
|
|
}
|
|
if p.Allocated, err = number(f[2]); err != nil {
|
|
return nil, fmt.Errorf("zpool list: allocated of %s: %v", p.Name, err)
|
|
}
|
|
if p.Free, err = number(f[3]); err != nil {
|
|
return nil, fmt.Errorf("zpool list: free of %s: %v", p.Name, err)
|
|
}
|
|
if p.Capacity, err = strconv.Atoi(strings.TrimSuffix(f[4], "%")); err != nil {
|
|
return nil, fmt.Errorf("zpool list: capacity of %s is %q", p.Name, f[4])
|
|
}
|
|
if fr, e := strconv.Atoi(strings.TrimSuffix(f[5], "%")); e == nil {
|
|
p.Fragmentation = &fr
|
|
}
|
|
if p.DedupRatio, err = ratio(f[7]); err != nil {
|
|
return nil, fmt.Errorf("zpool list: dedupratio of %s is %q", p.Name, f[7])
|
|
}
|
|
ps = append(ps, p)
|
|
}
|
|
return ps, nil
|
|
}
|
|
|
|
// PoolRow is one pool's space as the tools answer it.
|
|
type PoolRow struct {
|
|
Name string `json:"name"`
|
|
Health string `json:"health"`
|
|
Size uint64 `json:"size_bytes"`
|
|
Allocated uint64 `json:"allocated_bytes"`
|
|
Free uint64 `json:"free_bytes"`
|
|
Capacity int `json:"capacity_pct"`
|
|
Fragmentation *int `json:"fragmentation_pct,omitempty"`
|
|
Said string `json:"said"`
|
|
}
|
|
|
|
// Row says a pool's space in a line.
|
|
func (p Pool) Row() PoolRow {
|
|
r := PoolRow{Name: p.Name, Health: p.Health, Size: p.Size, Allocated: p.Allocated, Free: p.Free, Capacity: p.Capacity,
|
|
Fragmentation: p.Fragmentation}
|
|
frag := "fragmentation not known"
|
|
if p.Fragmentation != nil {
|
|
frag = fmt.Sprintf("%d %% fragmented", *p.Fragmentation)
|
|
}
|
|
r.Said = fmt.Sprintf("pool %s (%s): %s of %s allocated (%d %%), %s free, %s", p.Name, p.Health, bytesSaid(p.Allocated),
|
|
bytesSaid(p.Size), p.Capacity, bytesSaid(p.Free), frag)
|
|
return r
|
|
}
|
|
|
|
// Pools are every imported pool.
|
|
func (m *Machine) Pools(ctx context.Context) ([]Pool, error) {
|
|
out, err := m.ask(ctx, "zpool", PoolsArgv()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ParsePools(out)
|
|
}
|
|
|
|
// Device is one line of a pool's configuration: the pool itself, a vdev (mirror, raidz), a disk, or a
|
|
// section (logs, cache, spares, special, dedup), with its children.
|
|
type Device struct {
|
|
Name string `json:"name"`
|
|
// Kind is pool, vdev (a group of disks), device (a disk or a partition), or section.
|
|
Kind string `json:"kind"`
|
|
// Role is data, log, cache, spare, special or dedup: the section a device is in.
|
|
Role string `json:"role,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Read uint64 `json:"read_errors"`
|
|
Write uint64 `json:"write_errors"`
|
|
Checksum uint64 `json:"checksum_errors"`
|
|
Note string `json:"note,omitempty"`
|
|
Children []*Device `json:"children,omitempty"`
|
|
}
|
|
|
|
// Errors is the error count status gives this line, read, write and checksum together: a vdev's line
|
|
// counts what was found at the vdev, not the sum of its disks.
|
|
func (d *Device) Errors() uint64 { return d.Read + d.Write + d.Checksum }
|
|
|
|
// Scan is one `scan:` or `scrub:` line of status with its continuation. Status gives a pool's scrub
|
|
// (or, when it was more recent, its error scrub), and then its resilver: the pool-wide one, or the
|
|
// rebuild of each top-level vdev that had one (a sequential resilver, named by its vdev). ZFS keeps one
|
|
// pool-wide scan, so a resilver after a scrub hides that scrub; the pool's history still has it.
|
|
type Scan struct {
|
|
// Kind is scrub, resilver or error scrub.
|
|
Kind string `json:"kind"`
|
|
// Vdev is the top-level vdev of a rebuild; "" for a pool-wide scan.
|
|
Vdev string `json:"vdev,omitempty"`
|
|
// State is finished, running, paused, canceled or unknown.
|
|
State string `json:"state"`
|
|
Started *time.Time `json:"started,omitempty"`
|
|
Paused *time.Time `json:"paused,omitempty"`
|
|
Ended *time.Time `json:"ended,omitempty"`
|
|
Took string `json:"took,omitempty"`
|
|
// Repaired is what a scrub repaired or a resilver rewrote, in status's own human size (status prints
|
|
// it so even under -p); for an error scrub, the error blocks it scrubbed.
|
|
Repaired string `json:"repaired,omitempty"`
|
|
Errors *int `json:"errors,omitempty"`
|
|
Done *float64 `json:"done_pct,omitempty"`
|
|
ToGo string `json:"to_go,omitempty"`
|
|
NoEstimate bool `json:"no_estimate,omitempty"`
|
|
Said string `json:"said"`
|
|
}
|
|
|
|
// PoolStatus is one pool as `zpool status` says it.
|
|
type PoolStatus struct {
|
|
Name string `json:"name"`
|
|
State string `json:"state"`
|
|
Status string `json:"status,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
See string `json:"see,omitempty"`
|
|
// Scans are the scan lines status gives, in its order; none when no scan was ever asked.
|
|
Scans []Scan `json:"scans"`
|
|
// Other are the lines status gives for a removal, a checkpoint or an expansion.
|
|
Other map[string]string `json:"other,omitempty"`
|
|
Root *Device `json:"layout,omitempty"`
|
|
Errors string `json:"errors"`
|
|
}
|
|
|
|
var sections = map[string]string{"logs": "log", "cache": "cache", "spares": "spare", "special": "special", "dedup": "dedup"}
|
|
|
|
// ParseStatus reads `zpool status -p` for every pool it shows.
|
|
func ParseStatus(out string) ([]PoolStatus, error) {
|
|
var pools []PoolStatus
|
|
var cur *PoolStatus
|
|
key := ""
|
|
inConfig := false
|
|
var stack []*Device
|
|
section := ""
|
|
var scanKeys, scanSaid []string
|
|
flush := func() {
|
|
if cur != nil {
|
|
cur.Scans = []Scan{}
|
|
for i := range scanSaid {
|
|
cur.Scans = append(cur.Scans, parseScan(scanKeys[i], scanSaid[i]))
|
|
}
|
|
pools = append(pools, *cur)
|
|
}
|
|
scanKeys, scanSaid = nil, nil
|
|
}
|
|
for _, raw := range strings.Split(out, "\n") {
|
|
line := strings.TrimRight(raw, "\r ")
|
|
trimmed := strings.TrimSpace(line)
|
|
if k, v, ok := statusKey(line); ok {
|
|
inConfig = false
|
|
key = k
|
|
switch k {
|
|
case "pool":
|
|
flush()
|
|
cur = &PoolStatus{Name: v}
|
|
stack, section = nil, ""
|
|
case "config":
|
|
inConfig = true
|
|
}
|
|
if cur == nil {
|
|
continue
|
|
}
|
|
switch k {
|
|
case "state":
|
|
cur.State = v
|
|
case "status":
|
|
cur.Status = v
|
|
case "action":
|
|
cur.Action = v
|
|
case "see":
|
|
cur.See = v
|
|
case "scan", "scrub":
|
|
scanKeys, scanSaid = append(scanKeys, k), append(scanSaid, v)
|
|
case "errors":
|
|
cur.Errors = v
|
|
case "pool", "config":
|
|
default:
|
|
if cur.Other == nil {
|
|
cur.Other = map[string]string{}
|
|
}
|
|
cur.Other[k] = v
|
|
}
|
|
continue
|
|
}
|
|
if cur == nil || trimmed == "" {
|
|
continue
|
|
}
|
|
if !inConfig {
|
|
// A continuation of the key before it, indented by a tab.
|
|
add := func(s *string) { *s = strings.TrimSpace(*s + " " + trimmed) }
|
|
switch key {
|
|
case "status":
|
|
add(&cur.Status)
|
|
case "action":
|
|
add(&cur.Action)
|
|
case "scan", "scrub":
|
|
scanSaid[len(scanSaid)-1] += "\n" + trimmed
|
|
case "errors":
|
|
add(&cur.Errors)
|
|
default:
|
|
if cur.Other != nil {
|
|
if v, ok := cur.Other[key]; ok {
|
|
cur.Other[key] = strings.TrimSpace(v + " " + trimmed)
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
// The configuration: a tab, two spaces per level, then the columns.
|
|
body := strings.TrimPrefix(line, "\t")
|
|
depth := (len(body) - len(strings.TrimLeft(body, " "))) / 2
|
|
f := strings.Fields(body)
|
|
if f[0] == "NAME" && len(f) > 1 && f[1] == "STATE" {
|
|
continue
|
|
}
|
|
d := &Device{Name: f[0]}
|
|
if depth == 0 {
|
|
if role, ok := sections[f[0]]; ok && len(f) == 1 {
|
|
d.Kind, d.Role, section = "section", role, role
|
|
if cur.Root != nil {
|
|
cur.Root.Children = append(cur.Root.Children, d)
|
|
}
|
|
// A section stands at the pool's level and holds what is indented under it.
|
|
stack = []*Device{d}
|
|
continue
|
|
}
|
|
}
|
|
if len(f) > 1 {
|
|
d.State = f[1]
|
|
}
|
|
rest := f[min(len(f), 2):]
|
|
if len(rest) >= 3 && isCount(rest[0]) && isCount(rest[1]) && isCount(rest[2]) {
|
|
d.Read, d.Write, d.Checksum = count(rest[0]), count(rest[1]), count(rest[2])
|
|
rest = rest[3:]
|
|
}
|
|
d.Note = strings.Join(rest, " ")
|
|
if depth == 0 {
|
|
d.Kind, d.Role = "pool", ""
|
|
cur.Root = d
|
|
stack, section = []*Device{d}, ""
|
|
continue
|
|
}
|
|
role := section
|
|
if role == "" {
|
|
role = "data"
|
|
}
|
|
d.Role = role
|
|
if depth < len(stack) {
|
|
stack = stack[:depth]
|
|
}
|
|
if len(stack) == 0 {
|
|
return nil, fmt.Errorf("zpool status: the device %s of pool %s has no parent", d.Name, cur.Name)
|
|
}
|
|
parent := stack[len(stack)-1]
|
|
parent.Children = append(parent.Children, d)
|
|
stack = append(stack, d)
|
|
}
|
|
flush()
|
|
for i := range pools {
|
|
kinds(pools[i].Root)
|
|
}
|
|
return pools, nil
|
|
}
|
|
|
|
// kinds names each device by its place: a device with children is a vdev, one without a device.
|
|
func kinds(d *Device) {
|
|
if d == nil {
|
|
return
|
|
}
|
|
for _, c := range d.Children {
|
|
if c.Kind == "" {
|
|
if len(c.Children) > 0 {
|
|
c.Kind = "vdev"
|
|
} else {
|
|
c.Kind = "device"
|
|
}
|
|
}
|
|
kinds(c)
|
|
}
|
|
}
|
|
|
|
var keyLine = regexp.MustCompile(`^\s{0,8}([a-z]+):(?:\s(.*))?$`)
|
|
|
|
// statusKey is a `key: value` line of status: pool, state, status, action, see, scan, config, errors, …
|
|
func statusKey(line string) (string, string, bool) {
|
|
if strings.HasPrefix(line, "\t") {
|
|
return "", "", false
|
|
}
|
|
m := keyLine.FindStringSubmatch(line)
|
|
if m == nil {
|
|
return "", "", false
|
|
}
|
|
return m[1], strings.TrimSpace(m[2]), true
|
|
}
|
|
|
|
func isCount(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
_, err := strconv.ParseFloat(strings.TrimRight(s, "KMGTP"), 64)
|
|
return err == nil
|
|
}
|
|
|
|
// count is an error count: exact under -p, else 1.2K and the like.
|
|
func count(s string) uint64 {
|
|
mult := 1.0
|
|
switch s[len(s)-1] {
|
|
case 'K':
|
|
mult = 1e3
|
|
case 'M':
|
|
mult = 1e6
|
|
case 'G':
|
|
mult = 1e9
|
|
case 'T':
|
|
mult = 1e12
|
|
case 'P':
|
|
mult = 1e15
|
|
}
|
|
f, _ := strconv.ParseFloat(strings.TrimRight(s, "KMGTP"), 64)
|
|
return uint64(f * mult)
|
|
}
|
|
|
|
var (
|
|
scrubFinished = regexp.MustCompile(`^scrub repaired (\S+) in (.+?) with (\d+) errors on (.+)$`)
|
|
resilverFinished = regexp.MustCompile(`^resilvered (?:\((\S+)\) )?(\S+) in (.+?) with (\d+) errors on (.+)$`)
|
|
scanRunning = regexp.MustCompile(`^(scrub|resilver|error scrub)(?: \((\S+)\))? in progress since (.+)$`)
|
|
scanPaused = regexp.MustCompile(`^(scrub|error scrub) paused since (.+)$`)
|
|
scanCanceled = regexp.MustCompile(`^(scrub|resilver|error scrub)(?: \((\S+)\))? canceled on (.+)$`)
|
|
errScrubFinished = regexp.MustCompile(`^scrubbed (\d+) error blocks in (\d+ days \d+:\d+:\d+) on (.+)$`)
|
|
scanStartedOn = regexp.MustCompile(`(?m)^(?:scrub|error scrub) started on (.+)$`)
|
|
scanDone = regexp.MustCompile(`([\d.]+)% done`)
|
|
scanToGo = regexp.MustCompile(`(\d+ days \d+:\d+:\d+|\d+:\d+:\d+) to go`)
|
|
scanRepaired = regexp.MustCompile(`(?m)^(\S+) (?:repaired|resilvered),`)
|
|
)
|
|
|
|
// parseScan reads one scan line and its continuation, as zpool_main.c's print_scan_scrub_resilver_status,
|
|
// print_rebuild_status_impl and print_err_scrub_status write them (OpenZFS 2.4.4).
|
|
func parseScan(key, said string) Scan {
|
|
s := Scan{State: "unknown", Said: said}
|
|
first, more, _ := strings.Cut(said, "\n")
|
|
first = strings.TrimSpace(first)
|
|
switch {
|
|
case first == "none requested":
|
|
s.State = "none"
|
|
return s
|
|
case scrubFinished.MatchString(first):
|
|
m := scrubFinished.FindStringSubmatch(first)
|
|
s.Kind, s.State, s.Repaired, s.Took = "scrub", "finished", m[1], m[2]
|
|
s.Errors, s.Ended = atoiPtr(m[3]), when(m[4])
|
|
case resilverFinished.MatchString(first):
|
|
m := resilverFinished.FindStringSubmatch(first)
|
|
s.Kind, s.Vdev, s.State, s.Repaired, s.Took = "resilver", m[1], "finished", m[2], m[3]
|
|
s.Errors, s.Ended = atoiPtr(m[4]), when(m[5])
|
|
case errScrubFinished.MatchString(first):
|
|
m := errScrubFinished.FindStringSubmatch(first)
|
|
s.Kind, s.State, s.Repaired, s.Took, s.Ended = "error scrub", "finished", m[1]+" error blocks", m[2], when(m[3])
|
|
case scanRunning.MatchString(first):
|
|
m := scanRunning.FindStringSubmatch(first)
|
|
s.Kind, s.Vdev, s.State, s.Started = m[1], m[2], "running", when(m[3])
|
|
case scanPaused.MatchString(first):
|
|
m := scanPaused.FindStringSubmatch(first)
|
|
s.Kind, s.State, s.Paused = m[1], "paused", when(m[2])
|
|
case scanCanceled.MatchString(first):
|
|
m := scanCanceled.FindStringSubmatch(first)
|
|
s.Kind, s.Vdev, s.State, s.Ended = m[1], m[2], "canceled", when(m[3])
|
|
}
|
|
if s.Kind == "" && key == "scrub" {
|
|
s.Kind = "error scrub"
|
|
}
|
|
if s.State == "finished" && s.Ended != nil {
|
|
if d, ok := took(s.Took); ok {
|
|
st := s.Ended.Add(-d)
|
|
s.Started = &st
|
|
}
|
|
}
|
|
if more != "" {
|
|
if m := scanStartedOn.FindStringSubmatch(more); m != nil {
|
|
s.Started = when(m[1])
|
|
}
|
|
if m := scanDone.FindStringSubmatch(more); m != nil {
|
|
v, _ := strconv.ParseFloat(m[1], 64)
|
|
s.Done = &v
|
|
}
|
|
if m := scanToGo.FindStringSubmatch(more); m != nil {
|
|
s.ToGo = m[1]
|
|
}
|
|
s.NoEstimate = strings.Contains(more, "no estimated completion time")
|
|
if m := scanRepaired.FindStringSubmatch(more); m != nil && s.Repaired == "" {
|
|
s.Repaired = m[1]
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func atoiPtr(s string) *int {
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &n
|
|
}
|
|
|
|
// Scrub is the pool's scrub as status shows it; nil when status shows none (never asked, or hidden by a
|
|
// later resilver).
|
|
func (p PoolStatus) Scrub() *Scan {
|
|
for i := range p.Scans {
|
|
if p.Scans[i].Kind == "scrub" {
|
|
return &p.Scans[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Resilvers are the pool's resilvers as status shows them: the pool-wide one, or each vdev's rebuild.
|
|
func (p PoolStatus) Resilvers() []Scan {
|
|
var out []Scan
|
|
for _, s := range p.Scans {
|
|
if s.Kind == "resilver" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ScansSaid is every scan line's first line, joined.
|
|
func (p PoolStatus) ScansSaid() string {
|
|
var out []string
|
|
for _, s := range p.Scans {
|
|
out = append(out, firstLine(s.Said))
|
|
}
|
|
if len(out) == 0 {
|
|
return "no scan on record"
|
|
}
|
|
return strings.Join(out, "; ")
|
|
}
|
|
|
|
// when reads status's dates, as ctime writes them: "Sun Sep 13 05:36:34 2026", in local time.
|
|
func when(s string) *time.Time {
|
|
s = strings.TrimSpace(s)
|
|
for _, layout := range []string{time.ANSIC, "Mon Jan 2 15:04:05 2006"} {
|
|
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
|
|
return &t
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// took reads a scan's duration: "05:12:33" or "1 days 05:12:33".
|
|
func took(s string) (time.Duration, bool) {
|
|
days := 0
|
|
if d, rest, ok := strings.Cut(s, " days "); ok {
|
|
n, err := strconv.Atoi(d)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
days, s = n, rest
|
|
}
|
|
p := strings.Split(s, ":")
|
|
if len(p) != 3 {
|
|
return 0, false
|
|
}
|
|
var v [3]int
|
|
for i := range p {
|
|
n, err := strconv.Atoi(p[i])
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
v[i] = n
|
|
}
|
|
return time.Duration(days)*24*time.Hour + time.Duration(v[0])*time.Hour + time.Duration(v[1])*time.Minute +
|
|
time.Duration(v[2])*time.Second, true
|
|
}
|
|
|
|
// Status is every pool's status.
|
|
func (m *Machine) Status(ctx context.Context) ([]PoolStatus, error) {
|
|
out, err := m.ask(ctx, "zpool", StatusArgv()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ParseStatus(out)
|
|
}
|
|
|
|
// PoolsAnswer is what zfs_pools answers.
|
|
type PoolsAnswer struct {
|
|
Applicable bool `json:"applicable"`
|
|
Pools []PoolHealth `json:"pools"`
|
|
Said []string `json:"said"`
|
|
}
|
|
|
|
// PoolHealth is one pool's health and space.
|
|
type PoolHealth struct {
|
|
PoolRow
|
|
State string `json:"state"`
|
|
Status string `json:"status,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
Errors string `json:"errors"`
|
|
DeviceErrs uint64 `json:"device_errors"`
|
|
Scan string `json:"last_scan"`
|
|
}
|
|
|
|
// PoolsNow is every pool's health in a line, with its space.
|
|
func (m *Machine) PoolsNow(ctx context.Context) (any, error) {
|
|
ps, err := m.Pools(ctx)
|
|
if err != nil {
|
|
return notHere(err)
|
|
}
|
|
st, err := m.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a := PoolsAnswer{Applicable: true, Pools: []PoolHealth{}, Said: []string{}}
|
|
for _, p := range ps {
|
|
h := PoolHealth{PoolRow: p.Row(), State: p.Health}
|
|
if s := statusOf(st, p.Name); s != nil {
|
|
h.State, h.Status, h.Action, h.Errors, h.Scan = s.State, s.Status, s.Action, s.Errors, s.ScansSaid()
|
|
h.DeviceErrs = sumErrors(s.Root)
|
|
}
|
|
line := h.Said + fmt.Sprintf("; %d device errors; errors: %s", h.DeviceErrs, h.Errors)
|
|
if h.Status != "" {
|
|
line += "; status: " + h.Status
|
|
}
|
|
a.Pools = append(a.Pools, h)
|
|
a.Said = append(a.Said, line)
|
|
}
|
|
if len(ps) == 0 {
|
|
a.Said = append(a.Said, "ZFS is here, and no pool is imported.")
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// LayoutAnswer is what zfs_pool_layout answers.
|
|
type LayoutAnswer struct {
|
|
Applicable bool `json:"applicable"`
|
|
Pools []PoolStatus `json:"pools"`
|
|
Said []string `json:"said"`
|
|
}
|
|
|
|
// Layout is every pool's vdevs and devices with their state and errors.
|
|
func (m *Machine) Layout(ctx context.Context) (any, error) {
|
|
st, err := m.Status(ctx)
|
|
if err != nil {
|
|
return notHere(err)
|
|
}
|
|
a := LayoutAnswer{Applicable: true, Pools: st, Said: []string{}}
|
|
for _, p := range st {
|
|
a.Said = append(a.Said, fmt.Sprintf("pool %s: %s", p.Name, p.State))
|
|
walk(p.Root, func(d *Device, depth int) {
|
|
if d.Kind == "pool" {
|
|
return
|
|
}
|
|
line := strings.Repeat(" ", depth) + d.Name
|
|
if d.Kind == "section" {
|
|
a.Said = append(a.Said, line+" ("+d.Role+")")
|
|
return
|
|
}
|
|
line += " " + d.State
|
|
if d.Errors() > 0 {
|
|
line += fmt.Sprintf(" — ERRORS read %d, write %d, checksum %d", d.Read, d.Write, d.Checksum)
|
|
}
|
|
if d.Note != "" {
|
|
line += " " + d.Note
|
|
}
|
|
a.Said = append(a.Said, line)
|
|
}, 0)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func walk(d *Device, f func(*Device, int), depth int) {
|
|
if d == nil {
|
|
return
|
|
}
|
|
f(d, depth)
|
|
for _, c := range d.Children {
|
|
walk(c, f, depth+1)
|
|
}
|
|
}
|
|
|
|
func sumErrors(d *Device) uint64 {
|
|
var n uint64
|
|
walk(d, func(x *Device, _ int) {
|
|
if x.Kind != "section" {
|
|
n += x.Errors()
|
|
}
|
|
}, 0)
|
|
return n
|
|
}
|
|
|
|
func statusOf(st []PoolStatus, name string) *PoolStatus {
|
|
for i := range st {
|
|
if st[i].Name == name {
|
|
return &st[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|