Files
mesh-catalog/modules/zfs/cmd/zfs-tools/apply_properties.go
T

174 lines
5.7 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
)
// The apply's pool and dataset properties (design 42 zfs-owns-zfs §16, §10).
//
// For each set pool property the apply runs `zpool set <property>=<value> <pool>`, and for `default` `zpool
// set <property>=<ZFS's default> <pool>`. For each set dataset property it runs `zfs set <property>=<value>
// <dataset>`, and for `default` `zfs inherit <property> <dataset>`, so its source stays default. A property
// already at its value runs nothing. Each change is read back with `zpool get -Hp` or `zfs get -Hp` and said.
// sharenfs and sharesmb are never here: the parse refuses them (config.go).
// poolDefaults are ZFS's defaults of the pool properties the module sets.
var poolDefaults = map[string]string{"autotrim": "off", "autoexpand": "off", "autoreplace": "off", "failmode": "wait"}
// PoolGetArgv reads one pool property's value.
func PoolGetArgv(pool, property string) []string {
return []string{"get", "-Hp", "-o", "value", property, "--", pool}
}
// PoolSetArgv sets one pool property.
func PoolSetArgv(pool, property, value string) []string {
return []string{"set", property + "=" + value, "--", pool}
}
// DatasetGetArgv reads one dataset property's value and source.
func DatasetGetArgv(dataset, property string) []string {
return []string{"get", "-Hp", "-o", "value,source", property, "--", dataset}
}
// DatasetSetArgv sets one dataset property.
func DatasetSetArgv(dataset, property, value string) []string {
return []string{"set", property + "=" + value, "--", dataset}
}
// DatasetInheritArgv gives a dataset property back to ZFS's inheritance.
func DatasetInheritArgv(dataset, property string) []string {
return []string{"inherit", property, "--", dataset}
}
// sizeBytes reads `1M`, `128K`, `5T` or plain digits as bytes; false for another form.
func sizeBytes(v string) (uint64, bool) {
mult := uint64(1)
if n := len(v); n > 0 {
if i := strings.IndexByte("KMGT", v[n-1]); i >= 0 {
mult = uint64(1) << (10 * (i + 1))
v = v[:n-1]
}
}
n, err := strconv.ParseUint(v, 10, 64)
return n * mult, err == nil
}
// sameValue says whether ZFS's `-p` value is the wanted one in the form the config states it.
func sameValue(property, got, want string) bool {
switch property {
case "recordsize", "quota", "reservation":
if want == "none" {
return got == "0" || got == "none"
}
g, ok1 := sizeBytes(got)
w, ok2 := sizeBytes(want)
return ok1 && ok2 && g == w
}
return got == want
}
// ApplyProperties converges the set pool and dataset properties. It answers every command that failed, as
// one error; a property whose read-back differs from what was set is a failure too.
func (m *Machine) ApplyProperties(ctx context.Context, c *Config) error {
var failed []error
fail := func(key string, err error) { m.failure(&failed, key, err) }
for _, p := range sortedKeys(c.PoolProperties) {
key := "pool-properties." + p
want := c.PoolProperties[p]
if want == "default" {
want = poolDefaults[p]
}
got, err := m.ask(ctx, "zpool", PoolGetArgv(c.Pool, p)...)
if err != nil {
fail(key, err)
continue
}
before := strings.TrimSpace(got)
if before == want {
_ = m.keepAsSet(key, before)
continue
}
if _, err := m.AsRoot(ctx, "zpool", PoolSetArgv(c.Pool, p, want)...); err != nil {
fail(key, err)
continue
}
back, err := m.ask(ctx, "zpool", PoolGetArgv(c.Pool, p)...)
if err != nil || strings.TrimSpace(back) != want {
fail(key, fmt.Errorf("read back %q after setting %q (%v)", strings.TrimSpace(back), want, err))
continue
}
if err := m.Changed("pool", key, before, want, strings.TrimSpace(back)); err != nil {
fail(key, err)
}
}
for _, ds := range sortedKeys(c.DatasetProperties) {
for _, p := range sortedKeys(c.DatasetProperties[ds]) {
key := "dataset-properties." + ds + "." + p
want := c.DatasetProperties[ds][p]
value, source, err := m.datasetProperty(ctx, ds, p)
if err != nil {
fail(key, err)
continue
}
before := value
if want == "default" {
if source != "local" && source != "received" {
_ = m.keepAsSet(key, before)
continue
}
if _, err := m.AsRoot(ctx, "zfs", DatasetInheritArgv(ds, p)...); err != nil {
fail(key, err)
continue
}
back, bsource, err := m.datasetProperty(ctx, ds, p)
if err != nil || bsource == "local" || bsource == "received" {
fail(key, fmt.Errorf("read back source %q after inheriting (%v)", bsource, err))
continue
}
if err := m.Changed("dataset", key, before, "default", back); err != nil {
fail(key, err)
}
continue
}
if sameValue(p, value, want) {
_ = m.keepAsSet(key, before)
continue
}
if _, err := m.AsRoot(ctx, "zfs", DatasetSetArgv(ds, p, want)...); err != nil {
fail(key, err)
continue
}
back, _, err := m.datasetProperty(ctx, ds, p)
if err != nil || !sameValue(p, back, want) {
fail(key, fmt.Errorf("read back %q after setting %q (%v)", back, want, err))
continue
}
if err := m.Changed("dataset", key, before, want, back); err != nil {
fail(key, err)
}
}
}
return errors.Join(failed...)
}
// keepAsSet keeps a value found already at what is set: no command, no event, the state says it is as set.
func (m *Machine) keepAsSet(key, value string) error {
return m.keep(key, AppliedEntry{Before: value, After: value, Outcome: OutcomeApplied})
}
func (m *Machine) datasetProperty(ctx context.Context, ds, p string) (value, source string, err error) {
out, err := m.ask(ctx, "zfs", DatasetGetArgv(ds, p)...)
if err != nil {
return "", "", err
}
f := strings.Split(strings.TrimSpace(out), "\t")
if len(f) != 2 {
return "", "", fmt.Errorf("zfs get answered %q, not a value and a source", strings.TrimSpace(out))
}
return f[0], f[1], nil
}