diff --git a/cmd/image/commands.go b/cmd/image/commands.go index c3453d5..cdd71b3 100644 --- a/cmd/image/commands.go +++ b/cmd/image/commands.go @@ -6,16 +6,8 @@ import ( "github.com/cocoonstack/cocoon/cmd/cliutil" ) -// Actions is the image-subcommand surface, backed by cocoon's cloudimg store. -type Actions interface { - Pull(cmd *cobra.Command, args []string) error - List(cmd *cobra.Command, args []string) error - Inspect(cmd *cobra.Command, args []string) error - RM(cmd *cobra.Command, args []string) error -} - // Command builds the `image` subcommand tree against the given handler. -func Command(h Actions) *cobra.Command { +func Command(h *Handler) *cobra.Command { imageCmd := &cobra.Command{Use: "image", Short: "Manage macOS disk images (reuses cocoon's cloudimg store)"} // --state-dir is a root persistent flag pull := &cobra.Command{ diff --git a/cmd/image/handler.go b/cmd/image/handler.go index 6f68a57..adf4e8d 100644 --- a/cmd/image/handler.go +++ b/cmd/image/handler.go @@ -16,9 +16,7 @@ import ( const maxPullAttempts = 3 -var _ Actions = (*Handler)(nil) - -// Handler implements Actions on cocoon's cloudimg store. +// Handler is the image command surface on cocoon's cloudimg store. type Handler struct{} // NewHandler returns a Handler backed by the cloudimg store. diff --git a/cmd/vm/commands.go b/cmd/vm/commands.go index 438ed7a..3327fa2 100644 --- a/cmd/vm/commands.go +++ b/cmd/vm/commands.go @@ -6,23 +6,8 @@ import ( "github.com/cocoonstack/cocoon/cmd/cliutil" ) -// Actions mirrors cocoon's cmd/vm Actions, re-declared rather than imported because cocoon's commands.go drags in the Linux-only CH/netlink backend. -type Actions interface { - Create(cmd *cobra.Command, args []string) error - Run(cmd *cobra.Command, args []string) error - Start(cmd *cobra.Command, args []string) error - Stop(cmd *cobra.Command, args []string) error - List(cmd *cobra.Command, args []string) error - Inspect(cmd *cobra.Command, args []string) error - Console(cmd *cobra.Command, args []string) error - RM(cmd *cobra.Command, args []string) error - Snapshot(cmd *cobra.Command, args []string) error - Restore(cmd *cobra.Command, args []string) error - Clone(cmd *cobra.Command, args []string) error -} - // Command builds the `vm` subcommand tree against the given handler. -func Command(h Actions) *cobra.Command { +func Command(h *Handler) *cobra.Command { vmCmd := &cobra.Command{Use: "vm", Short: "Manage macOS VMs"} // --state-dir is a root persistent flag createCmd := &cobra.Command{ @@ -104,6 +89,7 @@ func Command(h Actions) *cobra.Command { } restoreCmd.Flags().String("tag", "", "snapshot tag to restore (default: newest)") restoreCmd.Flags().Bool("force", false, "stop the VM, restore, then relaunch if it was running") + restoreCmd.Flags().String("vnc-password", "", "VNC password for the relaunch of a running VM whose display is password-gated (≤8 chars)") cloneCmd := &cobra.Command{ Use: "clone SRC", diff --git a/cmd/vm/handler.go b/cmd/vm/handler.go index 91ef3f3..fdfa2ee 100644 --- a/cmd/vm/handler.go +++ b/cmd/vm/handler.go @@ -17,9 +17,7 @@ const ( netCNI = "cni" ) -var _ Actions = (*Handler)(nil) - -// Handler implements the vm Actions: per-VM CoW overlays on a golden macOS qcow2, booted by qemu-system-x86_64 on an x86 Linux/KVM host. +// Handler is the vm command surface: per-VM CoW overlays on a golden macOS qcow2, booted by qemu-system-x86_64 on an x86 Linux/KVM host. type Handler struct{} // NewHandler returns a Handler ready to serve the vm subcommands. diff --git a/cmd/vm/lifecycle_test.go b/cmd/vm/lifecycle_test.go index f97b9ec..2bc5009 100644 --- a/cmd/vm/lifecycle_test.go +++ b/cmd/vm/lifecycle_test.go @@ -65,6 +65,7 @@ func TestRestoreRefusesRunningPasswordedVNC(t *testing.T) { cmd := newLifecycleTestCommand(t, stateDir) cmd.Flags().String("tag", "", "") cmd.Flags().Bool("force", true, "") + cmd.Flags().String("vnc-password", "", "") err = NewHandler().Restore(cmd, []string{"macos-demo"}) if err == nil || !strings.Contains(err.Error(), "password-gated VNC") { diff --git a/cmd/vm/snapshot.go b/cmd/vm/snapshot.go index 044e992..7f8c414 100644 --- a/cmd/vm/snapshot.go +++ b/cmd/vm/snapshot.go @@ -67,9 +67,14 @@ func (h *Handler) Restore(cmd *cobra.Command, args []string) error { if force, _ := cmd.Flags().GetBool("force"); !force { return fmt.Errorf("vm %q is running; stop it first or pass --force to stop+restore", r.Name) } - if r.VNCDisp >= 0 && (r.VNCPassSet || r.Netns != "") { - return fmt.Errorf("vm %q serves a password-gated VNC display and a relaunch cannot carry the password; stop it, restore, then start --vnc %d --vnc-password", r.Name, r.VNCDisp) + vncPass, _ := cmd.Flags().GetString("vnc-password") + if err := validateVNCPassword(vncPass); err != nil { + return err + } + if vncPass == "" && r.VNCDisp >= 0 && (r.VNCPassSet || r.Netns != "") { + return fmt.Errorf("vm %q serves a password-gated VNC display; pass --vnc-password to relaunch it, or stop, restore, then start --vnc %d --vnc-password", r.Name, r.VNCDisp) } + r.VNCPass = vncPass terminate(ctx, r, stopGracePeriod) r.PID = 0 if err := saveRec(dir, r); err != nil { diff --git a/docs/snapshots.md b/docs/snapshots.md index c7da053..295bfcf 100644 --- a/docs/snapshots.md +++ b/docs/snapshots.md @@ -8,6 +8,14 @@ cocoon-macos vm snapshot m1 --tag clean cocoon-macos vm restore m1 --tag clean # --force to stop+restore+relaunch a running VM ``` +`--force` on a running VM keeps its VNC display across the relaunch. A display +that was started with `--vnc-password` (every CNI display, optionally others) +needs the password again: pass `--vnc-password` to `restore`, or stop, restore +and `start --vnc N --vnc-password`; without it the restore is refused before +any disk is touched. VMs launched by a cocoon-macos older than this rule with a +passworded non-CNI display are not recognised as password-gated until their +next stop or start. + Snapshots are **offline qcow2-internal** (`qemu-img snapshot`, VM stopped) and capture **disk state only**. Live RAM snapshot is intentionally impossible here: `-cpu +invtsc` makes the macOS guest non-migratable, so resume-from-RAM can't work. This is by design (see [Roadmap](roadmap.md)).