This document describes the VMware Changed Block Tracking (CBT) migration design and implementation for Apache CloudStack 24.0.0. It is written as source material for an Apache CloudStack CWIKI architecture page and should be treated as an implementation guide, not only as a design sketch.
The feature provides a warm VMware-to-KVM migration flow. CloudStack creates a KVM-side source-equivalent disk replica from a VMware VDDK snapshot, keeps that replica current with one or more VMware CBT delta cycles, requires the operator to power off the VMware source VM, runs one final CBT delta cycle, finalizes the replica with virt-v2v, and imports the finalized disk(s) as a regular CloudStack KVM VM.
The essential architectural point is that VMware CBT migration is a two-phase disk strategy:
virt-v2v only after the source VM is powered off.This gives CloudStack warm migration behavior without trying to apply VMware changed blocks to a disk that has already been transformed for KVM, and without requiring a second full VMware read at cutover time.
The existing VMware import paths are useful for one-time imports, but they do not provide a warm migration loop where the target disk is kept current while the source VM continues to run on VMware. Large VMs therefore either require a longer downtime window or repeated full-copy style attempts.
VMware CBT provides the changed-block metadata needed for a replication-style workflow, but the design must avoid a common trap: once virt-v2v modifies a disk for KVM guest bootability, VMware CBT byte ranges can no longer be safely applied to that transformed disk. The implementation must therefore keep the replication target source-equivalent until final cutover, then run guest conversion only after the final delta is applied.
qemu-img.Current implementation scope:
virt-v2v fallback for QCOW2 file targets, gated by global configuration.details keys used by the existing OVF/VDDK import flow.Not implemented or intentionally blocked:
virt-v2v fallback for Ceph/RBD targets. RBD targets require in-place finalization because fallback produces local QCOW2 files.qemu-img convert. The UI currently receives state transitions, disk target paths, delta cycle byte counts, dirty rates, and cycle descriptions through the list API.virt-v2v runs only at finalization, after the final CBT delta cycle.virt-v2v work is local to the KVM-side replica and does not perform a second full VMware read.listVmwareCbtMigrations, not only the async job result.ReadyForImport state.The implementation has four main layers:
virt-v2v finalization, and cleanup.Management-server API and orchestration:
StartVmwareCbtMigrationCmdCheckVmwareCbtMigrationPrerequisitesCmdListVmwareCbtMigrationsCmdSyncVmwareCbtMigrationCmdCutoverVmwareCbtMigrationCmdCancelVmwareCbtMigrationCmdDeleteVmwareCbtMigrationCmdVmwareCbtMigrationManagerImplVmwareCbtStorageTargetVmwareCbtMigrationCutoverPolicyPersistence:
cloud.vmware_cbt_migrationcloud.vmware_cbt_migration_diskcloud.vmware_cbt_migration_cycleKVM agent wrappers:
LibvirtVmwareCbtPrepareCommandWrapperLibvirtVmwareCbtSyncCommandWrapperLibvirtVmwareCbtCutoverCommandWrapperLibvirtVmwareCbtCleanupCommandWrapperUI:
ui/src/views/tools/ImportUnmanagedInstance.vueui/src/views/tools/ManageInstances.vueui/src/views/tools/VmwareCbtMigrations.vueThe diagrams below are intentionally plain text so they can be pasted directly into Apache CWIKI source and remain editable.
VMware source VM | | VMware snapshot + CBT metadata v VDDK / nbdkit-vddk on KVM conversion host | | Initial baseline: full logical disk read v CloudStack primary storage | | Source-equivalent replica | - qcow2 file on filesystem-like storage | - raw RBD image on Ceph/RBD storage v Delta sync cycles | | CBT changed ranges only v Operator powers off source VM | | Cutover: final delta + virt-v2v finalization v CloudStack KVM VM
VMware CBT initial sync is not guest-filesystem-used-space-oriented. CloudStack does not inspect the guest filesystem to decide which files are in use; it reads the disk image exposed by VDDK/NBD and writes the target replica.
The amount of data transferred depends on what VDDK exposes for the source disk and datastore. Some source paths expose the baseline as one allocated logical range, while others expose sparse hole,zero extents. The destination can remain thin or sparse when the source and copy path expose zero ranges correctly, but operators must size the initial-copy window for the full logical disk capacity.
The KVM agent uses qemu-img convert for the general full-copy path. When nbdcopy is installed, it can accelerate supported pre-created raw targets and RBD targets served through a local qemu-nbd bridge. nbdcopy is optional; capability and correctness do not depend on it.
Example VMware disk capacity: 5 GiB Guest-used data: near 0 GiB Datastore/path behavior: datastore-dependent During initial sync: VMware/VDDK exposes allocation/extents to NBD. CloudStack reads the ranges that qemu sees through that NBD export. qemu-img writes a qcow2 target and can preserve thin/sparse output. Possible outcome: full logical transfer or sparse-aware transfer Target allocation: backend- and extent-dependent
The initial baseline reads the logical disk presented by VDDK. Linked clone vs full clone is not enough by itself to predict transfer volume. The decisive question is whether VDDK/NBD exposes trustworthy allocation and zero extents for the source datastore and disk chain.
Linked clone source child delta VMDK | v parent/base VMDK chain | v VDDK presents one logical disk Full clone source single/full VMDK | v VDDK presents one logical disk CBT initial sync: both are read as the full logical disk capacity. The target qcow2 can still be sparse/thin after the write.
Initial sync completed | v Delta cycle 1 -> changed bytes / dirty rate measured | v Delta cycle 2 -> quiet-cycle counters updated | v Repeat until one boundary is met: min cycles satisfied AND quiet bytes threshold satisfied AND quiet dirty-rate threshold satisfied AND quiet cycle count satisfied OR max cycle count reached | v Migration becomes ReadyForCutover
Final CBT delta sync | v Check selected conversion host capability | +--> virt-v2v-in-place available | | | v | run virt-v2v-in-place finalization | +--> virt-v2v supports --in-place | | | v | run virt-v2v --in-place finalization | +--> non-in-place fallback explicitly allowed | v run regular virt-v2v -o local fallback finalization Result: finalized qcow2 moved to primary storage root VM imported with VirtIO root disk controller
During sync: /mnt/<pool-uuid>/cloudstack-cbt/<migration-uuid>/... After finalization/import: /mnt/<pool-uuid>/<generated-volume-uuid>.qcow2 CloudStack DB target path: updated to the final primary-storage-root location
checkVmwareCbtMigrationPrerequisites validates the source and destination before a long-running job is started. The command is synchronous and returns a structured preflight response with pass, warn, and fail findings.
The preflight checks include:
existingvcenterid or external vCenter fields.The preflight response includes, among other fields:
readystoragewritertypestoragewritersupportedstoragerequiresinplacefinalizationconvertinstancehostinplacefinalizationsupportednoninplacefinalizationfallbackallowednoninplacefinalizationfallbacksupportedsourcecpunumbersourcecpuspeedsourcememorysourceguestosidsourceguestosdiskfindingIf CBT is supported but not enabled, preflight reports a warning. The start path can enable CBT before creating the baseline snapshot.
The source guest OS fields are populated from the VMware inventory and are used to decide whether Windows-specific conversion prerequisites apply. They do not change the migration mode or disk-copy behavior by themselves.
VMware exposes more than one guest OS signal. The VM's configured guest OS option can differ from the runtime guest OS label reported by VMware Tools. For example, a powered-on VM can show Ubuntu Linux (64-bit) in the vSphere summary while VM Options still show Other (64-bit). CloudStack discovery and preflight therefore treat source guest OS values as inventory facts rather than as a safe automatic target OS selection.
The target CloudStack guest OS is the optional osid supplied to startVmwareCbtMigration and used later by the shared KVM import path. Operators should verify this value before starting migration, especially for generic VMware guest IDs such as ubuntu64Guest, which can represent multiple Ubuntu releases. If the desired target guest OS is not selectable, the corresponding guest_os_hypervisor mapping must exist for the source hypervisor version.
startVmwareCbtMigration is async. It creates the migration record, persists source disk rows, creates a baseline VMware snapshot, prepares target paths, and dispatches the initial full sync to the selected KVM host.
For Windows source VMs, the start path repeats the same conversion dependency check reported by preflight before the baseline snapshot and initial sync are started. This prevents a long CBT copy from producing a disk that cannot be made bootable because the selected KVM conversion host is missing virtio-win.
State transitions:
CreatedInitialSyncReplicating when the initial VDDK full sync completes successfullyFailed if any initial copy or metadata step failsImportant implementation details:
/mnt/<storage-pool-uuid>/cloudstack-cbt/<migration-uuid>/<disk>.qcow2/var/lib/libvirt/images/cloudstack-cbt/<migration-uuid>/<disk>.qcow2nbdkit as password=+<file>.changeId values from the VMware snapshot.syncVmwareCbtMigration is async and serialized by migration ID through BaseAsyncCmd.migrationSyncObject.
Allowed migration states:
ReplicatingReadyForCutoverDelta cycle sequence:
vmware_cbt_migration_cycle row.changeId.QueryingChangedAreas, then CopyingChangedBlocks.changeId.Replicating or move to ReadyForCutover.The cycle response exposes:
cyclenumbersnapshotmorchangedbytesdirtyratedurationstatedescriptioncreatedlastupdatedDelta sync progress is represented as state and cycle metadata rather than a streamed progress bar. While a cycle is running, the row description can show what the manager knows, for example that changed areas are being queried or that a number of ranges has been dispatched. After the agent returns, the cycle shows copied bytes, dirty rate, duration, and a textual summary.
cutoverVmwareCbtMigration is async and serialized by migration ID.
Allowed migration states:
ReadyForCutoverReadyForImportCutover requires the source VM to be powered off. CloudStack checks the VMware power state and rejects cutover if the source VM is still powered on:
Cannot cut over VMware CBT migration <uuid> while source VM <name> is in power state PowerOn. Gracefully shut down the source VM, then retry cutover.
CloudStack does not attempt a graceful shutdown or power-off. The operator must shut down the VMware source VM before final cutover. This avoids surprising the operator and avoids needing to encode site-specific guest shutdown policy in the migration path.
Cutover sequence:
CuttingOver.virt-v2v finalization on the KVM host.ReadyForImport.Completed, store the new CloudStack VM ID, and clear stored external vCenter credentials.ReadyForImport so the operator can retry cutover to repeat only the import step against already finalized disks.cancelVmwareCbtMigration is synchronous. If the migration is not terminal, it sends a best-effort cleanup command, marks the migration Cancelled, and clears stored external vCenter credentials.
deleteVmwareCbtMigration is synchronous. It is allowed for Failed, Cancelled, and Completed migrations. With cleanup=true (the default), it sends a cleanup command before deleting failed or cancelled child rows and the parent row. Completed migration deletion is always record-only: CloudStack removes the CBT bookkeeping rows but never deletes the imported VM, CloudStack volumes, finalized target disks, or primary-storage files.
Deletion behavior:
vmware_cbt_migration_cycle rows.vmware_cbt_migration_disk rows.vmware_cbt_migration row.Failed or Cancelled rows, cleans target files only if the paths are under the migration-specific /cloudstack-cbt/<migration-uuid>/ directory and cleanup is enabled. For RBD targets, cleanup removes only temporary RBD image names containing the CloudStack-owned cloudstack-cbt-<migration-uuid>- marker.Completed rows, ignores cleanup and removes only CBT migration records.The KVM cleanup wrapper resolves migration directories from disk target paths and removes only the migration-specific directory tree for file targets. For RBD targets it uses the destination storage pool and deletes only marked temporary images, never arbitrary RBD images.
The feature deliberately separates replication from final conversion.
Initial full sync does not run virt-v2v. The KVM agent opens the VMware source snapshot through nbdkit with the VDDK plugin. Filesystem-like primary storage writes QCOW2 with qemu-img convert:
nbdkit -r -U - vddk ... --run \ 'qemu-img convert -f raw -O qcow2 "$uri" "<targetPath>"'
Ceph/RBD primary storage is a raw RBD image. When nbdcopy is available, the agent pre-creates the image, exposes it through a temporary local qemu-nbd bridge, and copies NBD to NBD. Otherwise it uses qemu-img convert directly against the qemu RBD URL. LINSTOR similarly uses nbdcopy when available for its pre-created raw DRBD device and falls back to qemu-img convert.
Example RBD fallback path:
nbdkit -r -U - vddk ... --run \ 'qemu-img convert -f raw -O raw "$uri" "rbd:<pool>/<image>:..."'
This produces a source-equivalent replica on the selected primary storage. It is not yet a CloudStack-imported VM disk. It is a replication target that can safely receive raw changed blocks from later CBT cycles.
Delta sync also does not run virt-v2v. Management creates a VMware snapshot, queries changed disk areas using VMware CBT metadata, and dispatches changed byte ranges to the KVM agent. The agent opens the snapshot through nbdkit, parses the temporary nbdkit Unix socket from the NBD URI, uses qemu-img convert --image-opts to materialize bounded raw chunks for only the changed ranges, and patches the replica with qemu-io. File targets use -f qcow2; RBD targets use -f raw against the qemu RBD URL for the target image.
Final cutover is where virt-v2v runs. After the source VM is powered off, the manager runs one final CBT delta cycle so the replica is current. Then the agent runs one of the finalization paths:
virt-v2v-in-place, when available.virt-v2v --in-place, when the installed virt-v2v supports it.virt-v2v -o local, only when the non-in-place fallback is explicitly enabled and the target is a QCOW2 file target.Ceph/RBD targets require one of the in-place finalization paths. They do not allow non-in-place fallback because fallback creates local QCOW2 output rather than modifying the raw RBD image that CloudStack will import.
So there is not a second full VMware read during finalization. The final virt-v2v step is local work against the up-to-date KVM-side replica. It may still require local storage reads and writes, especially in fallback mode, but it is not another VDDK full copy from vCenter.
The initial baseline may read up to the full logical VMware disk range, regardless of guest filesystem usage. This does not imply thick allocation on the destination: QCOW2, RBD, and LINSTOR can remain sparse or thin when the source and copy path expose zero ranges correctly. Delta cycles are CBT-range based and copy only changed ranges.
This design avoids applying future CBT raw block deltas to a disk that has already been modified by virt-v2v. Applying VMware CBT ranges to a converted guest disk would be unsafe because the layout no longer represents the source VMware disk byte-for-byte.
All commands are admin-only.
checkVmwareCbtMigrationPrerequisitesSynchronous preflight command.
Required:
zoneidclusteridsourcevmnameSource vCenter selection:
existingvcenteridvcenter, datacentername, username, passwordOptional:
hostipclusternameconvertinstancehostidconvertinstancestoragepoolidserviceofferingiddetailsThe command has requestHasSensitiveInfo=true because external vCenter credentials can be provided.
startVmwareCbtMigrationAsync command. Starts the migration and initial full sync.
Required:
zoneidclusteridserviceofferingidsourcevmnameSource vCenter selection:
existingvcenteridvcenter, datacentername, username, passwordOptional import inputs:
displaynamehostnameaccountdomainidprojectidtemplateidosiddatadiskofferinglistnicnetworklistnicipaddresslistforcedOptional conversion inputs:
convertinstancehostidconvertinstancestoragepooliddetailsIf convertinstancestoragepoolid is omitted, CloudStack auto-selects the only CBT-compatible primary storage pool in the destination cluster/zone. Supported target pool types are NetworkFilesystem, Filesystem, SharedMountPoint, and RBD. If more than one compatible pool is available, the operator must provide the pool explicitly.
Useful details keys:
vddk.lib.dirvddk.transportsvddk.thumbprintcpuNumbercpuSpeedmemoryThe command has requestHasSensitiveInfo=true and responseHasSensitiveInfo=false.
listVmwareCbtMigrationsSynchronous status command and the main progress source for UI and automation.
Filters:
idzoneidaccountidvcentersourcevmnamestateThe response includes migration-level state, current step, current step duration, last error, cycle counters, changed byte totals, disks, and cycles. currentstepduration is populated for non-terminal migrations and is intended for the UI table marker similar to Import VM Tasks.
syncVmwareCbtMigrationAsync command. Runs one delta sync cycle.
Required:
idOptional:
usernamepasswordFor an external vCenter migration, stored credentials are used when available. If override credentials are supplied, both username and password must be provided. Overrides are stored back to the migration so a UI operator can return later and continue syncing.
cutoverVmwareCbtMigrationAsync command. Runs final powered-off sync, finalizes disks, and imports the VM.
Required:
idOptional:
usernamepasswordThe command fails early if the source VMware VM is not powered off.
cancelVmwareCbtMigrationSynchronous command.
Required:
idCancels a non-terminal migration, attempts cleanup, and clears stored external source credentials.
deleteVmwareCbtMigrationSynchronous command.
Required:
idOptional:
cleanup (default true)Allowed for Failed, Cancelled, and Completed migrations. Completed migration deletes are record-only and never remove the imported CloudStack VM, volumes, finalized target disks, or primary-storage files.
The cutover recommendation is based on global settings:
| Global setting | Default | Meaning |
|---|---|---|
vmware.cbt.migration.min.cycles | 1 | Minimum number of manual/API delta cycles that must complete before quiet-cycle readiness can make the migration eligible for cutover. |
vmware.cbt.migration.max.cycles | 5 | Maximum number of manual/API delta cycles before CloudStack marks the migration ready for cutover even if the quiet-cycle thresholds were not met. |
vmware.cbt.migration.quiet.cycles | 2 | Number of consecutive quiet cycles required before CloudStack marks the migration ready for cutover. |
vmware.cbt.migration.quiet.bytes | 1073741824 | Maximum changed bytes in one cycle for that cycle to be considered quiet. The default is 1 GiB. |
vmware.cbt.migration.quiet.dirty.rate | 16777216 | Maximum changed bytes per second for that cycle to be considered quiet. The default is 16 MiB/s. |
vmware.cbt.migration.agent.command.timeout | 86400 | Timeout in seconds for long-running VMware CBT data-plane commands on the KVM agent. This covers initial full sync, regular delta sync, final delta sync, and cutover finalization. The default is 24 hours. |
The manager tracks:
A cycle is quiet when its changed bytes and dirty rate are below the configured thresholds. Both thresholds must pass. For example, with defaults, a cycle that copies 1.2 GiB at 10 MiB/s is not quiet because the changed-byte threshold is exceeded even though the dirty-rate threshold passed.
Once enough quiet cycles have accumulated, or once the maximum manual/API delta-cycle count is reached, the migration moves to ReadyForCutover.
The operator can still manually run sync cycles while the migration is in Replicating or ReadyForCutover.
The UI intentionally exposes the Cutover button only when the server-side migration state is exactly ReadyForCutover. Before that point the UI exposes Sync delta, because the server has not yet recommended final cutover. If a cutover is started, CloudStack creates one additional final CBT delta cycle inside the cutover operation. Therefore the final displayed cycle count can be one higher than vmware.cbt.migration.max.cycles: for example, five ordinary delta cycles can make the migration ready, and cutover can then record cycle 6 as the final synchronization cycle.
The long-running CBT work is done by KVM agent commands. CloudStack sets the agent command wait value from vmware.cbt.migration.agent.command.timeout, and the KVM wrappers use the same value as the timeout for child processes such as nbdkit, qemu-img, and virt-v2v.
The timeout is finite by design. The shared CloudStack Script helper treats a zero timeout as its default one-hour timeout, so 0 is not a safe way to mean “run forever”. For large source disks, tune the timeout above the expected worst-case duration. A rough estimate is:
timeout_seconds >= source_bytes / expected_bytes_per_second * safety_factor
For example, a 1 TiB initial sync at 100 MiB/s is roughly 3 hours before overhead; at 25 MiB/s it is roughly 12 hours. The default 24-hour timeout is intended to be safe for large migrations while still protecting the agent from permanently stuck child processes.
Related import settings that operators often notice are:
| Global setting | Applies to CBT? | Default | Meaning |
|---|---|---|---|
convert.vmware.instance.to.kvm.timeout | No | 3 | OVF/VDDK import timeout, in hours, for the ConvertInstanceCommand virt-v2v path. CBT cutover has its own timeout because it uses CBT-specific agent commands. |
remote.kvm.instance.disks.copy.timeout | No | 30 | Remote KVM unmanaged import disk-copy timeout, in minutes. It is unrelated to VMware CBT. |
If a CBT command exceeds vmware.cbt.migration.agent.command.timeout, the migration is marked failed with the agent error details in last_error. Failed or cancelled migration records can be deleted with cleanup enabled to remove CloudStack-owned cloudstack-cbt/<migration-uuid> working files.
The feature targets the Apache CloudStack 24.0.0 release, so the schema changes are delivered through the canonical 4.23.0.0 to 24.0.0 upgrade path:
engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sqlengine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.javacloud.vmware_cbt_migrationOne row per migration session.
Important columns:
uuidzone_idaccount_iduser_idvm_idexisting_vcenter_idsource_usernamesource_passworddestination_cluster_idconvert_host_idstorage_pool_iddisplay_namehost_nametemplate_idservice_offering_idguest_os_iddata_disk_offering_mapnic_network_mapnic_ip_address_mapimport_detailsforcedvcenterdatacentersource_hostsource_clustersource_vm_namevddk_lib_dirvddk_transportsvddk_thumbprintstatecurrent_steplast_errorcompleted_cyclesquiet_cyclestotal_changed_byteslast_changed_byteslast_dirty_ratecreatedupdatedremovedsource_password is annotated with @Encrypt in VmwareCbtMigrationVO, using the same CloudStack encrypted DAO field mechanism used by VMware datacenter credentials and VM VNC passwords.
cloud.vmware_cbt_migration_diskOne row per source VMware disk.
Important columns:
migration_idsource_disk_idsource_disk_device_keysource_disk_pathdatastore_namecapacity_bytestarget_pathtarget_formatchange_idsnapshot_morefstateDisk states:
CreatedPreparedSyncingReadyFailedcloud.vmware_cbt_migration_cycleOne row per delta cycle.
Important columns:
migration_idcycle_numbersnapshot_morefchanged_bytesdirty_ratedurationstatedescriptionCycle states:
CreatedQueryingChangedAreasCopyingChangedBlocksCompletedFailedMigration states:
CreatedInitialSyncReplicatingReadyForCutoverCuttingOverReadyForImportCompletedFailedCancelledTerminal states:
CompletedFailedCancelledNormal successful path:
Created -> InitialSync -> Replicating -> ReadyForCutover -> CuttingOver -> ReadyForImport -> Completed
ReadyForImport is intentionally retryable. If the final virt-v2v finalization succeeded but CloudStack VM import failed, the next cutover command does not rerun the final CBT copy and finalization. It retries the VM import against the already finalized disk paths.
VmwareCbtStorageTarget maps primary storage to a target writer:
NetworkFilesystem, Filesystem, and SharedMountPoint use QCOW2_FILE.RBD maps to RBD_RAW. Initial sync writes a raw RBD image with nbdcopy through a local NBD bridge when available, otherwise with qemu-img convert -O raw. Delta sync writes changed ranges with qemu-io -f raw, and cutover finalization is in-place only.Linstor maps to RAW_BLOCK_DEVICE. The agent pre-creates each target volume at source capacity through the Linstor storage adaptor (qemu cannot create DRBD devices the way it creates RBD images), then the initial sync writes the local DRBD device with nbdcopy when available or qemu-img convert -n -O raw otherwise. Delta sync patches changed ranges with qemu-io -f raw against the device path, and cutover finalization is in-place only, using a plain <disk type='block'> libvirt XML (no qemu-nbd bridges are needed for local block devices). Target names are cbt-<mig8>-<disk-id> where mig8 is the first eight characters of the migration UUID: LINSTOR resource names (“cs-” + name) are capped at 48 characters, so the RBD-style naming with the full migration UUID does not fit; the short marker still guards cleanup on both the management server and the agent. The conversion host must be a LINSTOR satellite connected to the pool; host selection and the preflight storage probe (create a 4 MiB volume, qemu-io write/read on the device, delete) enforce this.For QCOW2 file targets, the initial replica is stored under the selected primary storage:
/mnt/<pool-uuid>/cloudstack-cbt/<migration-uuid>/<disk>.qcow2
If no storage pool is selected, the fallback path is:
/var/lib/libvirt/images/cloudstack-cbt/<migration-uuid>/<disk>.qcow2
For Ceph/RBD targets, the initial replica is a raw RBD image in the selected pool:
<ceph-pool>/cloudstack-cbt-<migration-uuid>-<source-disk-id>-<source-name>
Only the image name is persisted as the migration disk target path. The agent reconstructs the qemu RBD URL from the selected storage pool when writing initial and delta data. During in-place virt-v2v finalization, the agent starts a temporary localhost qemu-nbd bridge for each RBD image and gives virt-v2v a libvirt XML disk that points to that local NBD endpoint. This keeps RBD credentials and monitor options in the qemu layer and presents a local NBD endpoint to the conversion appliance.
The preferred cutover finalization is in-place:
virt-v2v-in-place, if present.virt-v2v --in-place, if the installed virt-v2v supports it.If neither in-place method is available, QCOW2 file targets can use regular virt-v2v -o local only when this global configuration is enabled:
vmware.cbt.allow.non.inplace.finalization=true
Default:
false
When fallback is enabled:
TMPDIR and virt-v2v output under the current migration directory on the selected primary storage.Fallback is never allowed for RBD targets. RBD migrations require virt-v2v-in-place or virt-v2v --in-place support on the selected conversion host.
Example fallback output path:
/mnt/<pool-uuid>/cloudstack-cbt/<migration-uuid>/virt-v2v-output-<random>/<migration-uuid>-sda
After successful finalization, both in-place and fallback paths are normalized to the same final layout used by the OVF/VDDK import flow: the KVM agent moves each finalized QCOW2 file to the selected primary storage root and gives it a generated UUID filename:
/mnt/<pool-uuid>/<generated-volume-uuid>
The agent returns the relocated target paths in the cutover answer. The management server persists them in vmware_cbt_migration_disk.target_path through the normal disk-result update path, so no manual database update or schema change is required for this relocation. CloudStack then imports the VM using the flat root-level file name, matching the final placement used by regular OVF/VDDK migrations.
For RBD targets, successful in-place finalization keeps the raw RBD image in place and CloudStack imports it by image name from the selected RBD storage pool. No post-finalization file move is performed, and completed migration deletion remains record-only.
RBD finalization creates only temporary localhost qemu-nbd processes and temporary XML/script files on the conversion host. The wrapper installs a shell trap to stop those bridge processes and remove the PID files after virt-v2v finishes or fails. The persistent storage artifact remains the raw RBD image whose name contains the cloudstack-cbt-<migration-uuid>- marker until CloudStack imports it or cleanup removes it for a failed/cancelled migration.
CloudStack imports finalized CBT disks with the KVM virtio root disk controller. The conversion host must have virtio-win available for Windows guests so that virt-v2v can enable the matching boot driver before the VM is started under KVM. This keeps the persisted rootDiskController detail aligned with the controller model used by the converted guest.
Ceph/RBD support relies on the same KVM host plumbing that normal CloudStack RBD primary storage uses, plus the CBT-specific VMware conversion tools. The selected conversion host must be able to access the destination RBD pool from both Java/libvirt storage code and the qemu command-line tools used by the CBT wrappers.
Required conversion-host capabilities:
librados and librbd client libraries compatible with the Ceph cluster.qemu-img and qemu-io can open rbd:<pool>/<image>:... URLs.nbdkit-vddk for VMware source reads.virt-v2v-in-place or virt-v2v --in-place for RBD cutover finalization.virtio-win or equivalent VirtIO driver media/packages for Windows guests, same as the existing VMware import requirements.CloudStack RBD primary-storage configuration must be valid on the selected KVM/conversion host. For CBT-to-RBD, CloudStack uses the existing KVM storage-pool metadata to build qemu RBD URLs, including monitor addresses, authentication mode, Ceph user, and secret. The CBT qemu path therefore does not rely on /etc/ceph/ceph.conf or a local keyring as its primary credential source when CloudStack supplies explicit RBD URL options.
A local Ceph configuration and keyring can still be useful for operator diagnostics and site-local tooling, for example running ceph -s, rbd ls <pool>, or similar commands without manually passing monitor and authentication options each time. Sites may choose to install those files for operational convenience, but they should not be documented as the required credential source for CBT RBD writes.
The Ceph client packages on every KVM/conversion host should match the Ceph cluster major release, or at least be explicitly supported by that cluster release. Avoid relying on the distribution's stock Ceph packages if they are older than the deployed Ceph cluster. Ancient librbd, librados, qemu RBD block-driver, or Java binding packages can fail in ways that look like CBT errors even though the actual problem is a client/cluster compatibility mismatch.
Practical guidance:
ceph-common, librados, librbd, qemu RBD block-driver packages, and Java RADOS/RBD bindings aligned as a set. Do not mix a modern Ceph cluster with old OS-default client libraries unless the Ceph vendor explicitly supports that combination.qemu-kvm-block-rbd; on Debian/Ubuntu-like systems it is commonly part of qemu block extras.CBT preflight and startVmwareCbtMigration actively probe RBD access when the selected destination pool is RBD. The management server sends the selected conversion host a small probe command using a temporary image named cloudstack-cbt-probe-<uuid>. The KVM agent builds the qemu RBD URL from the existing CloudStack storage-pool metadata, runs qemu-img create, writes and reads a 4 KiB pattern with qemu-io, and removes the temporary image through CloudStack's existing RBD storage adaptor. This catches qemu RBD driver issues, Ceph monitor/authentication problems, and Java RADOS/RBD cleanup failures before the long-running VMware copy starts.
Useful host checks:
ceph -s ceph osd lspools rbd ls <pool> qemu-img --help | grep -i rbd qemu-io --help | grep -i rbd
Useful package/version checks:
ceph --version rbd --version ldconfig -p | egrep 'librbd|librados' java -version
Useful CloudStack database check after the host reconnects:
SELECT h.id, h.name, hd.name, hd.value FROM cloud.host h JOIN cloud.host_details hd ON hd.host_id = h.id WHERE h.name = '<kvm-host-name>' AND hd.name IN ( 'host.vddk.blockcopy.support', 'host.vddk.blockcopy.inplace.finalization.support', 'host.vddk.blockcopy.rbd.support', 'host.vddk.support', 'host.vddk.version', 'host.virtv2v.inplace.version', 'vddk.lib.dir' ) ORDER BY hd.name;
An RBD-capable conversion host should already report VMware CBT capability as true before a migration starts, and the selected destination storage pool must be the RBD pool that the host can access. If these host details are stale after installing packages, restart cloudstack-agent, ensure the management server is running, and wait for host details to be refreshed.
The agent checks these on startup/ready and sends them as host details. Management then persists them into cloud.host_details.
| Host detail | How agent checks it |
|---|---|
host.virtv2v.version | Runs virt-v2v --version. On Ubuntu/Debian it also checks dpkg -l nbdkit. Version is parsed from virt-v2v --version. |
vddk.lib.dir | Reads vddk.lib.dir from agent.properties; if blank/invalid, auto-detects with find / -type d -name 'vmware-vix-disklib-distrib' 2>/dev/null | head -n 1. Valid means <dir>/lib64/libvixDiskLib.so* exists. |
host.vddk.version | Runs nbdkit vddk --dump-plugin libdir=<vddk-lib-dir> and parses vddk_library_version=<n>. |
host.vddk.support | True only if virt-v2v/nbdkit support is present, VDDK dir is valid, and nbdkit-vddk-plugin can load VDDK via nbdkit vddk --dump-plugin libdir=<dir>. |
host.qemu.img.version | Runs qemu-img --version, parses first non-empty line. |
host.qemu.nbd.version | Runs qemu-nbd --version, parses first non-empty line. |
host.qemu.io.version | Runs qemu-io --version, parses first non-empty line. |
host.vddk.blockcopy.support | True only if host.vddk.support is true and qemu-img --version, qemu-nbd --version, and qemu-io --version all exit 0. |
host.vddk.blockcopy.inplace.finalization.support | True only if CBT support is true and either virt-v2v-in-place --version works, or virt-v2v --help 2>&1 | grep -q -- '--in-place' succeeds. |
host.virtv2v.inplace.version | If virt-v2v-in-place exists, parses virt-v2v-in-place --version. If only virt-v2v --in-place exists, reports the normal virt-v2v version. |
host.vddk.blockcopy.rbd.support | True only if in-place finalization support is true and qemu-img --help advertises the rbd block driver. |
Important nuance: host.vddk.blockcopy.rbd.support is only the coarse host capability. For an actual selected RBD pool, preflight/start also runs an active probe: create a temporary cloudstack-cbt-probe-<uuid> RBD image, write/read 4 KiB using qemu-io, then delete it through CloudStack's RBD storage adapter. That catches Ceph auth, monitor, qemu RBD, librados/librbd, and Java binding problems.
Windows source VMs also require virtio-win on the selected KVM conversion host. CBT preflight and startVmwareCbtMigration use the same CheckConvertInstanceCommand path as OVF/VDDK import with Windows guest conversion enabled, so a missing virtio-win package is reported before the initial sync starts.
VDDK detection uses either agent configuration or auto-detection. The agent validates the VDDK library by checking that lib64/libvixDiskLib.so* exists and that nbdkit vddk --dump-plugin libdir=<path> reports a library version.
VDDK options can be provided as API details at migration start:
vddk.lib.dirvddk.transportsvddk.thumbprintIf a value is not provided by the API, the agent uses its configured or detected value. If vddk.thumbprint is not provided, the agent attempts to fetch the vCenter SHA1 certificate thumbprint with openssl.
The implementation does not hard-code one VDDK version. The installed VDDK build must be compatible with the host libraries and pass the agent capability check.
There are two source credential modes:
vmware_data_center record.External credential behavior:
source_password is stored through the CloudStack encrypted DAO field mechanism via @Encrypt.requestHasSensitiveInfo=true.responseHasSensitiveInfo=false.nbdkit password=+<file>, not by embedding the clear-text password in the command line.****** before storing last_error or cycle descriptions.The implementation avoids logging clear-text passwords. As with other CloudStack secret handling, this also depends on callers not placing secrets in unrelated free-form log messages.
CBT migration uses the same eventual import path as the existing OVF/VDDK VM import code. The final CloudStack VM sizing still comes from the selected service offering and any dynamic offering details.
The CBT-specific difference is timing: CBT validates obvious sizing problems early, before the long initial full sync begins. This is meant to avoid spending hours copying disks only to fail final VM import because the selected offering is too small.
The source VM sizing captured during preflight/start includes:
sourcecpunumbersourcecpuspeedsourcememoryValidation rule:
Validated fields:
Fixed offerings:
service_offering.cpu.service_offering.speed.service_offering.ram_size.Dynamic/custom offerings:
cpuNumber, cpuSpeed, and memory.mincpunumber and minmemory can be used where applicable.UnmanagedVMsManagerImpl.addServiceOfferingDetailsToParams.Example custom offering details:
details[0].key=cpuNumber details[0].value=4 details[1].key=cpuSpeed details[1].value=2100 details[2].key=memory details[2].value=4096
This early validation is not intended to be stricter than OVF/VDDK import. It is intended to fail earlier for the same class of sizing mismatch.
The CBT UI is integrated into the Import/Export Instances area.
Implemented UI behavior:
startVmwareCbtMigration job.syncVmwareCbtMigration job.cutoverVmwareCbtMigration job.listVmwareCbtMigrations as the detailed status source.Active states for UI refresh:
CreatedInitialSyncReplicatingCuttingOverThe table exposes:
Initial sync progress is currently coarse. While qemu-img convert runs on the agent, management can show the migration state, current step, target path, and disk state, but not a continuously updated percentage. Delta syncs have better post-cycle metrics because changed bytes and dirty rate are returned by the cycle result.
Initial full sync failure:
Failed.Failed.Delta sync failure:
Failed.Failed.Cutover finalization failure:
Failed.Import failure after successful finalization:
ReadyForImport.Cancel:
Cancelled.Delete:
Failed, Cancelled, or Completed.true, applies only to Failed and Cancelled.Completed deletion is record-only and intentionally preserves the imported VM and all destination storage artifacts.Important security choices:
qemu-nbd, qemu-io, nbdkit, and virt-v2v failures without logging or returning clear-text passwords.Operational caution:
source_username is not encrypted.Relevant unit coverage includes:
VmwareCbtMigrationOfferingValidationTestVmwareCbtMigrationDeletePolicyTestVmwareCbtStorageTargetTestVmwareCbtMigrationCutoverPolicyTestLibvirtVmwareCbtSyncCommandWrapperTestVmwareCbtSyncPlanTestLibvirtVmwareCbtCutoverCommandWrapperTestLibvirtVmwareCbtCleanupCommandWrapperTestLibvirtVmwareCbtRbdProbeCommandWrapperTestLibvirtStoragePoolTestThe tests cover:
qemu-nbd bridges.virt-v2v output path handling.Useful host capability check:
cmk list hosts name=<kvm-host-name> details=all | egrep \ 'host.vddk.blockcopy.support|host.vddk.blockcopy.inplace.finalization.support|host.vddk.blockcopy.rbd.support|host.vddk.support|host.vddk.version|host.virtv2v.version|host.virtv2v.inplace.version|host.qemu.img.version|host.qemu.nbd.version|host.qemu.io.version|vddk.lib.dir'
Useful VDDK plugin check on the KVM host:
nbdkit vddk --dump-plugin libdir=/opt/vmware-vix-disklib-distrib | \ egrep 'vddk_library_version|vddk_dll|vddk_transport_modes'
Useful process check during initial sync:
pgrep -af 'nbdkit|qemu-img|qemu-nbd|nbdcopy'
Useful target disk inspection:
qemu-img info /mnt/<pool-uuid>/cloudstack-cbt/<migration-uuid>/<disk>.qcow2