build / build (push) Successful in 1m1s
Supermicro BMC virtual media ("CD-ROM image" over a Windows share) only
offers the SMB1 dialect "NT LM 0.12", which Samba rejects with
server min protocol = SMB2. Add an opt-in "Allow legacy clients" SMB
setting that renders server min protocol = NT1 and
ntlm auth = ntlmv1-permitted. Off by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
4.4 KiB
Go
181 lines
4.4 KiB
Go
// Package shares manages SMB (Samba) and NFS exports: the definitions live in
|
|
// /etc/nasmon/shares.json, from which nasmon generates /etc/samba/smb.conf and
|
|
// /etc/exports.d/nasmon.exports and reloads the daemons.
|
|
package shares
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
type SMBShare struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Comment string `json:"comment"`
|
|
ReadOnly bool `json:"read_only"`
|
|
Guest bool `json:"guest"` // anyone on the LAN, no password
|
|
Users []string `json:"users"` // SMB users allowed when Guest is false
|
|
Browseable bool `json:"browseable"`
|
|
HideSysFiles bool `json:"hide_sys_files"` // veto ._*, .DS_Store, Thumbs.db, desktop.ini
|
|
}
|
|
|
|
type SMBConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Workgroup string `json:"workgroup"`
|
|
ServerString string `json:"server_string"`
|
|
Legacy bool `json:"legacy"` // allow SMB1 + NTLMv1 (IPMI/BMC virtual media, old devices)
|
|
Shares []SMBShare `json:"shares"`
|
|
}
|
|
|
|
type NFSExport struct {
|
|
Path string `json:"path"`
|
|
Clients []string `json:"clients"` // host, IP, CIDR or *
|
|
ReadOnly bool `json:"read_only"`
|
|
Squash string `json:"squash"` // root | all | none
|
|
Async bool `json:"async"`
|
|
}
|
|
|
|
type NFSConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Exports []NFSExport `json:"exports"`
|
|
}
|
|
|
|
type Config struct {
|
|
SMB SMBConfig `json:"smb"`
|
|
NFS NFSConfig `json:"nfs"`
|
|
}
|
|
|
|
func Defaults() Config {
|
|
return Config{
|
|
SMB: SMBConfig{Enabled: true, Workgroup: "WORKGROUP", ServerString: "Home NAS"},
|
|
NFS: NFSConfig{Enabled: true},
|
|
}
|
|
}
|
|
|
|
// FindSMB returns the index of the share with that name, or -1.
|
|
func (c *Config) FindSMB(name string) int {
|
|
for i, s := range c.SMB.Shares {
|
|
if s.Name == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// FindNFS returns the index of the export for that path, or -1.
|
|
func (c *Config) FindNFS(path string) int {
|
|
for i, e := range c.NFS.Exports {
|
|
if e.Path == path {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (c *Config) sortShares() {
|
|
sort.Slice(c.SMB.Shares, func(i, j int) bool { return c.SMB.Shares[i].Name < c.SMB.Shares[j].Name })
|
|
sort.Slice(c.NFS.Exports, func(i, j int) bool { return c.NFS.Exports[i].Path < c.NFS.Exports[j].Path })
|
|
}
|
|
|
|
// Validate checks the whole configuration (pure; no filesystem access).
|
|
func (c *Config) Validate() error {
|
|
if err := ValidateWorkgroup(c.SMB.Workgroup); err != nil {
|
|
return err
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, s := range c.SMB.Shares {
|
|
if err := s.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if seen[s.Name] {
|
|
return fmt.Errorf("duplicate share name %s", s.Name)
|
|
}
|
|
seen[s.Name] = true
|
|
}
|
|
paths := map[string]bool{}
|
|
for _, e := range c.NFS.Exports {
|
|
if err := e.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if paths[e.Path] {
|
|
return fmt.Errorf("duplicate NFS export %s", e.Path)
|
|
}
|
|
paths[e.Path] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Store is a thread-safe holder for the share definitions backed by a file.
|
|
type Store struct {
|
|
path string
|
|
mu sync.RWMutex
|
|
cur Config
|
|
}
|
|
|
|
func Load(path string) (*Store, error) {
|
|
st := &Store{path: path, cur: Defaults()}
|
|
b, err := os.ReadFile(path)
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
return st, nil
|
|
case err != nil:
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(b, &st.cur); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
st.cur.sortShares()
|
|
return st, nil
|
|
}
|
|
|
|
func (st *Store) Get() Config {
|
|
st.mu.RLock()
|
|
defer st.mu.RUnlock()
|
|
return st.cur.clone()
|
|
}
|
|
|
|
func (c Config) clone() Config {
|
|
out := c
|
|
out.SMB.Shares = make([]SMBShare, len(c.SMB.Shares))
|
|
for i, s := range c.SMB.Shares {
|
|
s.Users = append([]string(nil), s.Users...)
|
|
out.SMB.Shares[i] = s
|
|
}
|
|
out.NFS.Exports = make([]NFSExport, len(c.NFS.Exports))
|
|
for i, e := range c.NFS.Exports {
|
|
e.Clients = append([]string(nil), e.Clients...)
|
|
out.NFS.Exports[i] = e
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (st *Store) Save(c Config) error {
|
|
if err := c.Validate(); err != nil {
|
|
return err
|
|
}
|
|
c.sortShares()
|
|
b, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
if err := os.MkdirAll(filepath.Dir(st.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := st.path + ".tmp"
|
|
if err := os.WriteFile(tmp, append(b, '\n'), 0o644); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, st.path); err != nil {
|
|
return err
|
|
}
|
|
st.cur = c.clone()
|
|
return nil
|
|
}
|