Skip to content

VMware→KVM: cold (VDDK) and warm (CBT) migration into Ceph/RBD and Linstor, plus importVm adoption of existing RBD root volumes and Linstor root/data volumes - #13656

Open
andrijapanicsb wants to merge 29 commits into
apache:mainfrom
andrijapanicsb:kvm-rbd-vmware-migration
Open

Conversation

@andrijapanicsb

@andrijapanicsb andrijapanicsb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

This PR makes Ceph/RBD and Linstor first-class targets for KVM VM onboarding and adds an operator-driven warm VMware→KVM migration path. It bundles the following (Ceph/RBD plus Linstor as block-storage destinations):

1. VMware CBT → native RBD/qcow2 warm migration to KVM

An operator-driven, warm VMware-to-KVM migration built on VDDK and VMware Changed Block Tracking (CBT). It maintains a source-equivalent KVM-side replica kept current through repeated delta/sync cycles, then finalizes the guest with virt-v2v in-place at cutover — bounding downtime to the cutover window instead of a full cold copy.

  • Targets both filesystem (qcow2) and native Ceph/RBD (raw image) primary storage.
  • Full lifecycle as async operations with persisted migration state (migration / cycle / disk records), preflight validation, cancel/cleanup, and Import/Export UI integration.
  • New admin API commands: startVmwareCbtMigration, syncVmwareCbtMigration, cutoverVmwareCbtMigration, cancelVmwareCbtMigration, deleteVmwareCbtMigration, listVmwareCbtMigrations, checkVmwareCbtMigrationPrerequisites.
  • Server orchestration in VmwareCbtMigrationManagerImpl (+ VmwareCbtMigrationServiceImpl, cutover policy), backed by new VOs/DAOs and a DB schema migration.
  • KVM agent: libvirt CBT command wrappers (prepare / sync / cutover / RBD-probe) driving VDDK + qemu + virt-v2v, plus agent commands/answers and a sync-plan.
  • UI: new VMware CBT Migrations view and additions to Import/Manage Instances under Tools.
  • Docs: docs/vmware-cbt/ (architecture + README) and docs/vmware-cbt-migration.md.

2. Direct VDDK VMware import into Ceph/RBD — the cold (one-shot) path

The cold counterpart to the warm CBT migration in (1): extends the existing
VDDK + virt-v2v VMware→KVM import — which previously landed only on NFS/qcow2
(direct, or via a staged file) — so the converted disk is written straight into an
RBD pool as a raw image
, with in-place virt-v2v finalization and no intermediate
qcow2-on-NFS staging copy. (The warm path in (1) reuses this same full-disk copy for its
initial sync.) Adds host-capability detection for qemu-img RBD support, RBD qemu-copy,
virt-v2v in-place, and direct-RBD import — advertised via ReadyCommand and reconciled
by the agent manager.

3. importVm adoption of existing RBD (Ceph) root volumes and Linstor root + data volumes

Extends the KVM unmanaged-import / manage-volume flow so an existing raw RBD image can be adopted directly as a VM ROOT volume. Adds RBD-aware format handling in VolumeOrchestrator (RBD ⇒ RAW instead of the hardcoded QCOW2) and the RBD branch in the libvirt volume-inspection path (LibvirtCheckVolumeCommandWrapper), so CheckVolumeCommand succeeds on RBD instead of failing with "Disk not found or is invalid".

