ZFS: dedup toggle per pool and dedup option for new datasets
build / build (push) Successful in 1m4s
build / build (push) Successful in 1m4s
Pool cards show the dedup ratio and can set dedup=on|off on the pool root dataset (inherited by children); the new-dataset form gets a dedup option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Package zfsadm performs the few mutating ZFS operations the UI offers
|
||||
// (create / destroy datasets). Commands go through a Runner so they can be
|
||||
// (create / destroy datasets, toggle pool dedup). Commands go through a Runner so they can be
|
||||
// executed on the host via systemd-run: nasmon runs in a sandboxed mount
|
||||
// namespace (ProtectSystem=strict), where a plain "zfs create" would mount the
|
||||
// new dataset only inside the service and leave the host without it.
|
||||
@@ -59,6 +59,7 @@ func (a *Admin) ctx() (context.Context, context.CancelFunc) {
|
||||
// CreateOpts are the user-visible knobs of "zfs create".
|
||||
type CreateOpts struct {
|
||||
Compression string // "", lz4, zstd, off ("" = inherit)
|
||||
Dedup string // "", on, off ("" = inherit)
|
||||
Quota string // "", or a size like 100G / 1.5T
|
||||
Owner string // uid:gid applied to the mountpoint; "" = leave root
|
||||
}
|
||||
@@ -71,6 +72,9 @@ func (a *Admin) Create(full string, o CreateOpts) error {
|
||||
if err := ValidateCompression(o.Compression); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateDedup(o.Dedup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateSize(o.Quota); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,6 +85,9 @@ func (a *Admin) Create(full string, o CreateOpts) error {
|
||||
if o.Compression != "" {
|
||||
args = append(args, "-o", "compression="+o.Compression)
|
||||
}
|
||||
if o.Dedup != "" {
|
||||
args = append(args, "-o", "dedup="+o.Dedup)
|
||||
}
|
||||
if o.Quota != "" {
|
||||
args = append(args, "-o", "quota="+o.Quota)
|
||||
}
|
||||
@@ -106,6 +113,25 @@ func (a *Admin) Create(full string, o CreateOpts) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDedup sets the dedup property (on/off) on the root dataset of pool, so it
|
||||
// applies to every dataset that does not override it. Only newly written
|
||||
// blocks are affected either way.
|
||||
func (a *Admin) SetDedup(pool, value string) error {
|
||||
if err := ValidateFullName(pool); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(pool, "/") {
|
||||
return errors.New("dedup is set on the pool root dataset only")
|
||||
}
|
||||
if value != "on" && value != "off" {
|
||||
return errors.New("dedup must be on or off")
|
||||
}
|
||||
ctx, cancel := a.ctx()
|
||||
defer cancel()
|
||||
_, err := a.Run(ctx, "zfs", "set", "dedup="+value, pool)
|
||||
return err
|
||||
}
|
||||
|
||||
// EmptyRefer is the upper bound of "referenced" bytes for a dataset we still
|
||||
// consider empty (a fresh dataset refers ~96-128 KiB of metadata).
|
||||
const EmptyRefer = 1 << 20
|
||||
@@ -206,6 +232,14 @@ func ValidateCompression(s string) error {
|
||||
return errors.New("compression must be inherit, lz4, zstd or off")
|
||||
}
|
||||
|
||||
func ValidateDedup(s string) error {
|
||||
switch s {
|
||||
case "", "on", "off":
|
||||
return nil
|
||||
}
|
||||
return errors.New("dedup must be inherit, on or off")
|
||||
}
|
||||
|
||||
func ValidateOwner(s string) error {
|
||||
if s == "" || reOwner.MatchString(s) {
|
||||
return nil
|
||||
|
||||
@@ -31,6 +31,9 @@ func TestValidators(t *testing.T) {
|
||||
if ValidateCompression("gzip") == nil || ValidateCompression("lz4") != nil || ValidateCompression("") != nil {
|
||||
t.Error("compression validation")
|
||||
}
|
||||
if ValidateDedup("verify") == nil || ValidateDedup("on") != nil || ValidateDedup("") != nil {
|
||||
t.Error("dedup validation")
|
||||
}
|
||||
if ValidateOwner("ansible") == nil || ValidateOwner("1000:1000") != nil || ValidateOwner("") != nil {
|
||||
t.Error("owner validation")
|
||||
}
|
||||
@@ -74,6 +77,16 @@ func TestCreate(t *testing.T) {
|
||||
t.Errorf("calls:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
|
||||
// dedup goes between compression and quota
|
||||
f = &fake{}
|
||||
a = New(f.run)
|
||||
if err := a.Create("tank/d", CreateOpts{Compression: "zstd", Dedup: "on", Quota: "1T"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.calls) != 1 || f.calls[0] != "zfs create -p -o compression=zstd -o dedup=on -o quota=1T tank/d" {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
|
||||
// no owner, inherit everything
|
||||
f = &fake{}
|
||||
a = New(f.run)
|
||||
@@ -115,6 +128,28 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDedup(t *testing.T) {
|
||||
f := &fake{}
|
||||
a := New(f.run)
|
||||
for _, c := range [][2]string{{"tank/x", "on"}, {"tank", "verify"}, {"tank", ""}, {"-tank", "on"}, {"", "off"}} {
|
||||
if err := a.SetDedup(c[0], c[1]); err == nil {
|
||||
t.Errorf("%q=%q accepted", c[0], c[1])
|
||||
}
|
||||
}
|
||||
if len(f.calls) != 0 {
|
||||
t.Errorf("rejected input ran commands: %v", f.calls)
|
||||
}
|
||||
if err := a.SetDedup("tank", "on"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.SetDedup("tank", "off"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(f.calls, "|") != "zfs set dedup=on tank|zfs set dedup=off tank" {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDestroyEmpty(t *testing.T) {
|
||||
ok := map[string]string{"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": ""}
|
||||
f := &fake{reply: ok}
|
||||
|
||||
Reference in New Issue
Block a user