Skip to content

Commit d02f48c

Browse files
committed
feat(vm): add --cold clone and --vm-id for disk-only cold migration
clone --cold boots a fresh VM from the snapshot's disk and discards the saved CH state (state.json + memory ranges). The guest re-evaluates CPUID at boot, so the clone survives hypervisor-version / CPUID changes that fail a warm vm.restore (CpuidCheckCompatibility) — at the cost of losing in-memory runtime state. The COW overlay is a standard guest-owned image and stays valid across versions. clone --vm-id reuses the source VM's ID instead of generating one, so a managing controller (e.g. vk-cocoon) re-adopts the clone by its stored VMID instead of treating it as an orphan and destroying it. Threaded via a transient VMConfig.ColdBoot flag (mirrors OnDemand); gated to Cloud Hypervisor at the CLI. Reuses the cold-boot primitive (buildVMConfig -> buildCLIArgs -> launchProcess), extracted as launchFresh and shared with startOne.
1 parent 2f3589d commit d02f48c

5 files changed

Lines changed: 79 additions & 6 deletions

File tree

cmd/vm/commands.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ func addCloneFlags(cmd *cobra.Command) {
299299
cmd.Flags().String("bridge", "", "use TAP-on-bridge instead of CNI (value is bridge device, e.g. cni0)")
300300
cmd.Flags().Bool("no-direct-io", false, "disable O_DIRECT on writable disks (inherit from snapshot if not set)")
301301
cmd.Flags().Bool("on-demand", false, "use UFFD on-demand memory loading for faster clone (CH only; snapshot file must remain on disk)")
302+
cmd.Flags().Bool("cold", false, "clone the disk only and cold-boot the guest, discarding saved memory/vCPU state (CH only; survives hypervisor-version/CPUID changes that block warm restore)")
303+
cmd.Flags().String("vm-id", "", "reuse this VM ID instead of generating one (preserves identity for controller re-adoption after a cold migration)")
302304
cmd.Flags().Bool("pull", false, "auto-pull base image if not found locally (for cross-node clone)")
303305
cmd.Flags().String("from-dir", "", "clone from a snapshot directory (must contain snapshot.json) instead of the local snapshot DB; mutually exclusive with positional SNAPSHOT")
304306
}

cmd/vm/run.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,26 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
299299
return nil, "", nil, types.NetSetup{}, err
300300
}
301301
vmID := utils.GenerateID()
302+
// Reuse the source VM's ID to preserve identity across a cold migration, so
303+
// the managing controller (e.g. vk-cocoon) re-adopts the clone by its stored
304+
// VMID instead of treating it as an orphan.
305+
if id, _ := cmd.Flags().GetString("vm-id"); id != "" {
306+
vmID = id
307+
}
302308
if vmCfg.Name == "" {
303309
vmCfg.Name = "cocoon-clone-" + network.VMIDPrefix(vmID)
304310
}
305311
if err = vmCfg.Validate(); err != nil {
306312
return nil, "", nil, types.NetSetup{}, err
307313
}
308314

315+
if cold, _ := cmd.Flags().GetBool("cold"); cold {
316+
if conf.UseFirecracker {
317+
return nil, "", nil, types.NetSetup{}, fmt.Errorf("--cold clone is Cloud Hypervisor only")
318+
}
319+
vmCfg.ColdBoot = true
320+
}
321+
309322
if pull, _ := cmd.Flags().GetBool("pull"); pull && vmCfg.Image != "" && vmCfg.ImageType != "" {
310323
backends, initErr := cmdcore.InitImageBackends(ctx, conf)
311324
if initErr != nil {

hypervisor/cloudhypervisor/clone.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
6565
return nil, fmt.Errorf("verify base files: %w", err)
6666
}
6767

68-
stateReplacements := buildStateReplacements(chCfg, storageConfigs)
69-
7068
storageConfigs, err = ch.ensureCloneCidata(vmID, vmCfg, networkConfigs, storageConfigs, directBoot)
7169
if err != nil {
7270
return nil, err
@@ -75,6 +73,11 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
7573
return nil, fmt.Errorf("validate post-cidata storage: %w", vErr)
7674
}
7775

76+
if vmCfg.ColdBoot {
77+
return ch.coldBootClone(ctx, vmID, vmCfg, net, runDir, logDir, now, bootCfg, storageConfigs, sourceSnapshotID)
78+
}
79+
80+
stateReplacements := buildStateReplacements(chCfg, storageConfigs)
7881
patchStorageConfigs := restorePatchStorageConfigs(storageConfigs, directBoot, vmCfg.Windows, hadCidataInSnapshot)
7982

8083
consoleSock := hypervisor.ConsoleSockPath(runDir)
@@ -141,6 +144,54 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
141144
return info, nil
142145
}
143146