Also extends the same adoption path to Linstor (DRBD) primary storage, for both ROOT and DATA volumes: an externally prepared Linstor volume (e.g. landed by a migration/conversion tool) can be adopted without any data copy. Linstor volumes are raw local block devices, so volume inspection runs qemu-img against the /dev/drbd/… device path (LibvirtCheckVolumeCommandWrapper for ROOT, LibvirtGetVolumesOnStorageCommandWrapper + the volume import/unmanage gate for DATA), and VolumeOrchestrator records RAW. Because a host-local qemu-img file lock cannot see a DRBD resource that is Primary/open on another node (unlike RBD's cluster-wide exclusive lock), adoption additionally consults Linstor's cluster-wide InUse state — new KVMStoragePool.getVolumeInUseNode() capability (default no-op), answered via the existing LinstorUtil.isResourceInUse — and refuses a volume attached to a running VM anywhere in the cluster. Volumes are adopted under their existing resource names (Linstor cannot rename; 48-char limit).

The existing filesystem/qcow2 adoption path is left unchanged, by construction rather than by accident: the supported-pool-type list is extended additively; the raw-device inspection branch is guarded by an explicit RBD/Linstor check that returns early, so filesystem pools still fall through to the original checkQcow2File path; the new cluster-wide in-use lookup is a KVMStoragePool default returning null and is overridden only by LinstorStoragePool; and the format resolution is a new overload that delegates to the pre-existing method for every pool type other than RBD/Linstor (so qcow2 pools still resolve to QCOW2).

4. Consolidate KVM host-capability probe names across the RBD features

Because features (1) and (2) introduced host-capability keys independently, this commit unifies them under feature-neutral names that describe what is probed, and drops a redundant alias (host.rbd.qemu.copy.support, identical to host.qemu.rbd.support) and its delegate.

5. Linstor (DRBD) primary storage as a migration destination

Extends all three VMware→KVM paths above — staged import, direct VDDK import, and warm CBT migration — so the converted disks can also land on Linstor primary storage, not just Ceph/RBD. A generic RAW_BLOCK_DEVICE target type covers "write RAW into a host-local block device provided by the storage adaptor", so the same code paths serve Linstor (and lay groundwork for other block backends).

  • The KVM agent pre-creates each Linstor resource at source capacity (unlike RBD, qemu-img cannot create a DRBD device), copies with qemu-img convert -n — or nbdcopy when the host has it — into the local /dev/drbd/… device, and finalizes with virt-v2v-in-place fed a <disk type='block'> domain XML (no qemu-nbd bridge needed for a local device).
  • Warm CBT delta cycles stream each changed extent from the nbdkit/VDDK source window straight into the same device window (qemu-img convert -n -S 0), avoiding a temp-file round-trip.
  • Server orchestration adds Linstor to the staged / direct-VDDK / CBT destination pool types and validates that the conversion host is a LINSTOR satellite connected to the pool.
  • LINSTOR resource-name limits (48 chars, no rename) are respected with short deterministic names; imported volumes keep the name as their recorded path.
  • Also drops an unsupported -O flag from the virt-v2v-in-place invocation (fixes in-place finalization on virt-v2v < 2.5, e.g. Ubuntu 24.04) — which benefits the RBD path too.

6. Data-plane efficiency and correctness for the agent-driven copies

These apply to the agent's own nbdkit/VDDK full-disk copies (cold direct import and CBT initial full sync); they do not touch the staged virt-v2v conversion path.

  • VDDK NBD compression — the nbdkit VDDK plugin now runs with compression=<value> (new vddk.nbd.compression agent property, default fastlz; none/zlib/fastlz/skipz). On datastores that don't expose block allocation to VDDK, the full nominal disk size otherwise crosses the network; fastlz compresses both zero regions and real data on the ESXi side at low CPU cost (measured on a 30 GiB disk: 32.4 GB → 2.65 GB over the wire).
  • Multi-connection nbdcopy for RBD full copies — cold-direct-to-RBD and the CBT initial full sync to RBD now copy with nbdcopy over a localhost qemu-nbd bridge (pre-create the raw image, serve it locally, copy NBD-to-NBD) instead of a single-stream qemu-img convert, falling back to qemu-img convert when nbdcopy is unavailable. The Linstor block-device path already used nbdcopy directly; CBT delta cycles are unchanged (a range copier is required there).
  • Thin-safe zero handling — copies into a pre-created block-device target skip writing zero blocks (--target-is-zero / --destination-is-zero) only when the backend guarantees a freshly created volume reads back as zeros, via a new KVMStoragePool.isVolumeZeroInitialized() capability (Linstor answers it from the provider kind — thin providers only). On backends that do not guarantee it (e.g. LVM-thick) the zeros are written, so no stale data from a previously deleted volume leaks into the unwritten regions. qemu-img's --target-is-zero support is also probed directly (newer qemu dropped it from --help while still supporting it).

7. Windows guest post-migration handling

For Windows guests, a first-boot script is injected via virt-v2v --firstboot: on first boot it sets the SAN policy to OnlineAll and brings migrated data disks online and read-write, closing the common post-conversion gap where secondary disks come up Offline/Read-only under the default Windows SAN policy. It is best-effort and fail-safe — if it cannot run, the conversion still succeeds and the disks simply keep their default state. (Uses Storage cmdlets available on Windows Server 2012+.)

Networking / static-IP scope. After conversion a Windows guest gets a new virtio NIC that comes up with its default (DHCP) configuration. On a CloudStack-managed network this is the intended, deterministic outcome — the source MAC is preserved and the virtual router hands the guest exactly the IP CloudStack allocated for that NIC (a DHCP reservation keyed on the MAC), so no extra action is needed. Automatically re-applying an in-guest static IP (for DHCP-less networks, or guests that must remain internally static) is intentionally out of scope here — doing it reliably needs guest-registry inspection to distinguish static from DHCP and recover prefix/gateway/DNS, which is better suited to a dedicated follow-up. For those cases the operator supplies the address via nicipaddresslist and, on a DHCP-less destination, reconfigures networking inside the guest after migration.

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Bug fix
  • Enhancement

Feature/Enhancement Scale

  • Major
  • Minor

How Has This Been Tested?

  • Unit tests for all new and touched wrappers/managers/orchestration:
    LibvirtImportConvertedInstanceCommandWrapperTest, LibvirtConvertInstanceCommandWrapperTest,
    LibvirtCheckConvertInstanceCommandWrapperTest, LibvirtCheckVolumeCommandWrapperTest,
    the LibvirtVmwareCbt* suites, UnmanagedVMsManagerImplTest,
    VolumeImportUnmanageManagerImplTest, the server-side VmwareCbt* suites,
    and VolumeOrchestratorTest — all passing.

  • Manual/lab validation of warm CBT migration and direct-RBD import against a Ceph/RBD KVM cluster.

  • Linstor destination validated byte-identical on a live 3-node LINSTOR/DRBD lab against a real VMware source (cold direct-VDDK and warm CBT; qemu-img compare = images identical), and end-to-end through a live importVm that produced a Stopped CloudStack VM with its root and data disks as Linstor volumes backed by DRBD resources UpToDate on all three nodes. New Linstor unit tests across the destination gate lists, host selection, the convert/import/CBT wrappers, listPhysicalDisks, and the block-device delta/nbdcopy paths — all passing.

  • Linstor adoption (root + data) covered by new unit tests: CheckVolume inspection via the /dev/drbd device path (never an rbd: URI), the in-use-on-another-node refusal, and the Linstor ⇒ RAW format resolution — all passing; and validated live end-to-end on the 3-node LINSTOR lab: an externally-prepared Linstor root + data set adopted through importVm and the resulting VM started (adopt-and-boot), with the volumes backed by DRBD resources UpToDate on all three nodes.

  • Full functional pass on a two-host KVM cluster against all three destination types (NFS, Ceph/RBD, Linstor): cold direct-VDDK imports (dual-disk, single-disk, and with nbdcopy removed to exercise the qemu-img fallback), warm CBT migrations on each destination, volume adoption, native VM lifecycle, and negative/guard-rail cases. Boot was confirmed from the guest console rather than from CloudStack VM state, and data integrity by running sha256sum inside each migrated guest against checksums recorded at the source — including files written into the running source between delta cycles, which confirms the delta path carries mid-replication writes. Delta cycles were also measured: against a 25 GiB disk set an idle cycle moved ~2.9 MiB (0.011 %) and cycles tracked the volume actually written (write 96 MiB → ~101 MiB transferred), confirming changed-block transfer rather than full re-copy. Windows Server 2012 R2 was migrated cold and warm and booted in both cases, with the first-boot disk-online script confirmed firing.

Operator-driven warm VMware-to-KVM migration built on VDDK and VMware
Changed Block Tracking (CBT). Keeps a source-equivalent KVM-side replica
current through delta cycles, then finalizes with virt-v2v in-place at
cutover. Supports both filesystem (qcow2) and native Ceph/RBD (raw image)
primary storage targets, with preflight validation, async lifecycle
operations, persisted migration state, and Import/Export UI integration.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
Extends the existing VDDK + virt-v2v VMware-to-KVM import path so the
converted disk can be written straight into an RBD pool as a raw image,
with in-place virt-v2v finalization. Adds host-capability detection for
qemu-img RBD support, RBD qemu copy, virt-v2v in-place, and direct RBD
import, advertised via ReadyCommand and reconciled by the agent manager.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
Extends the KVM unmanaged import / manage-volume flow so an existing raw
RBD image can be adopted directly as a VM root volume, including the
RBD-format check in the volume orchestrator and the libvirt volume
inspection path.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
Unifies the host-capability keys that the CBT warm-migration and VDDK
direct-RBD-import features introduced independently under different
names, using feature-neutral names that describe what is probed:
  host.vmware.cbt.support                     -> host.vddk.blockcopy.support
  host.vmware.cbt.in.place.finalization.support -> host.vddk.blockcopy.inplace.finalization.support
  host.vmware.cbt.rbd.support                 -> host.vddk.blockcopy.rbd.support
  host.qemu.img.rbd.support                   -> host.qemu.rbd.support
  host.virt.v2v.inplace.support / host.virtv2v.in.place.version -> host.virtv2v.inplace.*
Drops the redundant host.rbd.qemu.copy.support alias (identical to
host.qemu.rbd.support) and its hostSupportsRbdQemuCopy() delegate.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
@andrijapanicsb andrijapanicsb added this to the 4.24.0 milestone Jul 21, 2026
@andrijapanicsb andrijapanicsb changed the title KVM/VMware: warm CBT migration and direct VDDK import into Ceph/RBD, plus importVm adoption of existing RBD root volumes Jul 21, 2026
andrijapanicsb and others added 8 commits July 21, 2026 06:58
…isk import

The "Manage Instances" source dropdown offered "Import QCOW2 image from
Local/Shared Storage", but importVm now adopts an existing ROOT disk from
Filesystem, NetworkFilesystem, SharedMountPoint and RBD (raw) pools, so the
QCOW2-only wording is misleading. Rename both entries and their wizard
title/description to "Import Instance using existing ROOT disk from
Local/Shared Storage", across the en, pt_BR and te locales.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
docs/vmware-cbt/architecture.md and docs/vmware-cbt/README.md were added
without the Apache license header, which failed the RAT license check and the
pre-commit insert-license hook. Add the standard ASF header (identical to the
one already present in docs/vmware-cbt-migration.md).

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
…ey names

State Apache CloudStack 4.24.0.0 as the target release (schema delivered via the
4.24.0.0 upgrade path, with a note that the DDL currently rides in
schema-42210to42300.sql pending the main version bump), and replace the
pre-consolidation host-capability key names with the current
host.vddk.blockcopy.* / host.virtv2v.inplace.version names.

Signed-off-by: Andrija Panic <andrija.panic@gmail.com>
Staged VMware-to-KVM imports (virt-v2v conversion to an NFS temporary
location followed by a copy to the destination pool) can now target
Linstor primary storage:

- Linstor added to the staged-conversion destination pool types, so
  destination pool discovery considers cluster- and zone-wide Linstor
  pools alongside NFS and RBD.
- The import host is validated to have access to each Linstor
  destination pool (it must be a LINSTOR satellite connected to the
  pool); the RBD qemu-copy capability check is preserved and the
  validation renamed to validateStagedImportHostSupport.
- The KVM-side converted-disk move already dispatches by pool type to
  LinstorStorageAdaptor.copyPhysicalDisk, which spawns the resource
  from the pool's resource group and qemu-img converts the qcow2 into
  the DRBD block device; converted-disk metadata for Linstor pools now
  reports the pool UUID and volume name instead of parsing NFS mounts
  (Linstor pools have no local mount path).

The imported volume keeps the copy-time UUID as its path; the Linstor
driver addresses existing volumes by path, so later volume lifecycle
operations (attach, resize, snapshot, delete) work unchanged.
Extends the direct (non-staged) VDDK import path, previously available
only for Ceph/RBD, to Linstor primary storage. With usevddk=true and
forceconverttopool=true on a Linstor conversion pool, the KVM host
copies each VMware disk over nbdkit/VDDK straight into a pre-created
DRBD block device and finalizes it with virt-v2v in place - no NFS
staging and no intermediate qcow2 copy.

Server side:
- Linstor added to the VDDK direct-conversion pool allow-list; the
  RBD/Linstor "requires usevddk" validation is shared.
- Conversion host selection for Linstor requires VDDK support,
  in-place virt-v2v support, and access to the Linstor pool (the host
  must be a LINSTOR satellite); explicit host selections are validated
  for pool access as well.
- CheckConvertInstanceCommand carries a new in-place finalization
  check so unsupported hosts fail fast with a clear message.
- The powered-off, non-cloned source VM requirement now applies to any
  direct block-storage import.

KVM agent side:
- The direct conversion routine handles both targets: for Linstor it
  pre-creates each resource at the source capacity through the storage
  adaptor (qemu-img cannot create DRBD devices, unlike RBD images),
  copies with qemu-img convert -n to the local DRBD device, and feeds
  virt-v2v-in-place a <disk type='block'> domain XML - no qemu URI or
  RBD-style network disks needed.
- Storage access is probed with a 4 MiB test volume and a qemu-io
  write/read round-trip before any data transfer.
- Linstor disk names use a shorter '-dNN' suffix: LINSTOR resource
  names are capped at 48 characters, which the RBD '-disk-NNN' scheme
  would exceed.
- The forced-conversion import short-circuit accepts Linstor temporary
  locations, backed by a new LinstorStorageAdaptor.listPhysicalDisks
  implementation (resource definitions filtered by the pool's resource
  group), replacing the previous UnsupportedOperationException.
Extends the VMware CBT warm-migration framework, previously limited to
qcow2 file and Ceph/RBD targets, with a generic raw-block-device target
type backed by Linstor primary storage. The initial full sync, the
incremental CBT delta cycles and the powered-off cutover all write
straight into the DRBD block device of a pre-created Linstor volume.

Target classification and orchestration:
- New VmwareCbtTargetStorageType.RAW_BLOCK_DEVICE; Linstor pools
  classify as supported block-device targets that require in-place
  virt-v2v finalization (the qcow2 fallback cannot write to devices).
- Linstor added to the CBT-compatible pool types for explicit and
  implicit destination pool selection.
- Conversion host selection requires VMware CBT migration support,
  in-place finalization support and access to the Linstor pool (the
  host must be a LINSTOR satellite); both auto-selection filtering and
  explicit host validation enforce it.
- The destination storage probe now covers block-device pools: the
  agent creates a 4 MiB volume through the storage adaptor, round-trips
  a qemu-io write/read on the local device and deletes it.

Naming: block-device target volumes are named cbt-<mig8>-<diskId>,
where mig8 is the first eight characters of the migration UUID. LINSTOR
resource names are limited to 48 characters (the volume name gets a
cs- prefix as a LINSTOR resource), so the RBD-style names carrying the
full migration UUID do not fit. The short marker keeps cleanup guarded:
both the management server and the agent only delete volumes whose
names start with the marker derived from the migration UUID.

Agent-side data path:
- Initial sync pre-creates each Linstor volume at source capacity via
  the storage adaptor (qemu-img cannot create DRBD devices) and runs
  the nbdkit/VDDK captive copy with qemu-img convert -n to the local
  device path.
- Delta cycles resolve the device path and patch changed extents with
  the existing qemu-io write mechanism; raw device targets are accepted
  by disk validation.
- Cutover feeds virt-v2v-in-place a <disk type='block'> domain XML with
  the local device path - the qemu-nbd localhost bridges required for
  RBD are unnecessary for block devices.
- Cleanup deletes only marker-guarded volumes through the adaptor.

The finalized volumes are imported through the existing shared path;
imported Linstor volumes keep the target name as their volume path,
which the Linstor driver uses to address existing volumes.
Covers the release note for the three import modes (staged, direct
VDDK, CBT warm migration), the operator guide's destination-specific
host requirements and operational notes (satellite/node naming, single
Primary writer, diskful replica preference, DRBD size rounding,
heartbeat fencing interaction, retained volume names), and the
as-built design guide's storage-target section (RAW_BLOCK_DEVICE
mapping, pre-created devices, convert -n, block-disk XML finalization,
48-character resource-name constraint and the short cleanup marker).
Found during live validation on an Ubuntu 24.04 LINSTOR lab node:
virt-v2v-in-place 2.4.0 does not know the -O (write updated output
XML) option, which only exists from virt-v2v 2.5 on (EL9 ships 2.5.x,
where the RBD flows were originally validated). Both the VMware CBT
cutover finalization and the direct VDDK import finalization passed -O
unconditionally, so otherwise fully capable hosts failed the in-place
step at the very end of a migration.

