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
134 lines
3.5 KiB
Go
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
|
|
}
|