174 lines
5.7 KiB
Go
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
|
|
}
|