Skip to content

Commit 9392a33

Browse files
committed
docs: image store, firmware install, --net modes, snapshot/clone, e2e, display-sleep
1 parent b68eb14 commit 9392a33

1 file changed

Lines changed: 91 additions & 9 deletions

File tree

README.md

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,53 @@ disk image to ghcr; a thin Go CLI clones that image and boots VMs from it.
2020
(serial/MLB/UUID/ROM, with the guest NIC MAC set to the ROM) into a per-VM OpenCore so clones
2121
don't all boot as the shipped placeholder serial. Confirmed in-guest via `system_profiler`
2222
two clones get two distinct serials, each matching what was injected.
23+
- **Image management** (`cocoon-macos image pull|list|inspect|rm`): pulls the golden qcow2 from
24+
ghcr into a content-addressed local store — cocoon's `cloudimg` backend imported directly — so
25+
`vm run <ref>` bakes a CoW overlay on the immutable shared base. **qcow2-only** (no OCI image
26+
layers; `oras` is just the ghcr transport for the qcow2 blob).
27+
- **Networking** (`--net user|tap|cni|bridge`): user-mode SLIRP + `--ssh-port` hostfwd by default;
28+
`tap`/`bridge`/`cni` auto-create a host TAP via cocoon's network plane (imported `network/bridge`
29+
+ `network/cni`), so macOS VMs join the **same** bridge/CNI forwarding plane as cocoon's CH/FC
30+
VMs on the node. The guest MAC stays = SMBIOS ROM. Auto-create is Linux-only (CAP_NET_ADMIN);
31+
user-mode + a pre-created `--tap` work everywhere.
32+
- **Snapshot / restore / clone** (`vm snapshot|restore|clone`): offline qcow2-internal snapshots
33+
(`qemu-img snapshot`, VM stopped — `-cpu +invtsc` blocks live RAM snapshot, so this is disk-state
34+
only) and CoW clones that cold-boot a **fresh** Apple identity, so two clones never share a serial/MAC.
2335

2436
## CLI
2537

2638
```bash
2739
go build -o cocoon-macos .
2840

29-
# clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm)
30-
cocoon-macos vm run ghcr-pulled-tahoe.qcow2 \
31-
--name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios \
32-
--opencore OpenCore.qcow2 --ovmf-code OVMF_CODE_4M.fd --ovmf-vars OVMF_VARS.fd
41+
# pull the golden qcow2 from ghcr into the local store (cocoon cloudimg; ~/.cocoon-macos)
42+
cocoon-macos image pull ghcr.io/cocoonstack/cocoon-macos/tahoe:26
43+
cocoon-macos image list # also: inspect, rm
44+
45+
# install the shared OpenCore loader + OVMF firmware ONCE into <state-dir>/firmware (reused by
46+
# every VM); afterwards vm create/run default to these, so --opencore/--ovmf-* become optional
47+
cocoon-macos firmware install --opencore OpenCore.qcow2 --ovmf-code OVMF_CODE_4M.fd --ovmf-vars OVMF_VARS.fd
48+
cocoon-macos firmware list
49+
50+
# clone the golden image into a per-VM overlay and boot it (x86 Linux + /dev/kvm).
51+
# IMAGE is a store ref (above) or a direct qcow2 path; firmware defaults to the install above.
52+
cocoon-macos vm run ghcr.io/cocoonstack/cocoon-macos/tahoe:26 \
53+
--name m1 --cpus 4 --memory 8192 --ssh-port 2222 --vnc 1 --random-smbios
3354

3455
cocoon-macos vm list # JSON of all VMs
3556
cocoon-macos vm inspect m1
3657
cocoon-macos vm stop m1
3758
cocoon-macos vm rm m1
3859
# also: create (no boot), start, console
60+
61+
# networking — join the host's bridge/CNI plane instead of user-mode SLIRP (Linux):
62+
cocoon-macos vm run <IMAGE> --net bridge --bridge br0 --random-smbios … # auto-creates a TAP on br0
63+
cocoon-macos vm run <IMAGE> --net cni --random-smbios … # CNI: TAP in a netns
64+
cocoon-macos vm run <IMAGE> --net tap --tap tap0 … # use a pre-created TAP verbatim
65+
66+
# snapshot / restore / clone (VM stopped for snapshot/restore; clone gets a unique identity):
67+
cocoon-macos vm snapshot m1 --tag clean
68+
cocoon-macos vm restore m1 --tag clean # --force to stop+restore+relaunch a running VM
69+
cocoon-macos vm clone m1 -n m2 --ssh-port 2223 --random-smbios
3970
```
4071

