ZFS: dedup toggle per pool and dedup option for new datasets
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:
2026-10-03 09:30:38 +03:00
co-authored by Claude Opus 5.5
parent b965d2c9db
commit 0cdee29359
9 changed files with 209 additions and 15 deletions
+35 -1
View File
@@ -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