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.Type == "autofs" {
|
||
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.Type == "autofs" {
|
||
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 ""
|
||
}
|