mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
219 lines
6.0 KiB
Go
219 lines
6.0 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"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
// readingsOf reads disk-load's readings of one machine's data items, every key `<node>.<module>.<item>`.
|
|
func readingsOf(node string) ([]health.Reading, error) {
|
|
state := stdio.State(health.ReadingsRead)
|
|
names, err := state.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []health.Reading
|
|
for _, k := range names {
|
|
if !strings.HasPrefix(k, node+".") {
|
|
continue
|
|
}
|
|
e, err := state.Get(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
continue
|
|
}
|
|
if r, err := health.DecodeReading(e.Value); err == nil {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// seatHolderKeys reads the placement seat's state of who holds which seat, every key.
|
|
func seatHolderKeys() (map[string][]byte, error) {
|
|
state := stdio.State(delivery.SeatHoldersRead)
|
|
names, err := state.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys := map[string][]byte{}
|
|
for _, k := range names {
|
|
e, err := state.Get(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e != nil {
|
|
keys[k] = e.Value
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// 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
|
|
}
|