Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/vm/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ func (h *Handler) Clone(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("hugepages") {
r.Hugepages, _ = cmd.Flags().GetBool("hugepages")
}
r.ExitOnReboot = srcRec.ExitOnReboot
if cmd.Flags().Changed("exit-on-reboot") {
r.ExitOnReboot, _ = cmd.Flags().GetBool("exit-on-reboot")
}
r.VNCDisp = vnc
r.SSHPort, _ = cmd.Flags().GetInt("ssh-port")
r.VNCPass = vncPass
Expand Down
1 change: 1 addition & 0 deletions cmd/vm/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func addVMFlags(cmd *cobra.Command) {
cmd.Flags().Int("cpus", 4, "vCPU count")
cmd.Flags().String("memory", "8192", "guest memory in MiB")
cmd.Flags().Bool("hugepages", false, "back guest RAM with 2 MiB hugepages (needs host hugepages reserved; lower TLB/EPT overhead)")
cmd.Flags().Bool("exit-on-reboot", false, "exit QEMU on guest reboot so an external supervisor can relaunch it cold")
cmd.Flags().StringArray("data-disk", nil, "attach an extra qcow2 data disk: comma-separated key=value (size= required e.g. size=20G; name= optional, default dataN). Repeatable, max 4. macOS has no in-guest agent, so fstype=/mount= are unsupported — format it in the guest (Disk Utility/diskutil)")
cmd.Flags().Int("vnc", -1, "VNC display number for the initial boot (n => port 590n); <0 disables. Launch-scoped: cleared on stop, re-enable per start with `vm start --vnc`")
cmd.Flags().Int("ssh-port", 0, "host port forwarded to guest :22; 0 disables")
Expand Down
17 changes: 9 additions & 8 deletions cmd/vm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ type record struct {
Image string `json:"image"`
ImageDigest string `json:"image_digest,omitempty"`

CPUs int `json:"cpus"`
Memory string `json:"memory"`
VNCDisp int `json:"vnc"`
VNCPass string `json:"-"` // launch-scoped, set from the flag each start; never persisted (would leak at rest)
SSHPort int `json:"ssh_port"`
NetMode string `json:"net_mode,omitempty"`
Hugepages bool `json:"hugepages,omitempty"`
DataDisks []string `json:"data_disks,omitempty"` // created data-disk qcow2 paths, attached on AHCI ports 0,1,3,5
CPUs int `json:"cpus"`
Memory string `json:"memory"`
VNCDisp int `json:"vnc"`
VNCPass string `json:"-"` // launch-scoped, set from the flag each start; never persisted (would leak at rest)
SSHPort int `json:"ssh_port"`
NetMode string `json:"net_mode,omitempty"`
Hugepages bool `json:"hugepages,omitempty"`
ExitOnReboot bool `json:"exit_on_reboot,omitempty"`
DataDisks []string `json:"data_disks,omitempty"` // created data-disk qcow2 paths, attached on AHCI ports 0,1,3,5

Disk string `json:"disk"`
OpenCore string `json:"opencore"`
Expand Down
13 changes: 8 additions & 5 deletions cmd/vm/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ func (h *Handler) create(cmd *cobra.Command, image string) (*record, error) {
ssh, _ := cmd.Flags().GetInt("ssh-port")
tap, _ := cmd.Flags().GetString("tap")
huge, _ := cmd.Flags().GetBool("hugepages")
exitOnReboot, _ := cmd.Flags().GetBool("exit-on-reboot")
r := &record{
Name: name, Image: image, ImageDigest: digest, Disk: overlay, OVMFCode: code, OVMFVars: ovmfVars,
CPUs: cpus, Memory: mem, VNCDisp: vnc, SSHPort: ssh, VNCPass: vncPass, NetMode: netMode, Tap: tap, Hugepages: huge,
VMID: utils.GenerateID(), Created: time.Now().Format(time.RFC3339),
ExitOnReboot: exitOnReboot,
VMID: utils.GenerateID(), Created: time.Now().Format(time.RFC3339),
}
if r.DataDisks, err = createDataDisks(ctx, dir, diskSpecs); err != nil {
return nil, err
Expand Down Expand Up @@ -180,10 +182,11 @@ func (h *Handler) launch(cmd *cobra.Command, dir string, r *record) error {
spec := qemu.Spec{
Name: r.Name, Disk: r.Disk, OpenCore: r.OpenCore, OVMFCode: r.OVMFCode, OVMFVars: r.OVMFVars,
CPUs: r.CPUs, Memory: r.Memory, VNCDisp: r.VNCDisp, SSHPort: r.SSHPort, MAC: r.MAC, VNCPass: r.VNCPass,
Tap: r.Tap, // set for tap/bridge/cni (a real host TAP); empty => user-mode SLIRP
Hugepages: r.Hugepages,
DataDisks: r.DataDisks,
MonSock: filepath.Join(dir, "monitor.sock"), QMPSock: filepath.Join(dir, "qmp.sock"),
Tap: r.Tap, // set for tap/bridge/cni (a real host TAP); empty => user-mode SLIRP
Hugepages: r.Hugepages,
ExitOnReboot: r.ExitOnReboot,
DataDisks: r.DataDisks,
MonSock: filepath.Join(dir, "monitor.sock"), QMPSock: filepath.Join(dir, "qmp.sock"),
}
// CNI: a 127.0.0.1 VNC inside the netns is unreachable; use a unix socket fronted by startVNCProxy
if r.Netns != "" && r.VNCDisp >= 0 {
Expand Down
6 changes: 6 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ start — the allocation is not best-effort. On `clone` the flag is only
applied when passed explicitly; otherwise the source VM's setting carries
over.

`--exit-on-reboot` on `create` / `run` / `clone` is for VMs owned by an external
supervisor. It persists with the VM and is inherited by clones. QEMU's
`-no-reboot` exit skips the normal `vm stop` cleanup, so the supervisor must
recover the existing record with `vm start`; standalone VMs keep QEMU's normal
in-process reboot behavior.

## What `vm run` does

1. `qemu-img create -b <golden> overlay.qcow2` — instant copy-on-write clone of the golden image.
Expand Down
21 changes: 13 additions & 8 deletions qemu/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ const (
type Spec struct {
Name string

CPUs int
Memory string // MiB, e.g. "8192"
VNCDisp int // n => host 127.0.0.1:590n; <0 disables
VNCSock string // when set, bind VNC to this unix socket instead of 127.0.0.1 (CNI: fronted by vncProxy)
VNCPass string // set via the monitor post-launch (macOS Screen Sharing needs password auth)
SSHPort int // host port forwarded to guest :22; 0 disables
Hugepages bool // needs host hugepages reserved; off => default RAM
DataDisks []string // extra qcow2 data disks; attached on the AHCI ports MacHDD/OpenCore leave free
CPUs int
Memory string // MiB, e.g. "8192"
VNCDisp int // n => host 127.0.0.1:590n; <0 disables
VNCSock string // when set, bind VNC to this unix socket instead of 127.0.0.1 (CNI: fronted by vncProxy)
VNCPass string // set via the monitor post-launch (macOS Screen Sharing needs password auth)
SSHPort int // host port forwarded to guest :22; 0 disables
Hugepages bool // needs host hugepages reserved; off => default RAM
ExitOnReboot bool // exit QEMU on a guest reboot so an external owner can relaunch it cold
DataDisks []string // extra qcow2 data disks; attached on the AHCI ports MacHDD/OpenCore leave free

Disk string
OpenCore string
Expand Down Expand Up @@ -76,6 +77,10 @@ func (s Spec) Args() []string {
"-device", "ide-hd,bus=sata.4,drive=MacHDD",
"-device", "vmware-svga",
}
if s.ExitOnReboot {
// macOS warm reset can stall in early boot under KVM; exit so the owner relaunches cold.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the stall observation isn't going to be attached, don't encode folklore in the comment — state the contract instead: // supervisor-owned guest: a reboot request exits qemu so the owner relaunches it cold. same for the 'Problem' paragraph in the body.

a = append(a, "-no-reboot")
}
a = append(memBackend, a...) // -object must precede the -machine memory-backend reference
// the SATA ports OpenCoreBoot (sata.2) and MacHDD (sata.4) leave free; the count is capped at 4 upstream
dataDiskPorts := []int{0, 1, 3, 5}
Expand Down
11 changes: 11 additions & 0 deletions qemu/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ func TestArgsCPU(t *testing.T) {
}
}

func TestArgsGuestRebootExitsQEMU(t *testing.T) {
s := Spec{Disk: "/v/d.qcow2", OpenCore: "/v/oc.qcow2", OVMFCode: "/v/c.fd", OVMFVars: "/v/v.fd", CPUs: 4, Memory: "4096", VNCDisp: -1}
if slices.Contains(s.Args(), "-no-reboot") {
t.Fatalf("standalone VMs must keep QEMU's normal reboot behavior: %v", s.Args())
}
s.ExitOnReboot = true
if !slices.Contains(s.Args(), "-no-reboot") {
t.Fatalf("externally managed VM must exit QEMU for a cold relaunch: %v", s.Args())
}
}

// argVals returns each token immediately following flag in args.
func argVals(args []string, flag string) []string {
var out []string
Expand Down