Skip to content

Commit cd6ac30

Browse files
committed
feat(vm): qcow2 baking via cocoon storage — overlay on the immutable store blob
vm create now resolves IMAGE through cocoon's cloudimg store (cloudimg.Config -> content-addressed blob path + digest) and bakes the per-VM CoW overlay on that immutable base with cocoon's utils.RunQemuImg, recording image_digest. A filesystem path still works (legacy/testbed), so the positional IMAGE accepts both a store ref ('cocoon-macos image pull' then 'vm run <ref>') and a direct qcow2 path. Base stays read-only; the macOS image is never mutated. Phase 2 of the cocoon-import refactor.
1 parent cf25da2 commit cd6ac30

1 file changed

Lines changed: 60 additions & 18 deletions

File tree

cmd/vm/handler.go

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package vm
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"net"
@@ -13,31 +14,63 @@ import (
1314

1415
"github.com/spf13/cobra"
1516

17+
"github.com/cocoonstack/cocoon/config"
18+
"github.com/cocoonstack/cocoon/images/cloudimg"
19+
"github.com/cocoonstack/cocoon/types"
20+
"github.com/cocoonstack/cocoon/utils"
21+
1622
"github.com/cocoonstack/cocoon-macos/qemu"
1723
)
1824

25+
// resolveBase returns the immutable base qcow2 for IMAGE: a direct filesystem path (legacy), else
26+
// an image ref resolved through cocoon's cloudimg store (returns the content-addressed blob + its
27+
// digest). Per-VM overlays are baked on this base, which stays read-only.
28+
func resolveBase(cmd *cobra.Command, image, name string) (string, string, error) {
29+
if _, err := os.Stat(image); err == nil {
30+
return image, "", nil
31+
}
32+
ctx := cmd.Context()
33+
if ctx == nil {
34+
ctx = context.Background()
35+
}
36+
store, err := cloudimg.New(ctx, &config.Config{RootDir: stateDir(cmd), DNS: "8.8.8.8,1.1.1.1"})
37+
if err != nil {
38+
return "", "", err
39+
}
40+
vm := &types.VMConfig{Config: types.Config{Image: image}, Name: name}
41+
sc, _, err := store.Config(ctx, []*types.VMConfig{vm})
42+
if err != nil {
43+
return "", "", fmt.Errorf("resolve image %q (not a file, not in the store): %w", image, err)
44+
}
45+
if len(sc) == 0 || len(sc[0]) == 0 {
46+
return "", "", fmt.Errorf("image %q resolved to no disk", image)
47+
}
48+
return sc[0][0].Path, vm.ImageDigest, nil
49+
}
50+
1951
// Handler implements Actions by cloning a golden macOS qcow2 (copy-on-write overlay)
2052
// and launching qemu-system-x86_64 on an x86 Linux/KVM host.
2153
type Handler struct{}
2254

2355
func NewHandler() *Handler { return &Handler{} }
2456

2557
type record struct {
26-
Name string `json:"name"`
27-
Image string `json:"image"`
28-
Disk string `json:"disk"`
29-
OpenCore string `json:"opencore"`
30-
OVMFCode string `json:"ovmf_code"`
31-
OVMFVars string `json:"ovmf_vars"`
32-
CPUs int `json:"cpus"`
33-
Memory string `json:"memory"`
34-
VNCDisp int `json:"vnc"`
35-
SSHPort int `json:"ssh_port"`
36-
PID int `json:"pid"`
37-
Created string `json:"created"`
38-
MAC string `json:"mac,omitempty"`
39-
SMBIOS *qemu.SMBIOS `json:"smbios,omitempty"`
40-
VNCPass string `json:"vnc_password,omitempty"`
58+
Name string `json:"name"`
59+
Image string `json:"image"`
60+
ImageDigest string `json:"image_digest,omitempty"`
61+
Disk string `json:"disk"`
62+
OpenCore string `json:"opencore"`
63+
OVMFCode string `json:"ovmf_code"`
64+
OVMFVars string `json:"ovmf_vars"`
65+
CPUs int `json:"cpus"`
66+
Memory string `json:"memory"`
67+
VNCDisp int `json:"vnc"`
68+
SSHPort int `json:"ssh_port"`
69+
PID int `json:"pid"`
70+
Created string `json:"created"`
71+
MAC string `json:"mac,omitempty"`
72+
SMBIOS *qemu.SMBIOS `json:"smbios,omitempty"`
73+
VNCPass string `json:"vnc_password,omitempty"`
4174
}
4275

4376
func stateDir(cmd *cobra.Command) string {
@@ -93,9 +126,18 @@ func (h *Handler) create(cmd *cobra.Command, image string) (*record, error) {
93126
if err := os.MkdirAll(dir, 0o755); err != nil {
94127
return nil, err
95128
}
129+
base, digest, err := resolveBase(cmd, image, name)
130+
if err != nil {
131+
return nil, err
132+
}
96133
overlay := filepath.Join(dir, "disk.qcow2")
97-
if out, err := exec.Command("qemu-img", "create", "-f", "qcow2", "-b", image, "-F", "qcow2", overlay).CombinedOutput(); err != nil {
98-
return nil, fmt.Errorf("qemu-img create overlay: %v: %s", err, out)
134+
ctx := cmd.Context()
135+
if ctx == nil {
136+
ctx = context.Background()
137+
}
138+
// bake a per-VM CoW overlay on the immutable base (cocoon's storage convention; base stays RO)
139+
if err := utils.RunQemuImg(ctx, "create", "-f", "qcow2", "-F", "qcow2", "-b", base, overlay); err != nil {
140+
return nil, fmt.Errorf("bake overlay on %s: %w", base, err)
99141
}
100142
ovmfVars := filepath.Join(dir, "OVMF_VARS.fd")
101143
if err := copyFile(varsTmpl, ovmfVars); err != nil {
@@ -107,7 +149,7 @@ func (h *Handler) create(cmd *cobra.Command, image string) (*record, error) {
107149
ssh, _ := cmd.Flags().GetInt("ssh-port")
108150
vncPass, _ := cmd.Flags().GetString("vnc-password")
109151
r := &record{
110-
Name: name, Image: image, Disk: overlay, OpenCore: oc, OVMFCode: code, OVMFVars: ovmfVars,
152+
Name: name, Image: image, ImageDigest: digest, Disk: overlay, OpenCore: oc, OVMFCode: code, OVMFVars: ovmfVars,
111153
CPUs: cpus, Memory: mem, VNCDisp: vnc, SSHPort: ssh, VNCPass: vncPass, Created: time.Now().Format(time.RFC3339),
112154
}
113155
if random, _ := cmd.Flags().GetBool("random-smbios"); random {

0 commit comments

Comments
 (0)