A trim schedule of weekly or monthly with no trim-day or trim-time is one refused value naming it; a keep count of 0 is that kind's none: the existing scheduled ZFS snapshots are kept and a warning names them. Builds: design 42 zfs-owns-zfs §20, §21, §22, A-33, A-35
167 lines
5.7 KiB
Go
167 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// The run's ZFS snapshots: removing them, only by their own record (design 42 zfs-owns-zfs §22).
|
|
//
|
|
// After taking, the run reads the ledger again in the same run (no read, no removal) and, per dataset and kind,
|
|
// removes the oldest past its keep count, all of them in that run. Before any destroy it checks that the name
|
|
// is `<a dataset of the pool>@mesh-<kind>-<UTC stamp>`, that the snapshot carries `mesh:schedule=<kind>`, that
|
|
// it is in the ledger, that it has no hold and no clone; anything else is refused, said and left. Then `zfs
|
|
// destroy -- <dataset>@<snapshot>`, the ledger's `removed`, and `snapshot.removed.schedule`. When `snapshots`
|
|
// goes back to `none` nothing is removed. A ZFS snapshot taken by hand is never removed.
|
|
|
|
// HoldsArgv lists a snapshot's holds.
|
|
func HoldsArgv(snapshot string) []string { return []string{"holds", "-H", "--", snapshot} }
|
|
|
|
// SnapshotPropertyArgv reads one property of a snapshot (mesh:schedule, clones).
|
|
func SnapshotPropertyArgv(snapshot, property string) []string {
|
|
return []string{"get", "-Hp", "-o", "value", property, "--", snapshot}
|
|
}
|
|
|
|
// DestroyArgv destroys one snapshot, never a dataset: the name always holds its @.
|
|
func DestroyArgv(dataset, snapshot string) []string {
|
|
return []string{"destroy", "--", dataset + "@" + snapshot}
|
|
}
|
|
|
|
// surplus are the entries of a dataset and kind past its keep count, oldest first.
|
|
func surplus(entries []LedgerEntry, keep int) []LedgerEntry {
|
|
sort.SliceStable(entries, func(i, j int) bool {
|
|
if entries[i].At != entries[j].At {
|
|
return entries[i].At < entries[j].At
|
|
}
|
|
return entries[i].Name < entries[j].Name
|
|
})
|
|
if len(entries) <= keep {
|
|
return nil
|
|
}
|
|
return entries[:len(entries)-keep]
|
|
}
|
|
|
|
// refuseRemoval is the reason an entry may not be destroyed, "" when every check holds.
|
|
func (m *Machine) refuseRemoval(ctx context.Context, pool string, e LedgerEntry) (string, error) {
|
|
full := e.Dataset + "@" + e.Name
|
|
nm := meshSnapshot.FindStringSubmatch(e.Name)
|
|
switch {
|
|
case e.Dataset != pool && !strings.HasPrefix(e.Dataset, pool+"/"):
|
|
return "its dataset is not a dataset of the pool", nil
|
|
case nm == nil || nm[1] != e.Kind:
|
|
return "its name is not mesh-" + e.Kind + "-<UTC stamp>", nil
|
|
}
|
|
sched, err := m.ask(ctx, "zfs", SnapshotPropertyArgv(full, "mesh:schedule")...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(sched) != e.Kind {
|
|
return fmt.Sprintf("it carries mesh:schedule=%q, not %q", strings.TrimSpace(sched), e.Kind), nil
|
|
}
|
|
holds, err := m.ask(ctx, "zfs", HoldsArgv(full)...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(holds) != "" {
|
|
return "it has a hold", nil
|
|
}
|
|
clones, err := m.ask(ctx, "zfs", SnapshotPropertyArgv(full, "clones")...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if c := strings.TrimSpace(clones); c != "" && c != "-" {
|
|
return "it has a clone", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// PruneScheduled removes the scheduled ZFS snapshots past their keep counts (a person's act when person, by
|
|
// them). It answers what it did, and a failed command as the error.
|
|
func (m *Machine) PruneScheduled(ctx context.Context, c *Config, person bool, by string) ([]string, error) {
|
|
if c.Snapshots == nil || c.Snapshots.None || c.Pool == "" {
|
|
return nil, nil
|
|
}
|
|
removed, skipped := EventRemovedSched, EventSkippedSched
|
|
if person {
|
|
removed, skipped = EventRemovedPerson, EventSkippedPerson
|
|
}
|
|
if m.Paused() {
|
|
return []string{"snapshots paused: nothing removed"}, nil
|
|
}
|
|
entries, err := m.ledgerEntries()
|
|
if err != nil {
|
|
return []string{"nothing removed: the ledger cannot be read"}, nil
|
|
}
|
|
snaps, err := m.Snapshots(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
have := map[string]Snapshot{}
|
|
for _, s := range snaps {
|
|
have[s.Name] = s
|
|
}
|
|
groups := map[string][]LedgerEntry{}
|
|
for _, e := range entries {
|
|
k := e.Dataset + " " + e.Kind
|
|
groups[k] = append(groups[k], e)
|
|
}
|
|
var said []string
|
|
var failed []error
|
|
capacity := 0
|
|
if ps, err := m.Pools(ctx); err == nil {
|
|
for _, p := range ps {
|
|
if p.Name == c.Pool {
|
|
capacity = p.Capacity
|
|
}
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(groups) {
|
|
g := groups[k]
|
|
if c.Snapshots.Of(g[0].Kind) == 0 {
|
|
// A-35: a keep count of 0 is that kind's none: its scheduled snapshots are kept and a warning names them.
|
|
names := make([]string, 0, len(g))
|
|
for _, e := range g {
|
|
names = append(names, e.Dataset+"@"+e.Name)
|
|
}
|
|
sort.Strings(names)
|
|
m.Warnings = append(m.Warnings, fmt.Sprintf("the keep count of %s is 0, which removes nothing: kept %s", g[0].Kind, strings.Join(names, ", ")))
|
|
continue
|
|
}
|
|
for _, e := range surplus(g, c.Snapshots.Of(g[0].Kind)) {
|
|
full := e.Dataset + "@" + e.Name
|
|
why, err := m.refuseRemoval(ctx, c.Pool, e)
|
|
if err != nil {
|
|
failed = append(failed, fmt.Errorf("%s: %v", full, err))
|
|
continue
|
|
}
|
|
if why != "" {
|
|
var f2 []error
|
|
if err := m.say(skipped, map[string]any{"pool": c.Pool, "capacity": capacity, "reason": "refused to remove " + full + ": " + why}); err != nil {
|
|
m.failure(&f2, skipped, err)
|
|
}
|
|
said = append(said, "left "+full+": "+why)
|
|
continue
|
|
}
|
|
freed := have[full].Used
|
|
if _, there := have[full]; there {
|
|
if _, err := m.AsRoot(ctx, "zfs", DestroyArgv(e.Dataset, e.Name)...); err != nil {
|
|
failed = append(failed, fmt.Errorf("%s: %v", full, err))
|
|
continue
|
|
}
|
|
}
|
|
if err := m.recordLedger("removed", e); err != nil {
|
|
failed = append(failed, fmt.Errorf("%s was removed, and the ledger did not take it: %v", full, err))
|
|
continue
|
|
}
|
|
if err := m.say(removed, map[string]any{"snapshot": full, "kind": e.Kind, "space freed in bytes": freed, "by": by}); err != nil {
|
|
m.failure(&failed, removed, err)
|
|
}
|
|
said = append(said, fmt.Sprintf("removed %s (%s freed)", full, strconv.FormatUint(freed, 10)))
|
|
}
|
|
}
|
|
return said, joinErrors(failed)
|
|
}
|