Nothing ever consumed the output XML - both wrappers created a
temporary file for it and deleted it unread - so the option and the
temporary files are removed rather than gated on a version probe.
Verified live: virt-v2v-in-place 2.4.0 with the same libvirt XML
converts a guest on a DRBD block device successfully without -O.
…s can be deleted

cancelVmwareCbtMigration already removes the migration's target images from the
destination pool, but it leaves the migration record in place. Deleting the record
afterwards runs the cleanup a second time, because deleteVmwareCbtMigration
defaults cleanup to true, and that second pass failed on targets which were
legitimately already gone. The result was that cancelled migration records could
never be removed, even though no storage was left behind.

deletePhysicalDiskWithRetries now treats a target as successfully deleted when the
destination pool listing confirms it is no longer present. Absence is only reported
when it can be positively confirmed; if the pool cannot be inspected the target is
assumed to still exist, so genuine cleanup failures are still surfaced rather than
masked. This applies to both the RBD and the Linstor cleanup paths, and also covers
failed migrations whose cleanup only partially completed.
Making cleanup idempotent changed the contract of the cleanup wrapper: a target
the destination pool no longer lists is now treated as successfully removed. The
existing failure test stubbed only deletePhysicalDisk and left listPhysicalDisks
unstubbed, so the pool appeared empty and the target read as already absent,
which is not what that scenario describes.

