Skip to content
Merged
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
10 changes: 1 addition & 9 deletions cmd/image/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
4 changes: 1 addition & 3 deletions cmd/image/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 2 additions & 16 deletions cmd/vm/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 1 addition & 3 deletions cmd/vm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions cmd/vm/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
9 changes: 7 additions & 2 deletions cmd/vm/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions docs/snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down