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
+43 -6
View File
@@ -124,7 +124,9 @@ type zfsVM struct {
S sched.Status
Th config.Thresholds
Datasets []datasetRow
Form createForm // last submitted values (re-rendered after an error)
// PoolDedup maps a pool to the dedup property of its root dataset.
PoolDedup map[string]string
Form createForm // last submitted values (re-rendered after an error)
}
type datasetRow struct {
@@ -134,15 +136,18 @@ type datasetRow struct {
}
type createForm struct {
Pool, Name, Owner, Compression, Quota string
Pool, Name, Owner, Compression, Dedup, Quota string
}
func (s *Server) zfsView(st sched.Status, form createForm) zfsVM {
vm := zfsVM{S: st, Th: s.cfg.Get().Thresholds, Form: form}
vm := zfsVM{S: st, Th: s.cfg.Get().Thresholds, Form: form, PoolDedup: map[string]string{}}
if vm.Form.Owner == "" {
vm.Form.Owner = defaultOwner
}
for _, d := range st.Datasets {
if !strings.Contains(d.Name, "/") {
vm.PoolDedup[d.Name] = d.Dedup
}
row := datasetRow{Dataset: d, DomID: strings.NewReplacer("/", "-", ".", "_", ":", "_").Replace(d.Name)}
row.CanDestroy = strings.Contains(d.Name, "/") && d.Refer < zfsadm.EmptyRefer
for _, o := range st.Datasets {
@@ -170,6 +175,7 @@ func (s *Server) zfsCreate(w http.ResponseWriter, r *http.Request) {
Name: strings.Trim(strings.TrimSpace(r.FormValue("name")), "/"),
Owner: strings.TrimSpace(r.FormValue("owner")),
Compression: r.FormValue("compression"),
Dedup: r.FormValue("dedup"),
Quota: strings.ToUpper(strings.TrimSpace(r.FormValue("quota"))),
}
st := s.sch.Status()
@@ -191,20 +197,20 @@ func (s *Server) zfsCreate(w http.ResponseWriter, r *http.Request) {
return
}
}
for _, err := range []error{zfsadm.ValidateOwner(f.Owner), zfsadm.ValidateCompression(f.Compression), zfsadm.ValidateSize(f.Quota)} {
for _, err := range []error{zfsadm.ValidateOwner(f.Owner), zfsadm.ValidateCompression(f.Compression), zfsadm.ValidateDedup(f.Dedup), zfsadm.ValidateSize(f.Quota)} {
if err != nil {
fail(err.Error())
return
}
}
err := s.admin.Create(full, zfsadm.CreateOpts{Compression: f.Compression, Quota: f.Quota, Owner: f.Owner})
err := s.admin.Create(full, zfsadm.CreateOpts{Compression: f.Compression, Dedup: f.Dedup, Quota: f.Quota, Owner: f.Owner})
s.sch.RefreshStorage()
if err != nil {
log.Printf("zfs create %s: %v", full, err)
fail(err.Error())
return
}
log.Printf("dataset %s created (owner %s, compression %q, quota %q)", full, f.Owner, f.Compression, f.Quota)
log.Printf("dataset %s created (owner %s, compression %q, dedup %q, quota %q)", full, f.Owner, f.Compression, f.Dedup, f.Quota)
http.Redirect(w, r, "/zfs?msg="+url.QueryEscape("Dataset "+full+" created")+"#"+url.PathEscape(f.Pool), http.StatusSeeOther)
}
@@ -243,6 +249,37 @@ func (s *Server) zfsDestroy(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/zfs?msg="+url.QueryEscape("Dataset "+name+" destroyed"), http.StatusSeeOther)
}
// zfsDedup turns dedup on/off on a pool's root dataset (inherited by children).
func (s *Server) zfsDedup(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
pool := strings.TrimSpace(r.FormValue("pool"))
value := r.FormValue("dedup")
st := s.sch.Status()
fail := func(msg string) {
s.render(w, r, "zfs", pageData{Title: "ZFS", Data: s.zfsView(st, createForm{}), Alerts: len(st.Alerts), Error: "Dedup not changed: " + msg})
}
if !hasPool(st, pool) {
fail("unknown pool " + pool)
return
}
if value != "on" && value != "off" {
fail("dedup must be on or off")
return
}
err := s.admin.SetDedup(pool, value)
s.sch.RefreshStorage()
if err != nil {
log.Printf("zfs set dedup=%s %s: %v", value, pool, err)
fail(err.Error())
return
}
log.Printf("dedup=%s set on pool %s", value, pool)
http.Redirect(w, r, "/zfs?msg="+url.QueryEscape("Dedup "+value+" for "+pool)+"#"+url.PathEscape(pool), http.StatusSeeOther)
}
func hasPool(st sched.Status, name string) bool {
for _, p := range st.Pools {
if p.Name == name {