The test now states its premise explicitly - the image is still listed on the
pool, so the deletion genuinely failed and cleanup must report a failure. Two
further tests pin the rest of the contract: a target the pool no longer lists is
reported as cleaned up, and a pool that cannot be inspected still yields a
failure, so absence is never merely assumed.
@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@blueorangutan package kvm

@blueorangutan

Copy link
Copy Markdown

@andrijapanicsb a [SL] Jenkins job has been kicked to build packages. It will be bundled with kvm SystemVM template(s). I'll keep you posted as I make progress.

@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from blueorangutan Jul 31, 2026
@apache apache deleted a comment from codecov Bot Jul 31, 2026
@andrijapanicsb

andrijapanicsb commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Functional test report — VMware to KVM cold/warm migration, RBD and Linstor destinations

Full end-to-end functional test of this PR, run on a two-host KVM cluster against three destination
storage types: NFS, Ceph/RBD and Linstor.

Result: 15 of 15 planned test cells pass.

All cells were run from scratch on the final build. Source VMs were migrated directly. No clones or
throwaway copies were used, and all source VMs were left intact.

Guests used: Rocky Linux 9.8 (2 disks), Ubuntu 26.04 LTS (1 disk), Windows Server 2012 R2 (2 disks).


How results were verified

A VM was only recorded as booted when the guest itself provided the evidence. CloudStack reporting
state=Running was not accepted, because that only means the libvirt domain exists and says nothing
about whether the operating system came up.

