feat(nfs): per-host NFS options (IP/CIDR with own ro/async/squash flags)
This is a backward-compatible MINOR bump (0.4.0 → 0.5.0).
BREAKING NOTES (for users upgrading from pre-0.5.0):
- The nfs_exports.clients column schema changed from []string to
[]NFSClient (per-host options). A migration (0005) transforms existing
string arrays into object arrays, taking export-level options as
defaults for each host.
- ValidateNFSClient now only accepts IPv4 (192.168.1.1) or IPv4/CIDR
(192.168.1.0/24). Hostnames, wildcards, netgroups are rejected.
- If you use NASCTL_IMPORT_ON_BOOT, re-import your /etc/exports to pick
up per-host options.
What changed:
- NFSClient type: {host, read_only, async, root_squash, subtree_check, advanced}
- NFSExport.Clients is now []NFSClient (was []string)
- export-level flags (ro/async/root_squash/subtree_check/advanced) are
preserved as template defaults for newly added hosts in the UI.
- buildExportLine generates: path host1(ro,sync,...) host2(rw,async,...) fsid=N
- ValidateNFSClient: strict IPv4/CIDR only (0-255 octets, /0-32 prefix)
- parseExportLine now parses per-host options from /etc/exports (previously
only the first host's options were kept, others were discarded)
- UI: per-host rows with toggles (ro/async/root_squash/subtree_check) and
advanced options (all_squash, secure, wdelay, hide, crossmnt)
This commit is contained in:
+44
-33
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/darroyo/nasctl/internal/system"
|
||||
)
|
||||
|
||||
var nfsClientPattern = regexp.MustCompile(`^[a-zA-Z0-9_.:\-/\*@]+$`)
|
||||
var nfsClientPattern = regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$`)
|
||||
|
||||
func ImportNFSExports(path string) ([]db.NFSExport, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
@@ -47,9 +47,9 @@ func parseExports(data []byte) ([]db.NFSExport, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
validClients := make([]string, 0, len(export.Clients))
|
||||
validClients := make([]db.NFSClient, 0, len(export.Clients))
|
||||
for _, c := range export.Clients {
|
||||
if err := system.ValidateNFSClient(c); err != nil {
|
||||
if err := system.ValidateNFSClient(c.Host); err != nil {
|
||||
continue
|
||||
}
|
||||
validClients = append(validClients, c)
|
||||
@@ -102,8 +102,8 @@ func parseExportLine(line string) (db.NFSExport, bool) {
|
||||
return db.NFSExport{}, false
|
||||
}
|
||||
|
||||
var clients []string
|
||||
var options string
|
||||
var clients []db.NFSClient
|
||||
var firstOpts string
|
||||
|
||||
parts := splitExportsClients(rest)
|
||||
for _, part := range parts {
|
||||
@@ -115,54 +115,65 @@ func parseExportLine(line string) (db.NFSExport, bool) {
|
||||
open := strings.IndexByte(part, '(')
|
||||
close := strings.LastIndexByte(part, ')')
|
||||
|
||||
var client, opts string
|
||||
var host, opts string
|
||||
if open >= 0 && close > open {
|
||||
client = strings.TrimSpace(part[:open])
|
||||
host = strings.TrimSpace(part[:open])
|
||||
opts = strings.TrimSpace(part[open+1 : close])
|
||||
} else {
|
||||
client = part
|
||||
host = part
|
||||
opts = ""
|
||||
}
|
||||
|
||||
if !nfsClientPattern.MatchString(client) {
|
||||
if !nfsClientPattern.MatchString(host) {
|
||||
continue
|
||||
}
|
||||
|
||||
clients = append(clients, client)
|
||||
if opts != "" && options == "" {
|
||||
options = opts
|
||||
if opts == "" {
|
||||
opts = firstOpts
|
||||
if opts == "" {
|
||||
opts = "rw,sync,no_root_squash"
|
||||
}
|
||||
} else if firstOpts == "" {
|
||||
firstOpts = opts
|
||||
}
|
||||
|
||||
readOnly := strings.Contains(opts, "ro")
|
||||
async := strings.Contains(opts, "async")
|
||||
rootSquash := !strings.Contains(opts, "no_root_squash")
|
||||
subtreeCheck := strings.Contains(opts, "subtree_check")
|
||||
|
||||
adv := db.NFSAdvanced{
|
||||
AllSquash: strings.Contains(opts, "all_squash"),
|
||||
Secure: strings.Contains(opts, "secure"),
|
||||
WDelay: strings.Contains(opts, "wdelay"),
|
||||
Hide: strings.Contains(opts, "hide"),
|
||||
Crossmnt: strings.Contains(opts, "crossmnt"),
|
||||
}
|
||||
|
||||
clients = append(clients, db.NFSClient{
|
||||
Host: host,
|
||||
ReadOnly: readOnly,
|
||||
Async: async,
|
||||
RootSquash: rootSquash,
|
||||
SubtreeCheck: subtreeCheck,
|
||||
Advanced: adv,
|
||||
})
|
||||
}
|
||||
|
||||
if len(clients) == 0 {
|
||||
return db.NFSExport{}, false
|
||||
}
|
||||
|
||||
if options == "" {
|
||||
options = "rw,sync,no_root_squash"
|
||||
}
|
||||
|
||||
readOnly := strings.Contains(options, "ro")
|
||||
async := strings.Contains(options, "async")
|
||||
rootSquash := !strings.Contains(options, "no_root_squash")
|
||||
subtreeCheck := strings.Contains(options, "subtree_check")
|
||||
|
||||
adv := db.NFSAdvanced{
|
||||
AllSquash: strings.Contains(options, "all_squash"),
|
||||
Secure: strings.Contains(options, "secure"),
|
||||
WDelay: strings.Contains(options, "wdelay"),
|
||||
Hide: strings.Contains(options, "hide"),
|
||||
Crossmnt: strings.Contains(options, "crossmnt"),
|
||||
}
|
||||
advJSON, _ := json.Marshal(adv)
|
||||
first := clients[0]
|
||||
advJSON, _ := json.Marshal(first.Advanced)
|
||||
|
||||
return db.NFSExport{
|
||||
Path: path,
|
||||
Clients: clients,
|
||||
ReadOnly: readOnly,
|
||||
Async: async,
|
||||
RootSquash: rootSquash,
|
||||
SubtreeCheck: subtreeCheck,
|
||||
ReadOnly: first.ReadOnly,
|
||||
Async: first.Async,
|
||||
RootSquash: first.RootSquash,
|
||||
SubtreeCheck: first.SubtreeCheck,
|
||||
Advanced: string(advJSON),
|
||||
}, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user