Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/table.go
T
jochen 48fba44471
mesh/merge-gate fail: builds new: modules/mounts, modules/nfs-server, sent nowhere; no bus step; a manifest the change touches fails the module check: modu…
mesh/repo-check fail: its merge-check.sh failed: long-running resources without health: 70
mesh/delivery-group group feat/mounts-module rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
Add the nfs-server module: a node shares a folder with the mesh's machines only, every client mapped to the folder's owner (hq ADR 0263)
2026-10-08 18:36:01 +02:00

134 lines
3.5 KiB
Go

package main
import (
"context"
"path"
"strings"
)
// What the machine exports, read: the kernel's table, the other export files, and ZFS's property.
// Entry is one export: a path, one client, its options, and where it was found.
type Entry struct {
Path string `json:"path"`
Client string `json:"client"`
Options string `json:"options,omitempty"`
From string `json:"from"`
}
// ParseExports reads an exports(5) file or the kernel's etab: a path, then clients each with options.
func ParseExports(text, from string) []Entry {
var out []Entry
for _, line := range strings.Split(text, "\n") {
if i := strings.Index(line, "#"); i >= 0 {
line = line[:i]
}
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
p := fields[0]
if len(fields) == 1 {
out = append(out, Entry{Path: p, Client: "*", From: from})
continue
}
for _, c := range fields[1:] {
client, opts := c, ""
if i := strings.Index(c, "("); i >= 0 {
client, opts = c[:i], strings.TrimSuffix(c[i+1:], ")")
}
if client == "" {
client = "*"
}
out = append(out, Entry{Path: p, Client: client, Options: opts, From: from})
}
}
return out
}
// Live is the kernel's export table, or nil and why when it cannot be read.
func (m *Machine) Live() ([]Entry, string) {
text, err := m.Read(Etab)
if err != nil {
return nil, "the kernel's export table (" + Etab + ") could not be read: " + err.Error()
}
return ParseExports(text, Etab), ""
}
// IsLive says whether the kernel exports a path to a client.
func IsLive(live []Entry, p, client string) bool {
for _, e := range live {
if path.Clean(e.Path) == path.Clean(p) && e.Client == client {
return true
}
}
return false
}
// Dataset is one ZFS filesystem as `zfs list` answers it.
type Dataset struct {
Name string `json:"name"`
Mountpoint string `json:"mountpoint"`
ShareNFS string `json:"sharenfs"`
}
// Datasets are the ZFS filesystems, when zfs is there; nil without it.
func (m *Machine) Datasets() ([]Dataset, string) {
r := m.Run(context.Background(), "zfs", "list", "-H", "-o", "name,mountpoint,sharenfs", "-t", "filesystem")
if r.Code != 0 {
if r.Code == -1 && strings.Contains(r.Err, "executable file not found") {
return nil, ""
}
return nil, "zfs list did not answer: " + firstLine(r.Err)
}
var out []Dataset
for _, line := range strings.Split(r.Out, "\n") {
f := strings.Split(line, "\t")
if len(f) != 3 {
continue
}
out = append(out, Dataset{Name: f[0], Mountpoint: f[1], ShareNFS: f[2]})
}
return out, ""
}
// Foreign is every export on the machine that is not the mesh's: /etc/exports, the other files in
// /etc/exports.d (ZFS writes one), and every dataset whose sharenfs is on.
func (m *Machine) Foreign() ([]Entry, []Dataset, []string) {
var entries []Entry
var notes []string
files := append([]string{"/etc/exports"}, m.Glob("/etc/exports.d/*.exports")...)
for _, f := range files {
if f == ExportFile {
continue
}
text, err := m.Read(f)
if err != nil {
if m.Exists(f) {
notes = append(notes, f+" could not be read: "+err.Error())
}
continue
}
entries = append(entries, ParseExports(text, f)...)
}
sets, note := m.Datasets()
if note != "" {
notes = append(notes, note)
}
var shared []Dataset
for _, d := range sets {
if d.ShareNFS != "off" && d.ShareNFS != "-" && d.ShareNFS != "" {
shared = append(shared, d)
}
}
return entries, shared, notes
}
func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
return s
}