Check Evidence used
Disk order and placement listVolumes: ROOT = deviceid 0, DATADISK = deviceid 1, on the expected pool
Boot Guest console screenshot showing a login prompt, and/or virsh domhostname answered by the in-guest agent
Data integrity sha256sum run inside the migrated guest, compared against the value recorded at the source

Data integrity method

Each source data disk carries a test file (seed1.bin) together with a manifest file holding its
sha256sum. For the warm migrations, additional test files were written into the running source
guest while replication was in progress
, in between delta cycles, and their sha256sum values were
recorded at the source at the time of writing.

After cutover, each migrated VM was booted and the checksums were computed again, from inside the
migrated guest, on the migrated volumes. In every cell the checksums matched the values recorded at
the source exactly, including the files that were created after the initial full sync had completed.
This confirms that both the initial copy and the subsequent delta cycles transferred the data
correctly and completely.


Summary

# Cell Destination Result
C1 Cold direct-VDDK import, 2-disk source Ceph/RBD PASS
C2 Cold direct-VDDK import, 2-disk source Linstor PASS
C3 Cold direct-VDDK import, 2-disk source NFS (qcow2) PASS
C4 Cold direct-VDDK import, 1-disk source Ceph/RBD PASS
C5 Cold import with nbdcopy removed (fallback path) Ceph/RBD PASS
W1 Warm CBT migration Ceph/RBD PASS
W2 Warm CBT migration NFS PASS
W3 Warm CBT migration Linstor PASS
A1 Adopt a pre-existing volume (importsource=shared) Ceph/RBD PASS
R1 Native (non-imported) VM lifecycle RBD + Linstor PASS
N1 Cutover while source still powered on, expect rejection PASS
N2 Cold import of a powered-on source, expect rejection PASS
N3 Cancel an in-flight warm migration, expect no orphans Ceph/RBD PASS
N4 Warm start without a disk-offering mapping, expect rejection PASS
N5 Import with fewer NIC mappings than source NICs, expect rejection PASS
X1 Windows Server 2012 R2, cold import Ceph/RBD PASS (boots)
X2 Windows Server 2012 R2, warm CBT Ceph/RBD PASS (boots)

