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() }