Files
mesh-catalog/modules/mounts/cmd/mounts/units.go
T

180 lines
6.6 KiB
Go

package main
// The unit pair for one share (novox/hq ADR 0263 rule 4, to-be 52 §2): a `.mount` that is never enabled
// and an `.automount` that is. Nothing is mounted at boot and nothing can fail a boot: the mount is made
// at first use and released after ten idle minutes, and a mount point is always an autofs mount, so
// nothing writes into the empty directory under it.
import (
"fmt"
"strconv"
"strings"
)
// Marker opens every unit file this module writes. A unit file without it is not the module's, and the
// module never writes over, stops or removes one.
const Marker = "# Written by the mesh (module mounts, novox/hq ADR 0263)."
// Timing, in seconds.
const (
IdleSeconds = 600 // released after ten idle minutes
MountSeconds = 30 // a share of a node
OccasionalSeconds = 10 // a source that comes and goes
)
// EscapePath is `systemd-escape --path`: the name systemd gives the units for a mount point.
func EscapePath(p string) string {
p = strings.Trim(p, "/")
if p == "" {
return "-"
}
var b strings.Builder
for i := 0; i < len(p); i++ {
c := p[i]
switch {
case c == '/':
b.WriteByte('-')
case c == '.' && i == 0:
fmt.Fprintf(&b, `\x%02x`, c)
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == ':', c == '_', c == '.':
b.WriteByte(c)
default:
fmt.Fprintf(&b, `\x%02x`, c)
}
}
return b.String()
}
// Pair is one share's two units, as written.
type Pair struct {
Name string `json:"name"`
Kind string `json:"kind"` // nfs, smb or disk
Occasional bool `json:"occasional"`
Mountpoint string `json:"mountpoint"`
What string `json:"what"`
// Probe is what is tried to tell whether the source is there: host:port, or a device path.
Probe string `json:"probe"`
MountUnit string `json:"mount_unit"`
AutoUnit string `json:"automount_unit"`
Mount string `json:"-"`
Automount string `json:"-"`
// Credentials is the path of the source's credentials file (SMB), written beside the units.
Credentials string `json:"credentials,omitempty"`
}
// ServerShare is one share as the sharing machine says it exports it (its `node-nfs-server.exports`).
type ServerShare struct {
Name string `json:"name"`
Path string `json:"path"`
Access string `json:"access"`
UID int `json:"uid"`
GID int `json:"gid"`
Clients string `json:"clients,omitempty"`
Live bool `json:"live"`
// To is each node the server exports the share to, at the access it decided for that node.
To []ShareClient `json:"to,omitempty"`
}
// ShareClient is one node a share is exported to, as the server decided it.
type ShareClient struct {
Node string `json:"node"`
Address string `json:"address"`
Access string `json:"access"`
}
// ForNode is the share as the server exports it to one node: its access is the lower of the share's own
// and the node's, and false when the server does not export it to that node at all.
func (e ServerShare) ForNode(node string) (ServerShare, bool) {
for _, c := range e.To {
if c.Node == node {
if c.Access == "ro" || e.Access == "ro" {
e.Access = "ro"
}
return e, true
}
}
return e, false
}
func header(name, kind string) string {
return Marker + "\n# The " + kind + " " + name + ". Replaced whenever the module's settings or the sharing machine's\n" +
"# answer change, and removed when nothing asks for it; edit nothing here: change the module's settings.\n"
}
// NFSPair is the pair for a share of a node: NFS version 4.2 from the sharing machine's address on the
// private network. Read-write is mounted hard, so a write is never dropped while the server is away;
// read-only, soft, so a reader gets an error rather than waiting.
func NFSPair(w Wanted, at string, e ServerShare) Pair {
ro := w.ReadOnly || e.Access == "ro"
opts := "vers=4.2,hard,timeo=150,retrans=3,_netdev"
if ro {
opts = "ro,vers=4.2,soft,timeo=150,retrans=3,_netdev"
}
p := Pair{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, What: at + ":" + e.Path, Probe: hostPort(at, 2049)}
p.render("share", "nfs4", opts, MountSeconds, "remote-fs.target", "")
return p
}
// SMBPair is the pair for an occasional SMB source. Its login is in a file only root reads.
func SMBPair(s Source, credentials string, uid, gid int) Pair {
opts := "credentials=" + credentials + ",iocharset=utf8,_netdev,nofail"
if uid >= 0 {
opts += ",uid=" + strconv.Itoa(uid) + ",gid=" + strconv.Itoa(gid)
}
if s.ReadOnly {
opts = "ro," + opts
}
p := Pair{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint,
What: "//" + s.Host + "/" + s.Share, Probe: hostPort(s.Host, 445), Credentials: credentials}
p.render("source", "cifs", opts, OccasionalSeconds, "remote-fs.target", "")
return p
}
// DiskPair is the pair for a disk that is plugged in now and then: bound to its device, so the mount
// goes when the disk does.
func DiskPair(s Source) Pair {
dev := "/dev/disk/by-uuid/" + s.UUID
opts := "nofail"
if s.ReadOnly {
opts = "ro,nofail"
}
p := Pair{Name: s.Name, Kind: "disk", Occasional: true, Mountpoint: s.Mountpoint, What: dev, Probe: dev}
p.render("source", "", opts, OccasionalSeconds, "local-fs.target", EscapePath(dev)+".device")
return p
}
func hostPort(host string, port int) string {
if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
return "[" + host + "]:" + strconv.Itoa(port)
}
return host + ":" + strconv.Itoa(port)
}
func (p *Pair) render(kind, fstype, opts string, timeout int, target, device string) {
base := EscapePath(p.Mountpoint)
p.MountUnit, p.AutoUnit = base+".mount", base+".automount"
var m strings.Builder
m.WriteString(header(p.Name, kind))
m.WriteString("[Unit]\nDescription=" + p.Name + " (" + p.What + "), mounted by the mesh\n")
if device != "" {
m.WriteString("BindsTo=" + device + "\nAfter=" + device + "\n")
} else {
m.WriteString("Wants=network-online.target\nAfter=network-online.target\n")
}
m.WriteString("\n[Mount]\nWhat=" + p.What + "\nWhere=" + p.Mountpoint + "\n")
if fstype != "" {
m.WriteString("Type=" + fstype + "\n")
}
m.WriteString("Options=" + opts + "\nTimeoutSec=" + strconv.Itoa(timeout) + "\n")
p.Mount = m.String()
var a strings.Builder
a.WriteString(header(p.Name, kind))
// The automount is armed whether or not the source is there; only the mount waits on it, so an
// absent disk leaves an armed automount, never a failed unit.
a.WriteString("[Unit]\nDescription=" + p.Name + ", mounted by the mesh at first use\n")
a.WriteString("\n[Automount]\nWhere=" + p.Mountpoint + "\nTimeoutIdleSec=" + strconv.Itoa(IdleSeconds) + "\n")
a.WriteString("\n[Install]\nWantedBy=" + target + "\n")
p.Automount = a.String()
}