175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
// The nfs-server module's Go bundle (novox/hq ADR 0263): a node shares a folder with the mesh's machines
|
|
// only, one share per folder, every client mapped to the folder's owner.
|
|
//
|
|
// Started with no arguments it is served by the node's runtime as the operator account, over MCP on stdio
|
|
// (ADR 0188, ADR 0193): the node-nfs-server seat's verbs and the module's health, and beside them it puts
|
|
// the process's last look in the module's state `exports`, keyed by this machine (ADR 0201). Started as
|
|
// `nfs-server exports` it is the module's long-running process, as root: it renders the export file from
|
|
// the assignment's shares and has the kernel read it (process.go). stdout is the MCP channel; everything
|
|
// else is said on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"sort"
|
|
"syscall"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// ExportsState is the state the exports are kept in on the bus; MountsState the `mounts` modules' state,
|
|
// which says what each node wants of which machine.
|
|
const (
|
|
ExportsState = "exports"
|
|
MountsState = "mounts.shares"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 {
|
|
if os.Args[1] != "exports" || len(os.Args) != 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: nfs-server [exports]")
|
|
os.Exit(2)
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
|
|
defer stop()
|
|
if err := Here().RunExports(ctx); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
m := Here()
|
|
go ShareExports(m)
|
|
if err := stdio.Serve("", Tools(m)); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ShareExports puts the exports in the module's state every Every, for as long as the bundle runs, so a
|
|
// consumer's module and a view read them from the bus rather than from this machine.
|
|
func ShareExports(m *Machine) {
|
|
machine := os.Getenv("MESH_NODE")
|
|
if machine == "" {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: the runtime gave no machine name (MESH_NODE); the exports are not shared")
|
|
return
|
|
}
|
|
last := ""
|
|
put := func() {
|
|
m.TakeWants(machine)
|
|
_, err := stdio.State(ExportsState).Put(machine, m.Exports())
|
|
said := "put"
|
|
if err != nil {
|
|
said = err.Error()
|
|
}
|
|
if said != last {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: exports state:", said)
|
|
}
|
|
last = said
|
|
}
|
|
// The SDK reaches the bus once Serve runs.
|
|
time.Sleep(2 * time.Second)
|
|
m.TakeBackup(machine)
|
|
put()
|
|
backup := time.Tick(BackupEvery)
|
|
every := time.Tick(Every)
|
|
for {
|
|
select {
|
|
case <-backup:
|
|
m.TakeBackup(machine)
|
|
case <-every:
|
|
put()
|
|
}
|
|
}
|
|
}
|
|
|
|
// askBus calls a seat's verb over the bus as this module, through the runtime.
|
|
func askBus(key string, body any) (json.RawMessage, error) {
|
|
return stdio.Ask(key, body)
|
|
}
|
|
|
|
// TakeWants reads every node's `mounts` state from the bus and writes what is asked of this machine to
|
|
// the wants file, for the root process. When the bus does not answer, the last wants stay.
|
|
func (m *Machine) TakeWants(machine string) {
|
|
state := stdio.State(MountsState)
|
|
keys, err := state.Keys()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: the mounts modules' state could not be listed:", err)
|
|
return
|
|
}
|
|
raw := map[string][]byte{}
|
|
for _, k := range keys {
|
|
e, err := state.Get(k)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: the mounts state of", k, "could not be read:", err)
|
|
return
|
|
}
|
|
if e != nil {
|
|
raw[k] = e.Value
|
|
}
|
|
}
|
|
wants, notes := WantsFromStates(machine, raw)
|
|
for _, n := range notes {
|
|
fmt.Fprintln(os.Stderr, "nfs-server:", n)
|
|
}
|
|
text, _ := json.MarshalIndent(WantsFromBus{Looked: m.Now().UTC().Format(time.RFC3339), Wants: wants}, "", " ")
|
|
have, _ := m.Read(WantsFile)
|
|
if wantsOf(have) == wantsOf(string(text)) {
|
|
return
|
|
}
|
|
if err := m.Write(WantsFile, string(text)+"\n"); err != nil {
|
|
fmt.Fprintln(os.Stderr, "nfs-server: the wants could not be written:", err)
|
|
}
|
|
}
|
|
|
|
// wantsOf is a wants file without its time, to compare two.
|
|
func wantsOf(text string) string {
|
|
var w WantsFromBus
|
|
if json.Unmarshal([]byte(text), &w) != nil {
|
|
return text
|
|
}
|
|
raw, _ := json.Marshal(w.Wants)
|
|
return string(raw)
|
|
}
|
|
|
|
// WantsFromStates is what each node's `mounts` state asks of this machine: its wants whose server is this
|
|
// machine, keyed by the node. A state that cannot be read is said and skipped.
|
|
func WantsFromStates(machine string, states map[string][]byte) (map[string][]Want, []string) {
|
|
out := map[string][]Want{}
|
|
var notes []string
|
|
keys := make([]string, 0, len(states))
|
|
for k := range states {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, node := range keys {
|
|
var v struct {
|
|
Wants []struct {
|
|
Server string `json:"server"`
|
|
Share string `json:"share"`
|
|
Access string `json:"access"`
|
|
} `json:"wants"`
|
|
}
|
|
if err := json.Unmarshal(states[node], &v); err != nil {
|
|
notes = append(notes, fmt.Sprintf("the mounts state of %s is not one this module reads: %v", node, err))
|
|
continue
|
|
}
|
|
for _, w := range v.Wants {
|
|
if w.Server != machine || !validName(w.Share) {
|
|
continue
|
|
}
|
|
access := "rw"
|
|
if w.Access == "ro" {
|
|
access = "ro"
|
|
}
|
|
out[node] = append(out[node], Want{Share: w.Share, Access: access})
|
|
}
|
|
}
|
|
return out, notes
|
|
}
|