nfs-server: export a share only to the nodes granted and wanting it, and never root's, a symlinked or nested backup, or an unmounted dataset
The review of #136 found a share could squash every client to root, be judged at a symlink's name rather than its folder, sit inside or above a backup repository deeper than one level, export an unmounted dataset's empty folder, go to every address in the private network's range, and stay exported after the module left. Each is now refused or closed, with a test that reproduced it first: - a folder owned by uid or gid 0 is refused; - the path is resolved through symlinks and every check runs on the real folder, which the export line names; - a share inside a backup repository (any parent) or holding one within four levels is refused, and a look cut short by its bound refuses; - every line carries mp, or mp=<its filesystem's mount point>; - a share goes only to the nodes this machine's grants setting names and whose mounts module wants it on the bus, at the lower access, at the roster's address inside the range; nothing without wants; - the process is declared before its config, so on unassignment it finds the config gone, removes its export file and lets the shares go; - confirm accepts the seat's string "true".
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -21,6 +23,9 @@ type Status struct {
|
||||
Shares []Exported `json:"shares"`
|
||||
Refused []Refused `json:"refused,omitempty"`
|
||||
Problems []string `json:"problems,omitempty"`
|
||||
// Said is what does not match between the grants and the wants: a node wanting a share it is not
|
||||
// granted, a grant nobody wants, a share exported to no node.
|
||||
Said []string `json:"said,omitempty"`
|
||||
// Reloaded is when the process last had the kernel read the export files again, and why.
|
||||
Reloaded *time.Time `json:"reloaded,omitempty"`
|
||||
ReloadWhy string `json:"reload_why,omitempty"`
|
||||
@@ -40,8 +45,17 @@ func (m *Machine) Pass(ctx context.Context, last *Status) Status {
|
||||
c, problems := ParseConfig(text)
|
||||
s.Problems = append(s.Problems, problems...)
|
||||
s.Range = c.Range
|
||||
exports, refused := m.Judge(c)
|
||||
roster, note := m.Roster()
|
||||
if note != "" {
|
||||
s.Problems = append(s.Problems, note)
|
||||
}
|
||||
wants, note := m.Wants()
|
||||
if note != "" {
|
||||
s.Said = append(s.Said, note)
|
||||
}
|
||||
exports, refused, said := m.Judge(c, roster, wants)
|
||||
s.Refused = refused
|
||||
s.Said = append(s.Said, said...)
|
||||
|
||||
want := ExportFileText(exports)
|
||||
have, _ := m.Read(ExportFile)
|
||||
@@ -56,7 +70,7 @@ func (m *Machine) Pass(ctx context.Context, last *Status) Status {
|
||||
live, note := m.Live()
|
||||
if reload == "" {
|
||||
for _, e := range exports {
|
||||
if !IsLive(live, e.Path, e.Clients) {
|
||||
if !SharedLive(live, e) {
|
||||
reload = fmt.Sprintf("the share %s was not in the kernel's table", e.Name)
|
||||
break
|
||||
}
|
||||
@@ -75,7 +89,7 @@ func (m *Machine) Pass(ctx context.Context, last *Status) Status {
|
||||
s.Problems = append(s.Problems, note)
|
||||
}
|
||||
for i := range exports {
|
||||
exports[i].Live = IsLive(live, exports[i].Path, exports[i].Clients)
|
||||
exports[i].Live = SharedLive(live, exports[i])
|
||||
}
|
||||
s.Shares = exports
|
||||
raw, _ := json.MarshalIndent(s, "", " ")
|
||||
@@ -85,7 +99,7 @@ func (m *Machine) Pass(ctx context.Context, last *Status) Status {
|
||||
return s
|
||||
}
|
||||
|
||||
// RunExports is the process: a pass now and every Every until the context ends.
|
||||
// RunExports is the process: a pass now and every Every until the context ends, then Stopped.
|
||||
func (m *Machine) RunExports(ctx context.Context) error {
|
||||
last := m.Pass(ctx, nil)
|
||||
t := time.NewTicker(Every)
|
||||
@@ -93,6 +107,7 @@ func (m *Machine) RunExports(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
m.Stopped()
|
||||
return nil
|
||||
case <-t.C:
|
||||
last = m.Pass(ctx, &last)
|
||||
@@ -112,3 +127,65 @@ func (m *Machine) LastStatus() (*Status, error) {
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Roster is the mesh's machines by name, with their private addresses, as the mesh rendered them.
|
||||
func (m *Machine) Roster() (map[string]string, string) {
|
||||
text, err := m.Read(RosterFile)
|
||||
if err != nil {
|
||||
return map[string]string{}, fmt.Sprintf("the mesh's roster (%s) could not be read: %v; no node's address is known, so nothing is exported", RosterFile, err)
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 2 || strings.HasPrefix(f[0], "#") {
|
||||
continue
|
||||
}
|
||||
out[f[0]] = f[1]
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
// Wants is every node's wants of this machine, as the tools last took them from the bus. With none taken
|
||||
// yet, nothing is wanted, and nothing is exported.
|
||||
func (m *Machine) Wants() (map[string][]Want, string) {
|
||||
text, err := m.Read(WantsFile)
|
||||
if err != nil {
|
||||
return map[string][]Want{}, "no node's wants have been taken from the bus yet, so nothing is exported"
|
||||
}
|
||||
var w WantsFromBus
|
||||
if err := json.Unmarshal([]byte(text), &w); err != nil {
|
||||
return map[string][]Want{}, fmt.Sprintf("the wants taken from the bus (%s) could not be read: %v; nothing is exported", WantsFile, err)
|
||||
}
|
||||
if w.Wants == nil {
|
||||
w.Wants = map[string][]Want{}
|
||||
}
|
||||
return w.Wants, ""
|
||||
}
|
||||
|
||||
// SharedLive says whether the kernel exports a share to every node it is meant for.
|
||||
func SharedLive(live []Entry, e Exported) bool {
|
||||
for _, g := range e.To {
|
||||
if !IsLive(live, e.Path, g.Address) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Stopped is the process ending. When the module was unassigned — the node-engine removes the module's
|
||||
// files in reverse declaration order, so the config file, declared after this process, is already gone
|
||||
// when the process is stopped (mesh-host internal/apply: orphans removed in reverse order) — the
|
||||
// module's export file goes and the kernel lets the shares go. A plain restart, a push or the machine
|
||||
// shutting down leaves the config file, and the exports stay.
|
||||
func (m *Machine) Stopped() {
|
||||
if _, err := m.Read(ConfigFile); err == nil || !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err := m.unexport(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "nfs-server:", err)
|
||||
return
|
||||
}
|
||||
if _, err := m.AsRoot("exportfs", "-ra"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "nfs-server: the export file is gone, and exportfs -ra failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user