mesh/merge-gate pass: builds mounts → shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
The autofs line an armed automount leaves in mountinfo made List say "mounted" while nothing was mounted. One judgement, Mounted.IsAutomount, now serves List, the look and the checks before a mount.
215 lines
7.1 KiB
Go
215 lines
7.1 KiB
Go
package main
|
||
|
||
// What the module may not mount over (the review of 2026-10-08, findings 4a–4d): the mesh's own data on
|
||
// this machine, a mount point that something else uses, a folder that holds files, and a share this
|
||
// machine serves itself. And what the bundle leaves for the process so that it can tell: the paths the
|
||
// mesh keeps data in on this machine, from the controller's `data` verb over the bus.
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
// DataFile is what the bundle left from mesh-controller.data for this machine.
|
||
const DataFile = BusDir + "/data.json"
|
||
|
||
// DataPaths is where the mesh keeps data on this machine, as its controller says.
|
||
type DataPaths struct {
|
||
Machine string `json:"machine"`
|
||
Asked string `json:"asked"`
|
||
Paths []string `json:"paths"`
|
||
// Error is why the last ask got no answer; the paths are then the last answer's, or none.
|
||
Error string `json:"error,omitempty"`
|
||
Answered string `json:"answered,omitempty"`
|
||
}
|
||
|
||
// Want is one share this machine asks of one server, put on the bus so that the server exports the share
|
||
// to this machine and to nobody who does not ask (finding 5). The server decides the access it gives; this
|
||
// is only what is asked.
|
||
type Want struct {
|
||
Server string `json:"server"`
|
||
Share string `json:"share"`
|
||
Access string `json:"access"`
|
||
}
|
||
|
||
// Wants are what a config asks of its server, by share name.
|
||
func (c Config) Wants() []Want {
|
||
out := []Want{}
|
||
server := c.ServerNode()
|
||
if server == "" {
|
||
return out
|
||
}
|
||
for _, w := range c.Shares {
|
||
access := "rw"
|
||
if w.ReadOnly {
|
||
access = "ro"
|
||
}
|
||
out = append(out, Want{Server: server, Share: w.Name, Access: access})
|
||
}
|
||
sortWants(out)
|
||
return out
|
||
}
|
||
|
||
func sortWants(w []Want) {
|
||
for i := 1; i < len(w); i++ {
|
||
for j := i; j > 0 && w[j].Share < w[j-1].Share; j-- {
|
||
w[j], w[j-1] = w[j-1], w[j]
|
||
}
|
||
}
|
||
}
|
||
|
||
// DataFrom reads the paths out of mesh-controller.data's answer: `answer.data`, `data`, or the same as
|
||
// text in `output`. An answer with no data list is an error, never an empty list: an empty list would
|
||
// let every mount point through.
|
||
func DataFrom(raw json.RawMessage) ([]string, error) {
|
||
type item struct {
|
||
Path string `json:"path"`
|
||
}
|
||
var whole struct {
|
||
Answer *struct {
|
||
Data *[]item `json:"data"`
|
||
} `json:"answer"`
|
||
Data *[]item `json:"data"`
|
||
Output string `json:"output"`
|
||
}
|
||
if err := json.Unmarshal(raw, &whole); err != nil {
|
||
return nil, fmt.Errorf("the controller's data answer is not JSON: %v", err)
|
||
}
|
||
var items *[]item
|
||
switch {
|
||
case whole.Answer != nil && whole.Answer.Data != nil:
|
||
items = whole.Answer.Data
|
||
case whole.Data != nil:
|
||
items = whole.Data
|
||
case whole.Output != "":
|
||
var inner struct {
|
||
Data *[]item `json:"data"`
|
||
}
|
||
if err := json.Unmarshal([]byte(whole.Output), &inner); err == nil && inner.Data != nil {
|
||
items = inner.Data
|
||
}
|
||
}
|
||
if items == nil {
|
||
return nil, errors.New("the controller's data answer names no data list")
|
||
}
|
||
paths := []string{}
|
||
for _, it := range *items {
|
||
if strings.HasPrefix(it.Path, "/") {
|
||
paths = append(paths, filepath.Clean(it.Path))
|
||
}
|
||
}
|
||
return paths, nil
|
||
}
|
||
|
||
// nearData refuses a mount point at, under or above a path the mesh keeps data in here: at or under, the
|
||
// mount would hide the data; above, it would hide it too, and everything beside it.
|
||
func nearData(point string, paths []string) error {
|
||
for _, d := range paths {
|
||
switch {
|
||
case point == d:
|
||
return fmt.Errorf("the mount point %s is at %s, where the mesh keeps data on this machine; it is never mounted over", point, d)
|
||
case strings.HasPrefix(point, d+"/"):
|
||
return fmt.Errorf("the mount point %s is under %s, where the mesh keeps data on this machine; it is never mounted over", point, d)
|
||
case strings.HasPrefix(d, point+"/"):
|
||
return fmt.Errorf("the mount point %s is above %s, where the mesh keeps data on this machine; mounting there would hide it", point, d)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// sourceIs says whether what is mounted at a point is a pair's own source. For a network share the source
|
||
// is compared as the kernel writes it; for a disk, the device its by-uuid link resolves to.
|
||
func (m *Machine) sourceIs(kind, what string, mt Mounted) bool {
|
||
switch kind {
|
||
case "nfs":
|
||
return strings.HasPrefix(mt.Type, "nfs") && strings.TrimSuffix(mt.Source, "/") == strings.TrimSuffix(what, "/")
|
||
case "smb":
|
||
return (mt.Type == "cifs" || mt.Type == "smb3") && strings.EqualFold(strings.TrimSuffix(mt.Source, "/"), strings.TrimSuffix(what, "/"))
|
||
case "disk":
|
||
dev, err := filepath.EvalSymlinks(filepath.Join(m.Root, what))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
src, err := filepath.EvalSymlinks(filepath.Join(m.Root, mt.Source))
|
||
return err == nil && src == dev
|
||
}
|
||
return false
|
||
}
|
||
|
||
// kindOf is a unit's kind from what it mounts.
|
||
func kindOf(what, fstype string) string {
|
||
switch {
|
||
case strings.HasPrefix(fstype, "nfs"):
|
||
return "nfs"
|
||
case fstype == "cifs" || strings.HasPrefix(what, "//"):
|
||
return "smb"
|
||
case strings.HasPrefix(what, "/dev/"):
|
||
return "disk"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// notOwnSource says why a mounted filesystem at a point is not the source a unit of the module mounts
|
||
// there, or nothing when it is, or nothing is mounted there but an automount.
|
||
func (m *Machine) notOwnSource(kind, what, point string, mounted map[string]Mounted) error {
|
||
mt, there := mounted[point]
|
||
if !there || mt.IsAutomount() {
|
||
return nil
|
||
}
|
||
if kind != "" && m.sourceIs(kind, what, mt) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is mounted at %s (%s), which is not what the module mounts there (%s)", mt.Source, point, mt.Type, what)
|
||
}
|
||
|
||
// mountpointFree refuses a mount point another mount uses, or a folder that holds files. A point in
|
||
// mountinfo is never opened, so a network server that is away cannot hold the look.
|
||
func (m *Machine) mountpointFree(p Pair, own map[string]bool, mounted map[string]Mounted) error {
|
||
ours := own[p.MountUnit] && own[p.AutoUnit]
|
||
if mt, there := mounted[p.Mountpoint]; there {
|
||
if mt.IsAutomount() {
|
||
if ours {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is already an automount that is not the module's (%s)", p.Mountpoint, mt.Source)
|
||
}
|
||
if ours && m.sourceIs(p.Kind, p.What, mt) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is already mounted: %s (%s) is there, so this share is not mounted over it", p.Mountpoint, mt.Source, mt.Type)
|
||
}
|
||
dir, err := os.Open(filepath.Join(m.Root, p.Mountpoint))
|
||
if err != nil {
|
||
return nil // missing: it is made
|
||
}
|
||
defer dir.Close()
|
||
info, err := dir.Stat()
|
||
if err != nil {
|
||
return fmt.Errorf("%s cannot be looked at: %v", p.Mountpoint, err)
|
||
}
|
||
if !info.IsDir() {
|
||
return fmt.Errorf("%s is a file, not a folder", p.Mountpoint)
|
||
}
|
||
if names, err := dir.Readdirnames(1); err == nil && len(names) > 0 {
|
||
return fmt.Errorf("%s is not empty (it holds %s); a mount would hide what is in it, so it is not mounted there", p.Mountpoint, names[0])
|
||
} else if err != nil && err != io.EOF {
|
||
return fmt.Errorf("%s cannot be read: %v", p.Mountpoint, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// unitField reads one Key= of a unit file.
|
||
func unitField(text, key string) string {
|
||
for _, line := range strings.Split(text, "\n") {
|
||
if v, ok := strings.CutPrefix(strings.TrimSpace(line), key+"="); ok {
|
||
return strings.TrimSpace(v)
|
||
}
|
||
}
|
||
return ""
|
||
}
|