Disk ordering

The ordering behaviour is correct on every destination type. In all dual-disk cells the result was
deviceid 0 = ROOT and deviceid 1 = DATADISK, with the expected sizes and both volumes on the
requested pool.

The single-disk source produced exactly one volume, with no additional data disk created. This was
confirmed from inside the guest, where lsblk showed a single disk and no second device.


Warm CBT: delta cycles transfer only changed blocks

This was measured rather than assumed. The full disk set per VM is 20 GB root plus 5 GB data, i.e.
26,843,545,600 bytes (25 GiB). If a sync cycle were re-copying the whole image, every cycle would
move about 26.8 GB. Measured changedbytes per cycle:

Destination Cycle changedbytes Block ranges % of full 25 GiB Data written into the source before that cycle
RBD 1 70,516,736 (~67 MiB) 41 0.26 % 64 MiB
RBD 2 72,744,960 (~69 MiB) 63 0.27 % (final cutover cycle)
NFS 1 3,080,192 (~2.9 MiB) 35 0.011 % nothing, guest idle
NFS 2 53,673,984 (~51 MiB) 38 0.20 % 48 MiB
NFS 3 55,115,776 (~52.6 MiB) 50 0.21 % (final cutover cycle)
Linstor 1 4,456,448 (~4.2 MiB) 51 0.017 % nothing, guest idle
Linstor 2 4,587,520 (~4.4 MiB) 52 0.017 % nothing, guest idle
Linstor 3 106,037,248 (~101.1 MiB) 61 0.40 % 96 MiB