147+
// coldBootClone boots a fresh VM from the cloned disk, discarding the snapshot's
148+
// saved CH state (state.json + memory ranges). The guest re-evaluates CPUID at
149+
// boot, so the clone survives hypervisor-version / CPUID changes that would fail
150+
// a warm vm.restore — at the cost of losing in-memory runtime state. The disk
151+
// (COW overlay) is a standard guest-owned image and stays valid across versions.
152+
func (ch *CloudHypervisor) coldBootClone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, bootCfg *types.BootConfig, storageConfigs []*types.StorageConfig, sourceSnapshotID string) (_ *types.VM, err error) {
153+
logger := log.WithFunc("cloudhypervisor.coldBootClone")
154+
155+
rec := &hypervisor.VMRecord{
156+
VM: types.VM{
157+
ID: vmID, Hypervisor: typ, State: types.VMStateRunning,
158+
Config: *vmCfg, StorageConfigs: storageConfigs, NetSetup: net,
159+
// FirstBooted suppresses cidata re-attach for an already-provisioned
160+
// disk (see activeDisks) and meters this as a restart, not a boot.
161+
FirstBooted: true,
162+
},
163+
BootConfig: bootCfg,
164+
RunDir: runDir,
165+
LogDir: logDir,
166+
}
167+
168+
// Clone reassigns NIC MACs and disk serials, so a kernel cmdline baked at
169+
// snapshot time is stale; rebuild it for direct-boot guests.
170+
if isDirectBoot(bootCfg) {
171+
dns, dnsErr := ch.conf.DNSServers()
172+
if dnsErr != nil {
173+
return nil, fmt.Errorf("parse DNS servers: %w", dnsErr)
174+
}
175+
bootCfg.Cmdline = buildCmdline(storageConfigs, net.NetworkConfigs, vmCfg.Name, dns)
176+
}
177+
178+
pid, err := ch.launchFresh(ctx, rec, hypervisor.SocketPath(runDir))
179+
if err != nil {
180+
ch.MarkError(ctx, vmID)
181+
return nil, fmt.Errorf("launch CH: %w", err)
182+
}
183+
184+
info := &rec.VM
185+
info.CreatedAt, info.UpdatedAt, info.StartedAt = now, now, &now
186+
if err = ch.FinalizeClone(ctx, vmID, info, bootCfg, nil, sourceSnapshotID); err != nil {
187+
ch.AbortLaunch(ctx, pid, hypervisor.SocketPath(runDir), runDir, runtimeFiles)
188+
return nil, fmt.Errorf("finalize VM record: %w", err)
189+
}
190+
191+
logger.Infof(ctx, "VM %s cold-cloned from snapshot (disk only)", vmID)
192+
return info, nil
193+
}
194+
144195
func (ch *CloudHypervisor) restoreAndResumeClone(
145196
ctx context.Context,
146197
pid int,

hypervisor/cloudhypervisor/start.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,20 @@ func (ch *CloudHypervisor) startOne(ctx context.Context, id string) (bool, error
1919
return ch.StartSequence(ctx, id, hypervisor.StartSpec{
2020
RuntimeFiles: runtimeFiles,
2121
Launch: func(ctx context.Context, rec *hypervisor.VMRecord, sockPath string) (int, error) {
22-
vmCfg := buildVMConfig(ctx, rec, hypervisor.ConsoleSockPath(rec.RunDir))
23-
args := buildCLIArgs(vmCfg, sockPath)
24-
ch.saveCmdline(ctx, rec, args)
25-
return ch.launchProcess(ctx, rec, sockPath, args, rec.ResolvedNetnsPath())
22+
return ch.launchFresh(ctx, rec, sockPath)
2623
},
2724
})
2825
}
2926

27+
// launchFresh cold-boots a VM from its record: builds the CH config from the
28+
// record, persists the cmdline, and starts the process. Shared by normal start
29+
// and cold clone — both boot from disk with no saved CH state to restore.
30+
func (ch *CloudHypervisor) launchFresh(ctx context.Context, rec *hypervisor.VMRecord, sockPath string) (int, error) {
31+
args := buildCLIArgs(buildVMConfig(ctx, rec, hypervisor.ConsoleSockPath(rec.RunDir)), sockPath)
32+
ch.saveCmdline(ctx, rec, args)
33+
return ch.launchProcess(ctx, rec, sockPath, args, rec.ResolvedNetnsPath())
34+
}
35+
3036
func (ch *CloudHypervisor) launchProcess(ctx context.Context, rec *hypervisor.VMRecord, socketPath string, args []string, netnsPath string) (int, error) {
3137
processLog := ch.LogFilePath(rec.LogDir)
3238
logFile, err := os.Create(processLog) //nolint:gosec

types/vm.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type VMConfig struct {
3131
Name string `json:"name"`
3232

3333
OnDemand bool `json:"-"` // use UFFD on-demand memory restore (CH only); transient, not persisted
34+
ColdBoot bool `json:"-"` // clone disk only and cold-boot, discarding saved memory/vCPU state; transient
3435
User string `json:"-"`
3536
Password string `json:"-"`
3637
DataDisks []DataDiskSpec `json:"-"` // populated from --data-disk; consumed by Create

0 commit comments

Comments
 (0)