77 "strings"
88
99 "github.com/frappe/boat/internal/paths"
10+ "github.com/frappe/boat/internal/thinpool"
1011)
1112
1213// preflight is steps 0 and 0b: refuse before anything is laid down.
@@ -21,21 +22,40 @@ func (provisioning *provisioning) preflight(ctx context.Context) error {
2122// message so the operator knows to click Sync to Server before retrying — image
2223// sync is multi-minute and is deliberately not auto-triggered from provision.
2324//
24- // The probe stays even when the rootfs comes from a snapshot (the clone path):
25- // the KERNEL is hard-linked out of the same image directory regardless of where
26- // the rootfs blocks come from.
27- //
28- // The Python stats the file in process; here it is `test -f` with no sudo, which
29- // is the same reach — /var/lib/atlas/images is root-owned and world-readable, and
30- // this verb runs as root.
25+ // It probes the image's BASE LV DEVICE NODE, not the rootfs FILE. The rootfs file
26+ // lives in /var/lib/atlas/images/<name>/ which sync.go / bootstrap.go create 0700
27+ // root-owned; provision-vm runs as the boat DAEMON user, so a bare `test -f` there
28+ // reads the root-only dir as "not present" and fails a fully-synced host (the
29+ // boat-user-vs-root idiom trap, spec/33 §3.4), while sudo would need a new grant.
30+ // The base LV node (/dev/atlas/atlas-image-<name>) is a world-visible symlink AND
31+ // is the durable artifact every per-VM disk snapshots from, so `test -b` on it —
32+ // no sudo, no new grant — is both reachable and a truer "is this image synced?"
33+ // check. The clone path relies on the same image dir for the hard-linked kernel;
34+ // a truly-absent image dir there fails loud at the link step.
3135func (provisioning * provisioning ) requireImage (ctx context.Context ) error {
32- rootfsImage := provisioning .imageDirectory + "/" + provisioning .params .RootfsFilename
33- if provisioning .commands .OK (ctx , "test -f {}" , rootfsImage ) {
36+ // The ORIGIN the per-VM disk will snapshot from: the snapshot LV on the clone
37+ // path, else the base image LV — the same selection (and the same distinct
38+ // messages) resolveOrigin makes, so a clone never references the base image LV,
39+ // and this refuses BEFORE anything is laid down (resolveOrigin runs after the VM
40+ // dir exists). Probing the LV DEVICE NODE with `test -b` (a world-visible symlink)
41+ // needs no sudo and no new grant — unlike a `test -f` of the rootfs file, whose
42+ // 0700 root-owned dir the boat daemon user cannot stat.
43+ if provisioning .params .SnapshotRootfsPath != "" {
44+ origin := thinpool .NameFromDevice (provisioning .params .SnapshotRootfsPath )
45+ if provisioning .commands .OK (ctx , "test -b {}" , thinpool .DevicePath (origin )) {
46+ return nil
47+ }
48+ return fmt .Errorf (
49+ "snapshot LV not found: %s (from %s)" , origin , provisioning .params .SnapshotRootfsPath ,
50+ )
51+ }
52+ node := thinpool .DevicePath (thinpool .BaseImageLV (provisioning .params .ImageName ))
53+ if provisioning .commands .OK (ctx , "test -b {}" , node ) {
3454 return nil
3555 }
3656 return fmt .Errorf (
3757 "image '%s' not present on server (missing %s); run Sync to Server first" ,
38- provisioning .params .ImageName , rootfsImage ,
58+ provisioning .params .ImageName , node ,
3959 )
4060}
4161
0 commit comments