The Linstor rows come from a single migration and vary only the amount written into the running guest:
two consecutive idle cycles moved 4.2 MiB and 4.4 MiB, then 96 MiB was written into the source and the
next cycle moved 101.1 MiB.

Two results follow from this. An idle cycle moving 2.9 MiB out of 25 GiB is not a full re-copy, and
the amount transferred tracks the amount actually written, with the small excess accounted for by
filesystem metadata and normal OS activity.

CloudStack reports this per cycle, for example:

VMware CBT cycle 1 ... copied 35 changed block range(s), coalesced into 35 copy range(s) across 2 disk(s), totaling 3080192 bytes.

together with a dirtyrate value.


Data path coverage

nbdcopy path and qemu-img fallback, with log output

With nbdcopy available, RBD copies use it over a qemu-nbd bridge:

Copying VMware disk <src> to RBD image <pool>/<image> via nbdcopy over qemu-nbd bridge

and Linstor writes directly to the DRBD device:

nbdkit -r -U - vddk file=<src> ... compression='fastlz' \
  --run 'nbdcopy --destination-is-zero "$uri" /dev/drbd/by-res/<res>/0'

For cell C5, nbdcopy was removed from the conversion host. The probe failed and the code fell back
to qemu-img, for both disks:

Executing command [/bin/bash -c nbdcopy --version ]  -> Exit value ... is [127]   (WARN)
nbdkit -r -U - vddk file=<src> ... compression='fastlz' \
  --run 'qemu-img convert -f raw -O raw "$uri" rbd:<pool>/<image>...'

The resulting VM booted and the checksum taken inside the guest matched the source value, so the
fallback path produces the same data as the nbdcopy path. VDDK compression='fastlz' is active in
both cases.


Adoption of a pre-existing volume

A standalone bootable RBD image was created outside CloudStack and then adopted with:

importVm importsource=shared hypervisor=KVM storageid=<pool> diskpath=<image> networkid=<net>

The resulting ROOT volume's path is the pre-existing image name, so the volume was adopted in place
with no copy and no data movement. The VM booted, and the root filesystem was mounted with the
expected size when checked inside the guest.

What the import covers, and what it does not. On the shared/local disk path, importVm gives you
one ROOT volume and one NIC. The code builds a single NIC from the single networkid, and only handles
the ROOT volume. The nicnetworklist, nicipaddresslist and datadiskofferinglist parameters are
accepted by the API but ignored here — they only apply to the VMware/external import paths.

So if the VM has more than one disk, or more than one network, you need extra API calls after the
import:

To add Call
More data volumes listVolumesForImportimportVolumeattachVolume
More NICs addNicToVirtualMachine (and updateDefaultNicForVirtualMachine if the default should change)
A different disk controller, if it was not set at import updateVirtualMachine with details (rootDiskController)

This matters when migrating a VM with several disks and NICs: the import gets you the root disk and one
network, and you add the rest yourself. Setting details[0].rootDiskController=virtio during the import
saves you one of those steps.


Negative testing cases

All rejections were returned quickly, and each message identifies both the problem and the required
action.

N1errorcode 431: Cannot cut over VMware CBT migration <id> while source VM <vm> is in power state PowerOn. Gracefully shut down the source VM, then retry cutover.

N2errorcode 530: Direct VDDK import to a block storage pool is supported only for powered-off VMware VMs. Please power off VM <vm>, or disable forceconverttopool and use staged import with temporary conversion storage.

N4errorcode 431: Source VM <vm> has 1 data disk(s) but 0 disk offering mapping(s) were provided. Provide one datadiskofferinglist entry per data disk so the cutover import can place every disk.

N5errorcode 530: Different number of nics found on instance <vm>: 2 vs 1 nics provided.

N4 and N5 are both validated at start, within seconds, rather than after a full replication or a large
conversion has already run.

N3 (cancel) was checked by inspecting the storage rather than relying on the API result. During
replication the pool contained the migration's target images. After cancelVmwareCbtMigration there
were zero images matching that migration id, the source hypervisor snapshot had been removed, no
detached or orphaned volumes remained, and no partial VM record was left behind.


Windows guests

Scope for the Windows cells was boot verification. The original Windows Server 2012 R2 source was
migrated directly, without cloning, and left intact afterwards. It has a 40 GB root disk and a 5 GB
data disk.

Cold import (X1)

  • Disk order correct: ROOT deviceid 0, DATADISK deviceid 1.
  • Boots: the console reached the Windows lock screen with a live clock. The guest also switched the
    console to 1024x768, showing the display driver had loaded, and block statistics showed real guest
    I/O.
  • The Windows disk-online first-boot script is injected as intended:
