Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/main.go
T

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
}