tstest: move macOS VM storage to ~/.cache/tailscale/vmtest/macos/

Replace ~/VM.bundle (not a macOS convention, just an arbitrary choice
from the original tailmac code) with ~/.cache/tailscale/vmtest/macos/.

Default VM name is now "macos-base" instead of "llmacstation" (which
was an unrelated project). Remove all llmacstation references from
vmtest code.
This commit is contained in:
Brad Fitzpatrick 2026-04-10 08:53:36 -07:00
parent 3e518d50ae
commit 647bab9acd
5 changed files with 38 additions and 43 deletions

View File

@ -10,8 +10,8 @@
//
// go run ./tstest/build-macos-base-vm
//
// The VM is created at ~/VM.bundle/llmacstation/ and can be used by vmtest
// tests that include macOS nodes. The IPSW is cached at ~/VM.bundle/RestoreImage.ipsw.
// The VM is created at ~/.cache/tailscale/vmtest/macos/<name>/ and can be used
// by vmtest tests that include macOS nodes. The IPSW is cached alongside it.
//
// This only runs on macOS arm64 (Apple Silicon) and requires the Virtualization
// framework entitlement, so the helper Swift binary is compiled and ad-hoc signed
@ -31,7 +31,7 @@ import (
)
var (
vmName = flag.String("name", "llmacstation", "VM name (directory under ~/VM.bundle/)")
vmName = flag.String("name", "macos-base", "VM name (directory under ~/.cache/tailscale/vmtest/macos/)")
rebuild = flag.Bool("rebuild", false, "delete existing VM and recreate it")
)
@ -45,7 +45,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
bundleDir := filepath.Join(home, "VM.bundle")
bundleDir := filepath.Join(home, ".cache", "tailscale", "vmtest", "macos")
vmDir := filepath.Join(bundleDir, *vmName)
ipswPath := filepath.Join(bundleDir, "RestoreImage.ipsw")

View File

@ -13,6 +13,15 @@ import (
"time"
)
// macosVMDir returns the base directory for macOS VM images.
func macosVMDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".cache", "tailscale", "vmtest", "macos"), nil
}
// ensureTailMac locates the pre-built tailmac Host.app binary.
// Users must build it first with "make all" in tstest/tailmac/.
func (e *Env) ensureTailMac() error {
@ -28,37 +37,27 @@ func (e *Env) ensureTailMac() error {
return nil
}
// tailmacConfig is the JSON config format for tailmac VMs.
// It uses the field names from the llmacstation Config type (vmName, mac, etc.)
// since the base VM images are created by llmacstation.
type tailmacConfig struct {
VMName string `json:"vmName"`
MemorySize uint64 `json:"memorySize"`
DiskSize int64 `json:"diskSize"`
Mac string `json:"mac"`
Hostname string `json:"hostname"`
}
// startTailMacVM clones the base macOS VM, configures it for this test's
// vnet, and launches it headlessly via the tailmac Host.app.
//
// The base VM must have been created by llmacstation with a single-NIC config.
// The headless Host.app matches this by using only the socket-based NIC,
// connecting it directly to vnet's dgram socket.
// The base VM is created by "go run ./tstest/build-macos-base-vm". The
// headless Host.app uses a single socket-based NIC (matching the base VM's
// config) connected directly to vnet's dgram socket.
func (e *Env) startTailMacVM(n *Node) error {
baseID := *macosVMID
testID := fmt.Sprintf("vmtest-%s-%d", n.name, os.Getpid())
// Clone the base VM (APFS CoW makes this nearly instant).
home, err := os.UserHomeDir()
vmBase, err := macosVMDir()
if err != nil {
return fmt.Errorf("getting home dir: %w", err)
return err
}
baseDir := filepath.Join(home, "VM.bundle", baseID)
baseDir := filepath.Join(vmBase, baseID)
if _, err := os.Stat(baseDir); err != nil {
return fmt.Errorf("base macOS VM %q not found at %s; create with 'llmacstation create --name %s'", baseID, baseDir, baseID)
return fmt.Errorf("base macOS VM %q not found at %s; create with: go run ./tstest/build-macos-base-vm", baseID, baseDir)
}
cloneDir := filepath.Join(home, "VM.bundle", testID)
// Clone the base VM (APFS CoW via cp -c makes this nearly instant).
cloneDir := filepath.Join(vmBase, testID)
e.t.Logf("[%s] cloning macOS VM %s -> %s", n.name, baseID, testID)
if out, err := exec.Command("cp", "-c", "-r", baseDir, cloneDir).CombinedOutput(); err != nil {
if out2, err2 := exec.Command("cp", "-r", baseDir, cloneDir).CombinedOutput(); err2 != nil {
@ -73,19 +72,15 @@ func (e *Env) startTailMacVM(n *Node) error {
// The serverSocket field tells the Swift code where to connect the VM's NIC.
mac := n.vnetNode.NICMac(0)
cfg := struct {
VMName string `json:"vmName"`
MemorySize uint64 `json:"memorySize"`
DiskSize int64 `json:"diskSize"`
Mac string `json:"mac"`
Hostname string `json:"hostname"`
VMid string `json:"vmID"`
ServerSocket string `json:"serverSocket"`
MemorySize uint64 `json:"memorySize"`
Mac string `json:"mac"`
}{
VMName: testID,
MemorySize: 8 * 1024 * 1024 * 1024, // 8GB to match base VM
DiskSize: 72 * 1024 * 1024 * 1024,
Mac: mac.String(),
Hostname: testID,
VMid: testID,
ServerSocket: e.dgramSockAddr,
MemorySize: 8 * 1024 * 1024 * 1024, // 8GB, matching base VM
Mac: mac.String(),
}
cfgData, _ := json.MarshalIndent(cfg, "", " ")
cfgPath := filepath.Join(cloneDir, "config.json")
@ -95,7 +90,7 @@ func (e *Env) startTailMacVM(n *Node) error {
e.t.Logf("[%s] macOS VM config: mac=%s, socket=%s", n.name, mac, e.dgramSockAddr)
// Launch Host.app in headless mode. Headless mode uses a single NIC
// (matching the llmacstation VM config) connected to the dgram socket.
// connected to the vnet dgram socket.
hostBin := filepath.Join(e.tailmacDir, "Host.app", "Contents", "MacOS", "Host")
args := []string{"run", "--id", testID, "--headless"}

View File

@ -47,7 +47,7 @@ import (
var (
runVMTests = flag.Bool("run-vm-tests", false, "run tests that require VMs with KVM")
verboseVMDebug = flag.Bool("verbose-vm-debug", false, "enable verbose debug logging for VM tests")
macosVMID = flag.String("macos-vm-id", "llmacstation", "base macOS VM identifier in ~/VM.bundle/ for macOS VM tests")
macosVMID = flag.String("macos-vm-id", "macos-base", "base macOS VM name under ~/.cache/tailscale/vmtest/macos/")
)
// Env is a test environment that manages virtual networks and QEMU VMs.
@ -219,7 +219,7 @@ func (e *Env) Start() {
if err != nil {
t.Fatalf("getting home dir: %v", err)
}
vmDir := filepath.Join(home, "VM.bundle", *macosVMID)
vmDir := filepath.Join(home, ".cache", "tailscale", "vmtest", "macos", *macosVMID)
if _, err := os.Stat(vmDir); err != nil {
t.Skipf("macOS base VM %q not found at %s; create it with:\n\tgo run ./tstest/build-macos-base-vm",
*macosVMID, vmDir)

View File

@ -103,10 +103,10 @@ class Config: Codable {
}
// The VM Bundle URL holds the restore image and a set of VM images
// By default, VM's are persisted at ~/VM.bundle
// The VM Bundle URL holds the restore image and a set of VM images.
// VMs are stored under ~/.cache/tailscale/vmtest/macos/.
var vmBundleURL: URL = {
let vmBundlePath = NSHomeDirectory() + "/VM.bundle/"
let vmBundlePath = NSHomeDirectory() + "/.cache/tailscale/vmtest/macos/"
createDir(vmBundlePath)
let bundleURL = URL(fileURLWithPath: vmBundlePath)
return bundleURL

View File

@ -92,9 +92,9 @@ class VMController: NSObject, VZVirtualMachineDelegate {
virtualMachineConfiguration.storageDevices = [helper.createBlockDeviceConfiguration()]
if headless {
// In headless mode, use only the socket-based NIC. This matches
// the single-NIC configuration used by llmacstation when creating
// and saving VM state. Using a different NIC count would make
// saved state restoration fail silently.
// the single-NIC configuration used when creating the base VM.
// Using a different NIC count would make saved state restoration
// fail silently.
virtualMachineConfiguration.networkDevices = [helper.createSocketNetworkDeviceConfiguration()]
} else {
virtualMachineConfiguration.networkDevices = [helper.createNetworkDeviceConfiguration(), helper.createSocketNetworkDeviceConfiguration()]