4172
`vm run` does: `qemu-img create -b <golden> overlay.qcow2` (instant CoW clone) → copy a
@@ -53,6 +84,13 @@ Screen Sharing hangs on**. Pass `--vnc-password <≤8 chars>` to start QEMU with
5384
(set via the monitor post-launch) so Screen Sharing prompts and connects; tunnel first with
5485
`ssh -L 5901:127.0.0.1:5901 <host>`. Plain VNC clients (RealVNC/TigerVNC) work without a password.
5586

87+
**Display sleep blanks VNC.** macOS only repaints the emulated framebuffer while the display is
88+
awake; once it sleeps (~idle), VNC shows a blank **white/black** screen with just the cursor even
89+
though the guest is healthy (SSH works, WindowServer is up). It is *not* a GPU/driver problem — a
90+
mouse move repaints it, and the full Tahoe desktop renders fine (Finder/Dock/menu bar). The golden
91+
image's first-boot daemon now runs `pmset -a displaysleep 0 disablesleep 1` system-wide (covers the
92+
pre-login loginwindow) so the framebuffer stays painted; older images need a `setup`-stage rebuild.
93+
5694
## CI image pipeline (`.github/workflows/build-macos-image.yml`, `scripts/build-qemu-macos.sh`)
5795

5896
`workflow_dispatch` with `stage`:
@@ -76,7 +114,13 @@ runs in the Recovery Terminal against the installed Data volume (`dscl -f` offli
76114
Key host facts: GitHub `ubuntu-latest` exposes `/dev/kvm` (needs `chmod 666`); macOS Tahoe 26
77115
is the last Intel-supporting macOS, so this x86 path has a finite shelf life.
78116

79-
## Boot-to-desktop (WIP — blocked on the macOS 26 Setup Assistant)
117+
## Boot-to-desktop (GUI renders; auto-skip of the Setup Assistant is the remaining WIP)
118+
119+
The full Tahoe desktop **does render over VNC** — testbed-verified: boot → login window (the `cocoon`
120+
user) → type `cocoon` → Finder + Dock + menu bar + desktop widgets, all repainting normally. The
121+
earlier "white/black VNC" was purely **display sleep** (see the VNC note above), now fixed at the
122+
image level. What's left for a *fully unattended* boot-to-desktop is auto-skipping the first-run
123+
Setup Assistant + auto-login.
80124

81125
The `desktop` build stage + `provision-macos.sh` aim to make `:26` boot straight to `cocoon`'s
82126
desktop (auto-login, no Setup Assistant). The **post-SA recipe is validated** (proven on a testbed
@@ -98,9 +142,47 @@ VZ on Apple Silicon caps ~2 macOS VMs/host and can't use the App Store; QEMU + O
98142
has neither limit (at the cost of per-VM identity + Apple-ID ban risk at fleet scale). See the
99143
deep-research notes that motivated this project.
100144

145+
## E2E regression (`scripts/e2e.sh`)
146+
147+
A testbed lifecycle regression (modeled on cocoon's), two tiers:
148+
149+
- **`[DUMMY]`** — file-level lifecycle on a tiny throwaway qcow2, **no macOS boot**: image store
150+
(`pull`-less `list`/`rm`), `vm create` overlay-on-shared-base, `--random-smbios` serial/MAC
151+
uniqueness + MAC==ROM, `snapshot`/`restore` (qcow2-internal tag rollback), `clone` (CoW on the
152+
shared base + distinct identity + clone-of-clone backing), `stop` PID-reuse-safe teardown,
153+
`--net bridge` auto-TAP create + `tap_owned` + teardown-on-`rm` + user-`--tap` preservation +
154+
negative paths — with post-conditions asserting no leaked TAPs/procs and backing-chain integrity.
155+
Runs on any x86 Linux/KVM host; the `--random-smbios` rows need `root` + `nbd` + a real
156+
`CM_OPENCORE`, the `--net` rows need `root` + a test bridge (they `SKIP`, not `FAIL`, otherwise).
157+
- **`[REAL]`** (`--real`) — boots `tahoe:26` from the store, passes the OpenCore picker over the HMP
158+
monitor, asserts SSH-ready (`sw_vers` 26.x) + in-guest serial == injected. Testbed-only.
159+
160+
```bash
161+
sudo CM_BIN=./cocoon-macos CM_OPENCORE=.../OpenCore.qcow2 ./scripts/e2e.sh # [DUMMY] (30 rows)
162+
sudo CM_HOME=~/cm-demo CM_OPENCORE=... CM_OVMF_CODE=... CM_OVMF_VARS=... ./scripts/e2e.sh --real-only
163+
```
164+
165+
The `[DUMMY]` tier is the behavioral gate today's CI structurally can't give (CI is pure `go
166+
test`/`vet`/build — nothing opens `/dev/kvm` or mutates netlink); it belongs on a privileged
167+
self-hosted KVM runner. `[REAL]` stays a manual/nightly testbed run (~15GB + cold boot).
168+
169+
## Roadmap
170+
171+
- **Port cocoon's bypass/passthrough surface.** cocoon exposes extra VM hardware that cocoon-macos
172+
doesn't yet wire to QEMU; port it the same way as networking (Linux-tagged, reusing cocoon types):
173+
- **Multi-disk / data disks** — cocoon's `types.DataDiskSpec` + `StorageRole` (`data`/`cow`/…) and
174+
`--data-disk`. Maps to extra QEMU `-drive`/`-device virtio-blk` per disk (attach/detach lifecycle).
175+
- **Hardware passthrough (VFIO)** — cocoon's `extend/vfio` (`vfio.Spec`/`Attacher`, BDF/sysfs) and
176+
`cmd/vm attach`. Maps to QEMU `-device vfio-pci,host=<BDF>` for GPU/NIC/etc. passthrough.
177+
- **GUI boot-to-desktop** — finish the SA click-through (below).
178+
- **Live snapshot** is intentionally *not* on the roadmap: `-cpu +invtsc` makes the macOS guest
179+
non-migratable, so resume-from-RAM can't work; snapshot stays offline/disk-only + cold clone.
180+
101181
## Out of scope (v0.x)
102182

103-
cocoon engine integration (a `qemu` Hypervisor backend in cocoon) is a separate later phase.
104-
Per-VM SMBIOS injection (`--random-smbios`) is implemented + testbed-verified, but registering
105-
those identities with iServices/App Store is the consumer's policy concern — it needs validated
106-
(not just unique) serials and carries Apple-ID ban risk at fleet scale — so it is not done here.
183+
A `qemu` Hypervisor backend *inside* cocoon (so cocoon's own `cmd/core` dispatches macOS VMs) is a
184+
separate later phase — cocoon-macos currently **imports** cocoon's libraries (`cloudimg`, `network`,
185+
storage/CoW conventions) rather than the reverse. Per-VM SMBIOS injection (`--random-smbios`) is
186+
implemented + testbed-verified, but registering those identities with iServices/App Store is the
187+
consumer's policy concern — it needs validated (not just unique) serials and carries Apple-ID ban
188+
risk at fleet scale — so it is not done here.

0 commit comments

Comments
 (0)