From 647bab9acdf3b8b362be89dbcef4724f6753549e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 Apr 2026 08:53:36 -0700 Subject: [PATCH] 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. --- tstest/build-macos-base-vm/main.go | 8 +-- tstest/natlab/vmtest/tailmac.go | 57 +++++++++----------- tstest/natlab/vmtest/vmtest.go | 4 +- tstest/tailmac/Swift/Common/Config.swift | 6 +-- tstest/tailmac/Swift/Host/VMController.swift | 6 +-- 5 files changed, 38 insertions(+), 43 deletions(-) diff --git a/tstest/build-macos-base-vm/main.go b/tstest/build-macos-base-vm/main.go index 347487662..71070b5ad 100644 --- a/tstest/build-macos-base-vm/main.go +++ b/tstest/build-macos-base-vm/main.go @@ -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// 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") diff --git a/tstest/natlab/vmtest/tailmac.go b/tstest/natlab/vmtest/tailmac.go index 619290c88..0be2f535b 100644 --- a/tstest/natlab/vmtest/tailmac.go +++ b/tstest/natlab/vmtest/tailmac.go @@ -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"} diff --git a/tstest/natlab/vmtest/vmtest.go b/tstest/natlab/vmtest/vmtest.go index 512aadb23..8b2203a17 100644 --- a/tstest/natlab/vmtest/vmtest.go +++ b/tstest/natlab/vmtest/vmtest.go @@ -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) diff --git a/tstest/tailmac/Swift/Common/Config.swift b/tstest/tailmac/Swift/Common/Config.swift index 53d768020..53281628a 100644 --- a/tstest/tailmac/Swift/Common/Config.swift +++ b/tstest/tailmac/Swift/Common/Config.swift @@ -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 diff --git a/tstest/tailmac/Swift/Host/VMController.swift b/tstest/tailmac/Swift/Host/VMController.swift index 883bdcdd9..68324c507 100644 --- a/tstest/tailmac/Swift/Host/VMController.swift +++ b/tstest/tailmac/Swift/Host/VMController.swift @@ -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()]