(<vm>) Windows guest detected; injecting first-boot script to online migrated disks: ...
(<vm>) virt-v2v: Converting Windows Server 2012 R2 Standard (win2k12r2) to run on KVM
(<vm>) virt-v2v: Installing firstboot script: .../cloudstack-first...

Warm CBT migration (X2)

Change tracking was enabled on the source, the initial full sync completed with the source powered on,
a delta cycle ran, the source was gracefully shut down and cutover completed with no errors. Disk
order was correct and the migrated guest booted to the Windows lock screen. The delta cycle moved
16,580,608 bytes (~15.8 MiB) against a 45 GiB disk set, which is 0.035 %.

In-guest networking and data contents were out of scope for the Windows cells.


Notes

Disk controller on adopted volumes. Adopting a volume without further parameters produced
<target dev='hdb' bus='ide'/>. Passing details[0].rootDiskController=virtio produced
<target dev='vdb' bus='virtio'/> as expected. When adopting a volume it is therefore advisable
either to set a guest OS type that implies virtio support, or to pass the disk controller explicitly
in details, so that the VM does not end up on the IDE bus.

Disks are copied one at a time. On both the cold and warm paths, disks are converted sequentially
rather than in parallel. This was confirmed by process inspection, where only one copy pipeline exists
at any moment and references a single disk, and by log timestamps, where the second disk starts only
after the first pipeline has exited. Total migration time is therefore the sum of the disks rather
than the duration of the largest one.


Scope of this testing

  • All operations were driven through the API. The migrations were observable in the UI while they ran,
    but the UI wizards were not used to drive them, so this report does not cover whether the UI collects
    and passes the parameters the API requires.
  • Migrations were run one at a time. Concurrent migrations were not tested.
  • Only the direct-to-pool conversion path (forceconverttopool=true) was exercised. That was
    deliberate, since the goal was to confirm the most efficient pool-to-pool path; it requires a recent
    conversion host (EL9 or newer, with a current virt-v2v). The staged import path using temporary
    conversion storage was not exercised.
  • Volume adoption (importsource=shared) was exercised on Ceph/RBD and Linstor.
@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✖️ debian ✔️ suse15. SL-JID 18725

@andrijapanicsb

Copy link
Copy Markdown
Contributor Author
@blueorangutan

Copy link
Copy Markdown

@andrijapanicsb a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@blueorangutan test rocky9 kvm-rocky9 keepEnv

@blueorangutan

Copy link
Copy Markdown

@andrijapanicsb a [SL] Trillian-Jenkins test job (rocky9 mgmt + kvm-rocky9) has been kicked to run smoke tests

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.00316% with 3142 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.77%. Comparing base (fc83241) to head (00f101b).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
.../wrapper/LibvirtConvertInstanceCommandWrapper.java 17.79% 380 Missing and 8 partials ⚠️
.../vmware/manager/VmwareCbtMigrationServiceImpl.java 0.28% 354 Missing and 1 partial ⚠️
...c/main/java/com/cloud/vm/VmwareCbtMigrationVO.java 9.45% 201 Missing ⚠️
...ervisor/kvm/resource/LibvirtComputingResource.java 8.43% 150 Missing and 2 partials ⚠️
...i/command/admin/vm/StartVmwareCbtMigrationCmd.java 0.00% 140 Missing ⚠️
.../apache/cloudstack/vm/UnmanagedVMsManagerImpl.java 55.88% 97 Missing and 23 partials ⚠️
...wrapper/LibvirtVmwareCbtCutoverCommandWrapper.java 77.92% 82 Missing and 37 partials ⚠️
...wrapper/LibvirtVmwareCbtPrepareCommandWrapper.java 64.50% 85 Missing and 30 partials ⚠️
...ce/wrapper/LibvirtVmwareCbtSyncCommandWrapper.java 67.06% 82 Missing and 28 partials ⚠️
.../response/VmwareCbtMigrationPreflightResponse.java 0.00% 97 Missing ⚠️
... and 61 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #13656      +/-   ##
============================================
+ Coverage     19.64%   19.77%   +0.13%     
- Complexity    19789    20204     +415     
============================================
  Files          6368     6420      +52     
  Lines        575109   582046    +6937     
  Branches      70371    71206     +835     
============================================
+ Hits         112994   115115    +2121     
- Misses       449829   454446    +4617     
- Partials      12286    12485     +199     
Flag Coverage Δ
uitests 3.39% <ø> (-0.02%) ⬇️
unittests 21.05% <38.00%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment