package web import ( "log" "net/http" "net/url" "os" "sort" "strings" "nasmon/internal/collect" "nasmon/internal/sched" "nasmon/internal/shares" ) // ---- /shares: SMB + NFS management type smbForm struct { Orig, Name, Path, Comment string ReadOnly, Guest, Browseable, HideSys bool Users []string New bool } type nfsForm struct { Orig, Path, Clients, Squash string ReadOnly, Async bool New bool } type sharesVM struct { C shares.Config Svc collect.ShareServices Users []string Mountpoints []string Owner string Host string Form smbForm NForm nfsForm ApplyErr string // last Apply failure: config saved but not live Squashes []string } func (s *Server) applyErr() string { s.mu.Lock() defer s.mu.Unlock() return s.shareErr } func (s *Server) setApplyErr(e string) { s.mu.Lock() s.shareErr = e s.mu.Unlock() } // mountpoints lists dataset mountpoints — the only places shares may point to. func mountpoints(st sched.Status) []string { var out []string for _, d := range st.Datasets { if strings.HasPrefix(d.Mountpoint, "/") { out = append(out, d.Mountpoint) } } sort.Strings(out) return out } func (s *Server) sharesView(st sched.Status, form smbForm, nform nfsForm) sharesVM { c := s.shares.Get() vm := sharesVM{C: c, Svc: st.Services, Mountpoints: mountpoints(st), Owner: s.shm.Owner, Host: st.Host, Form: form, NForm: nform, ApplyErr: s.applyErr(), Squashes: []string{"root", "all", "none"}} if vm.Form.Name == "" && vm.Form.Orig == "" { vm.Form.New, vm.Form.Guest, vm.Form.Browseable, vm.Form.HideSys = true, true, true, true } if vm.NForm.Path == "" && vm.NForm.Orig == "" { vm.NForm.New = true if vm.NForm.Squash == "" { vm.NForm.Squash = "root" } } if u, err := s.shm.ListUsers(); err == nil { vm.Users = u } else { log.Printf("shares: list users: %v", err) } return vm } func smbToForm(sh shares.SMBShare) smbForm { return smbForm{Orig: sh.Name, Name: sh.Name, Path: sh.Path, Comment: sh.Comment, ReadOnly: sh.ReadOnly, Guest: sh.Guest, Browseable: sh.Browseable, HideSys: sh.HideSysFiles, Users: sh.Users} } func nfsToForm(e shares.NFSExport) nfsForm { return nfsForm{Orig: e.Path, Path: e.Path, Clients: strings.Join(e.Clients, ", "), Squash: e.Squash, ReadOnly: e.ReadOnly, Async: e.Async} } func (s *Server) sharesPage(w http.ResponseWriter, r *http.Request) { st := s.sch.Status() c := s.shares.Get() q := r.URL.Query() var form smbForm var nform nfsForm if i := c.FindSMB(q.Get("edit")); i >= 0 { form = smbToForm(c.SMB.Shares[i]) } if i := c.FindNFS(q.Get("nedit")); i >= 0 { nform = nfsToForm(c.NFS.Exports[i]) } if p := q.Get("path"); p != "" { // "share…" link from the ZFS page if form.Orig == "" { form.Path = p form.Name = strings.Trim(p[strings.LastIndex(p, "/")+1:], "/") } if nform.Orig == "" { nform.Path = p } } s.render(w, r, "shares", pageData{Title: "Shares", Data: s.sharesView(st, form, nform), Alerts: len(st.Alerts), Flash: q.Get("msg")}) } // saveAndApply persists the config and pushes it to the daemons. Returns the // message for the user and whether everything went live. func (s *Server) saveAndApply(c shares.Config) error { if err := s.shares.Save(c); err != nil { return err } if err := s.shm.Apply(c); err != nil { s.setApplyErr(err.Error()) log.Printf("shares: apply failed: %v", err) return nil } s.setApplyErr("") s.sch.RefreshStorage() return nil } func (s *Server) sharesFail(w http.ResponseWriter, r *http.Request, msg string, form smbForm, nform nfsForm) { st := s.sch.Status() s.render(w, r, "shares", pageData{Title: "Shares", Data: s.sharesView(st, form, nform), Alerts: len(st.Alerts), Error: msg}) } func (s *Server) sharesDone(w http.ResponseWriter, r *http.Request, msg, anchor string) { if e := s.applyErr(); e != "" { msg += " — but applying failed: " + e } http.Redirect(w, r, "/shares?msg="+url.QueryEscape(msg)+anchor, http.StatusSeeOther) } // pathOK checks that a share path exists and lies inside a ZFS dataset. func (s *Server) pathOK(st sched.Status, p string) error { if err := shares.ValidatePath(p); err != nil { return err } if !shares.UnderRoots(p, mountpoints(st)) { return errPathOutside } if !s.dirExists(p) { return errPathMissing } return nil } func statDir(p string) bool { fi, err := os.Stat(p) return err == nil && fi.IsDir() } type strErr string func (e strErr) Error() string { return string(e) } const ( errPathOutside = strErr("path must be inside a ZFS dataset (see the list of mountpoints)") errPathMissing = strErr("path does not exist or is not a directory") ) func (s *Server) smbSettings(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } c := s.shares.Get() c.SMB.Enabled = r.FormValue("enabled") != "" c.SMB.Workgroup = strings.ToUpper(strings.TrimSpace(r.FormValue("workgroup"))) c.SMB.ServerString = strings.TrimSpace(r.FormValue("server_string")) if err := shares.ValidateComment(c.SMB.ServerString); err != nil { s.sharesFail(w, r, "Not saved: server description: "+err.Error(), smbForm{}, nfsForm{}) return } if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, nfsForm{}) return } s.sharesDone(w, r, "SMB settings saved", "#smb") } func (s *Server) smbSave(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f := smbForm{ Orig: strings.TrimSpace(r.FormValue("orig")), Name: strings.TrimSpace(r.FormValue("name")), Path: strings.TrimSpace(r.FormValue("path")), Comment: strings.TrimSpace(r.FormValue("comment")), ReadOnly: r.FormValue("read_only") != "", Guest: r.FormValue("guest") != "", Browseable: r.FormValue("browseable") != "", HideSys: r.FormValue("hide_sys") != "", Users: r.Form["users"], } if len(f.Path) > 1 { f.Path = strings.TrimRight(f.Path, "/") } st := s.sch.Status() sh := shares.SMBShare{Name: f.Name, Path: f.Path, Comment: f.Comment, ReadOnly: f.ReadOnly, Guest: f.Guest, Users: f.Users, Browseable: f.Browseable, HideSysFiles: f.HideSys} if sh.Guest { sh.Users = nil } if err := sh.Validate(); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), f, nfsForm{}) return } if err := s.pathOK(st, sh.Path); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), f, nfsForm{}) return } c := s.shares.Get() if i := c.FindSMB(f.Orig); f.Orig != "" && i >= 0 { if sh.Name != f.Orig && c.FindSMB(sh.Name) >= 0 { s.sharesFail(w, r, "Not saved: share "+sh.Name+" already exists", f, nfsForm{}) return } c.SMB.Shares[i] = sh } else { if c.FindSMB(sh.Name) >= 0 { s.sharesFail(w, r, "Not saved: share "+sh.Name+" already exists — edit it instead", f, nfsForm{}) return } c.SMB.Shares = append(c.SMB.Shares, sh) } if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), f, nfsForm{}) return } log.Printf("shares: SMB share %s -> %s saved (guest=%v users=%v ro=%v)", sh.Name, sh.Path, sh.Guest, sh.Users, sh.ReadOnly) s.sharesDone(w, r, "Share "+sh.Name+" saved", "#smb") } func (s *Server) smbDelete(w http.ResponseWriter, r *http.Request) { name := strings.TrimSpace(r.FormValue("name")) c := s.shares.Get() i := c.FindSMB(name) if i < 0 { s.sharesFail(w, r, "No such share: "+name, smbForm{}, nfsForm{}) return } c.SMB.Shares = append(c.SMB.Shares[:i], c.SMB.Shares[i+1:]...) if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not deleted: "+err.Error(), smbForm{}, nfsForm{}) return } log.Printf("shares: SMB share %s deleted", name) s.sharesDone(w, r, "Share "+name+" deleted", "#smb") } func (s *Server) nfsSave(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f := nfsForm{ Orig: strings.TrimSpace(r.FormValue("orig")), Path: strings.TrimSpace(r.FormValue("path")), Clients: strings.TrimSpace(r.FormValue("clients")), Squash: r.FormValue("squash"), ReadOnly: r.FormValue("read_only") != "", Async: r.FormValue("async") != "", } if len(f.Path) > 1 { f.Path = strings.TrimRight(f.Path, "/") } st := s.sch.Status() e := shares.NFSExport{Path: f.Path, Clients: shares.SplitList(f.Clients), ReadOnly: f.ReadOnly, Squash: f.Squash, Async: f.Async} if err := e.Validate(); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, f) return } if err := s.pathOK(st, e.Path); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, f) return } c := s.shares.Get() if i := c.FindNFS(f.Orig); f.Orig != "" && i >= 0 { if e.Path != f.Orig && c.FindNFS(e.Path) >= 0 { s.sharesFail(w, r, "Not saved: "+e.Path+" is already exported", smbForm{}, f) return } c.NFS.Exports[i] = e } else { if c.FindNFS(e.Path) >= 0 { s.sharesFail(w, r, "Not saved: "+e.Path+" is already exported — edit it instead", smbForm{}, f) return } c.NFS.Exports = append(c.NFS.Exports, e) } if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, f) return } log.Printf("shares: NFS export %s to %v saved (ro=%v squash=%s)", e.Path, e.Clients, e.ReadOnly, e.Squash) s.sharesDone(w, r, "Export "+e.Path+" saved", "#nfs") } func (s *Server) nfsDelete(w http.ResponseWriter, r *http.Request) { p := strings.TrimSpace(r.FormValue("path")) c := s.shares.Get() i := c.FindNFS(p) if i < 0 { s.sharesFail(w, r, "No such export: "+p, smbForm{}, nfsForm{}) return } c.NFS.Exports = append(c.NFS.Exports[:i], c.NFS.Exports[i+1:]...) if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not deleted: "+err.Error(), smbForm{}, nfsForm{}) return } log.Printf("shares: NFS export %s deleted", p) s.sharesDone(w, r, "Export "+p+" deleted", "#nfs") } func (s *Server) nfsSettings(w http.ResponseWriter, r *http.Request) { c := s.shares.Get() c.NFS.Enabled = r.FormValue("enabled") != "" if err := s.saveAndApply(c); err != nil { s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, nfsForm{}) return } s.sharesDone(w, r, "NFS settings saved", "#nfs") } func (s *Server) userSave(w http.ResponseWriter, r *http.Request) { name := strings.TrimSpace(r.FormValue("name")) pass := r.FormValue("password") if err := s.shm.SetPassword(name, pass); err != nil { s.sharesFail(w, r, "User not saved: "+err.Error(), smbForm{}, nfsForm{}) return } http.Redirect(w, r, "/shares?msg="+url.QueryEscape("Password set for "+name)+"#users", http.StatusSeeOther) } func (s *Server) userDelete(w http.ResponseWriter, r *http.Request) { name := strings.TrimSpace(r.FormValue("name")) c := s.shares.Get() for _, sh := range c.SMB.Shares { for _, u := range sh.Users { if u == name { s.sharesFail(w, r, "User "+name+" is still allowed on share "+sh.Name+" — remove them there first", smbForm{}, nfsForm{}) return } } } if err := s.shm.DeleteUser(name); err != nil { s.sharesFail(w, r, "User not deleted: "+err.Error(), smbForm{}, nfsForm{}) return } http.Redirect(w, r, "/shares?msg="+url.QueryEscape("User "+name+" deleted")+"#users", http.StatusSeeOther) } func (s *Server) sharesApply(w http.ResponseWriter, r *http.Request) { if err := s.saveAndApply(s.shares.Get()); err != nil { s.sharesFail(w, r, err.Error(), smbForm{}, nfsForm{}) return } s.sharesDone(w, r, "Configuration applied", "") }