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 = `, and for `default` `zpool // set = `. For each set dataset property it runs `zfs set = // `, and for `default` `zfs inherit `, 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 }