A module's containers ran as whatever their image chose and shared folders were made writable by putting accounts in the folder's group (ADR 0312), which ADR 0325 supersedes. The engine now makes each declared module identity as a system account with the controller's numbers, runs a container declaring `identity` as it, and keeps each placed shared folder usable through one POSIX ACL entry per user, set through the kernel's xattrs on descriptors it opened itself. A folder holding data is kept only after the operator's warrant (the take act); a missing folder is made where its data lives and waits where its disk is not; root's data is never kept. A watch and a bounded sweep put back what an app narrows. The group grant is removed, and what it put an account in is taken back. Reuses the descriptor walks, the create act, the missing-path wait and the warrant- backed act message from the paused issue 567 branch.
196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package apply
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Whether a shared folder's disk is there (novox/hq 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 is the disk there, whatever now holds the folder (a
|
|
// mount made deeper is a placement, not a missing disk), and the record follows it.
|
|
//
|
|
// **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 mount point a ZFS dataset names (`zfs list`, when the machine has zfs),
|
|
// 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 hand `mount` of a disk that is not
|
|
// in fstab, then unmounted before the engine ever saw the folder) is not known to be missing: said here, because the
|
|
// folder would then be made on the filesystem below it.
|
|
|
|
// 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 {
|
|
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))...)
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
if recorded != "" {
|
|
return placement{Mount: have, Away: have != recorded && !mounted[recorded]}
|
|
}
|
|
for _, t := range append(MountTargets(ctx), others...) {
|
|
if atOrBelow(path, t) && t != have && atOrBelow(t, have) && !mounted[t] {
|
|
return placement{Mount: have, Away: true}
|
|
}
|
|
}
|
|
return placement{Mount: have}
|
|
}
|