ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by its group rather than by ACL entries. The engine now makes each library's group and each module's user in its libraries' groups; keeps a library's root and requested sub-paths in its group with group write and setgid on its home, making missing sub-paths as the root is; waits for the operator's word before touching a root that holds data, and on the take act walks it by descriptor (no link, same filesystem, bounded and resumed, files with other names left); holds the containers of a library whose disk or mount is not there; and puts the tool runner's account in the group. The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
262 lines
8.7 KiB
Go
262 lines
8.7 KiB
Go
package apply
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Whether a library's disk is there (novox/hq ADR 0328 §9; first ADR 0325 §9).
|
|
//
|
|
// **The rule the engine keeps.** A folder is placed on the filesystem it is first seen on: the mount point that
|
|
// holds it — the longest mount point in /proc/self/mountinfo that is the folder or above it — is recorded beside the
|
|
// kept folder. On every later apply the folder (or, when it is absent, its nearest existing parent) is judged
|
|
// against that record: held by another mount point than the one recorded while the recorded one is not mounted is
|
|
// the disk not there. A recorded mount point that is mounted while another holds the folder is a **move**: what is
|
|
// at the path now is not what was kept (a disk mounted over a folder first seen empty below it, a mount made
|
|
// deeper), so the folder is judged afresh — kept again only if it holds nothing, else it waits for the operator's
|
|
// word as any folder holding data does (ADR 0328 §7).
|
|
//
|
|
// **On first sight, with no record,** the engine knows a disk is missing only from where disks are meant to be:
|
|
// every mount target in /etc/fstab, every `Where=` of a systemd .mount unit, every mount point a ZFS dataset names
|
|
// (`zfs list`, when the machine has zfs), every mount point this engine has seen mounted since it started, and every
|
|
// mount point recorded for another kept folder. A target that is the folder or above it, lies below the mount point
|
|
// that holds what is there now, and is not mounted, is the disk not there. Otherwise the filesystem that holds what
|
|
// is there is the placed one. A mount nothing names in those places (a pool not imported, whose datasets `zfs list`
|
|
// does not show; a hand `mount` never seen) is not known to be missing on first sight: then a folder first seen
|
|
// empty below it is kept as empty, and the move above takes it back the moment the disk is there.
|
|
|
|
// MountPoints is every mount point this machine has mounted now, from /proc/self/mountinfo. A variable so a test
|
|
// can stand in for mounts it cannot make.
|
|
var MountPoints = func() []string {
|
|
return readMountPoints()
|
|
}
|
|
|
|
// mountHistory is every mount point this engine has seen mounted since it started (§9's "mountinfo history").
|
|
var mountHistory = struct {
|
|
sync.Mutex
|
|
seen map[string]bool
|
|
}{seen: map[string]bool{}}
|
|
|
|
// sawMounted adds points to the history, and is the history: every mount point seen, never "/".
|
|
func sawMounted(points []string) []string {
|
|
mountHistory.Lock()
|
|
defer mountHistory.Unlock()
|
|
for _, p := range points {
|
|
if p != "/" {
|
|
mountHistory.seen[p] = true
|
|
}
|
|
}
|
|
out := make([]string, 0, len(mountHistory.seen))
|
|
for p := range mountHistory.seen {
|
|
out = append(out, p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func readMountPoints() []string {
|
|
raw, err := os.ReadFile("/proc/self/mountinfo")
|
|
if err != nil {
|
|
return []string{"/"}
|
|
}
|
|
return mountinfoPoints(string(raw))
|
|
}
|
|
|
|
// MountTargets is every place a disk is meant to be mounted: /etc/fstab's targets and ZFS datasets' mount points,
|
|
// read at most once a minute — every look judges every access by them. A variable so a test can stand in.
|
|
var MountTargets = func(ctx context.Context) []string {
|
|
targets.Lock()
|
|
defer targets.Unlock()
|
|
if targets.read.IsZero() || time.Since(targets.read) > time.Minute {
|
|
targets.list, targets.read = readMountTargets(ctx), time.Now()
|
|
}
|
|
return append([]string(nil), targets.list...)
|
|
}
|
|
|
|
var targets struct {
|
|
sync.Mutex
|
|
list []string
|
|
read time.Time
|
|
}
|
|
|
|
func readMountTargets(ctx context.Context) []string {
|
|
var out []string
|
|
if raw, err := os.ReadFile("/etc/fstab"); err == nil {
|
|
out = append(out, fstabTargets(string(raw))...)
|
|
}
|
|
out = append(out, mountUnitTargets(systemdUnitDirs)...)
|
|
if _, err := exec.LookPath("zfs"); err == nil {
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
if raw, err := exec.CommandContext(ctx, "zfs", "list", "-H", "-o", "mountpoint").Output(); err == nil {
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if p := strings.TrimSpace(line); strings.HasPrefix(p, "/") {
|
|
out = append(out, filepath.Clean(p))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// systemdUnitDirs are where systemd keeps the units a machine mounts by.
|
|
var systemdUnitDirs = []string{"/etc/systemd/system", "/run/systemd/system", "/run/systemd/generator",
|
|
"/usr/lib/systemd/system", "/lib/systemd/system"}
|
|
|
|
// mountUnitTargets reads the `Where=` of every .mount unit in dirs.
|
|
func mountUnitTargets(dirs []string) []string {
|
|
var out []string
|
|
for _, dir := range dirs {
|
|
names, err := filepath.Glob(filepath.Join(dir, "*.mount"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, name := range names {
|
|
raw, err := os.ReadFile(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if v, ok := strings.CutPrefix(strings.TrimSpace(line), "Where="); ok && strings.HasPrefix(v, "/") {
|
|
out = append(out, filepath.Clean(strings.TrimSpace(v)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mountinfoPoints reads the mount point of each line of mountinfo, its fifth field, with the kernel's octal escapes
|
|
// undone.
|
|
func mountinfoPoints(raw string) []string {
|
|
var out []string
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 5 {
|
|
continue
|
|
}
|
|
out = append(out, filepath.Clean(unescapeMount(fields[4])))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mountSourceOf is the source and filesystem type of the mount at point, from mountinfo: for a ZFS dataset, its
|
|
// name. Empty when the machine says nothing of it.
|
|
func mountSourceOf(point string) (source, fstype string) { return MountSource(point) }
|
|
|
|
// MountSource is mountSourceOf as the machine answers it. A variable so a test can stand in for another filesystem
|
|
// mounted at the same point.
|
|
var MountSource = func(point string) (source, fstype string) {
|
|
raw, err := os.ReadFile("/proc/self/mountinfo")
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
before, after, ok := strings.Cut(line, " - ")
|
|
fields, rest := strings.Fields(before), strings.Fields(after)
|
|
if !ok || len(fields) < 5 || len(rest) < 2 {
|
|
continue
|
|
}
|
|
if filepath.Clean(unescapeMount(fields[4])) == point {
|
|
source, fstype = unescapeMount(rest[1]), rest[0]
|
|
}
|
|
}
|
|
return source, fstype
|
|
}
|
|
|
|
// fstabTargets reads the second field of each fstab line that mounts somewhere: not swap, not a comment.
|
|
func fstabTargets(raw string) []string {
|
|
var out []string
|
|
sc := bufio.NewScanner(strings.NewReader(raw))
|
|
for sc.Scan() {
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) < 2 || strings.HasPrefix(fields[0], "#") || !strings.HasPrefix(fields[1], "/") {
|
|
continue
|
|
}
|
|
out = append(out, filepath.Clean(unescapeMount(fields[1])))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unescapeMount undoes the kernel's and fstab's octal escapes: \040 is a space.
|
|
func unescapeMount(s string) string {
|
|
if !strings.Contains(s, `\`) {
|
|
return s
|
|
}
|
|
var b strings.Builder
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\\' && i+4 <= len(s) {
|
|
if v, ok := octal3(s[i+1 : i+4]); ok {
|
|
b.WriteByte(v)
|
|
i += 3
|
|
continue
|
|
}
|
|
}
|
|
b.WriteByte(s[i])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func octal3(s string) (byte, bool) {
|
|
if len(s) != 3 {
|
|
return 0, false
|
|
}
|
|
v := 0
|
|
for _, c := range s {
|
|
if c < '0' || c > '7' {
|
|
return 0, false
|
|
}
|
|
v = v*8 + int(c-'0')
|
|
}
|
|
return byte(v), v < 256
|
|
}
|
|
|
|
// mountHolding is the longest of points that is path or above it: the mount point that holds it.
|
|
func mountHolding(path string, points []string) string {
|
|
best := "/"
|
|
for _, p := range points {
|
|
if atOrBelow(path, p) && len(p) > len(best) {
|
|
best = p
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// placement is the judgement of where a folder's data lives.
|
|
type placement struct {
|
|
// Mount is the mount point that holds what is there: the folder, or its nearest existing parent.
|
|
Mount string
|
|
// Away is that the disk the folder was placed on is not there.
|
|
Away bool
|
|
// Moved is that the recorded mount point is mounted and another holds the folder: what is there is not what
|
|
// was kept.
|
|
Moved bool
|
|
}
|
|
|
|
// placedOn judges path, of which existing is what is there (the folder itself, or its nearest existing parent),
|
|
// against what was recorded and where disks are meant to be.
|
|
func placedOn(ctx context.Context, path, existing, recorded string, others []string) placement {
|
|
points := MountPoints()
|
|
mounted := map[string]bool{}
|
|
for _, p := range points {
|
|
mounted[p] = true
|
|
}
|
|
have := mountHolding(existing, points)
|
|
seen := sawMounted(points)
|
|
if recorded != "" {
|
|
return placement{Mount: have, Away: have != recorded && !mounted[recorded],
|
|
Moved: have != recorded && mounted[recorded]}
|
|
}
|
|
for _, t := range append(append(MountTargets(ctx), others...), seen...) {
|
|
if atOrBelow(path, t) && t != have && atOrBelow(t, have) && !mounted[t] {
|
|
return placement{Mount: have, Away: true}
|
|
}
|
|
}
|
|
return placement{Mount: have}
|
|
}
|