Copy-Offload Migrations
Copy-offload migrations in mtv-api-tests cover the VMware-to-OpenShift path where MTV can let the storage array move VM disk data instead of using the standard VDDK copy path. In practice, that means a vSphere source provider, shared storage between vSphere and OpenShift, a StorageMap with offloadPlugin.vsphereXcopyConfig, and the right storage credentials.
This project does more than validate a single happy path. The existing copy-offload coverage includes thin and thick disks, snapshots, RDM, multi-datastore layouts, warm migration, non-XCOPY fallback, VM naming edge cases, scale, and concurrent XCOPY/VDDK execution.
What You Need
- A vSphere source provider. The copy-offload validation in this project fails fast for non-VMware sources.
- Shared storage between vSphere and OpenShift.
- A block-backed OpenShift storage class. The copy-offload tests map the destination as
ReadWriteOncewithBlockvolume mode. - MTV installed and healthy.
- A cloneable test VM or template with working guest access. Several scenarios power VMs on, wait for guest info, create snapshots, or validate guest connectivity after migration.
- A clone method:
viborssh.
Warning: The repository's copy-offload guidance assumes SAN or block-backed storage. The existing project docs explicitly call out NFS as unsupported for copy-offload scenarios.
For older MTV environments, the project docs include this feature-gate example:
spec:
feature_copy_offload: 'true'
Note:
mtv-api-testsdoes not toggle that feature gate for you. If your MTV version requires it, enable it before running copy-offload scenarios.
Copy-Offload Provider Config
The copy-offload settings live under the VMware provider entry in .providers.json. This is the actual copyoffload block from .providers.json.example:
"copyoffload": {
# Supported storage_vendor_product values:
# - "ontap" (NetApp ONTAP)
# - "vantara" (Hitachi Vantara)
# - "primera3par" (HPE Primera/3PAR)
# - "pureFlashArray" (Pure Storage FlashArray)
# - "powerflex" (Dell PowerFlex)
# - "powermax" (Dell PowerMax)
# - "powerstore" (Dell PowerStore)
# - "infinibox" (Infinidat InfiniBox)
# - "flashsystem" (IBM FlashSystem)
"storage_vendor_product": "ontap",
# Primary datastore for copy-offload operations (required)
# This is the vSphere datastore ID (e.g., "datastore-12345") where VMs reside
# Get via vSphere: Datacenter → Storage → Datastore → Summary → More Objects ID
"datastore_id": "datastore-12345",
# Optional: Secondary datastore for multi-datastore copy-offload tests
# Only needed when testing VMs with disks spanning multiple datastores
# When specified, tests can validate copy-offload with disks on different datastores
"secondary_datastore_id": "datastore-67890",
# Optional: Non-XCOPY datastore for mixed datastore tests
# This should be a datastore that does NOT support XCOPY/VAAI primitives
# Used for testing VMs with disks on both XCOPY and non-XCOPY datastores
"non_xcopy_datastore_id": "datastore-99999",
"default_vm_name": "rhel9-template",
"storage_hostname": "storage.example.com",
"storage_username": "admin",
"storage_password": "your-password-here", # pragma: allowlist secret
# Vendor-specific fields (configure based on your storage_vendor_product):
# IMPORTANT: Only configure the fields for your selected storage_vendor_product.
# For example, if storage_vendor_product == "ontap", only configure ontap_svm.
# You may leave other vendor-specific fields blank or remove them from your config.
# Note: Both datastore_id and secondary_datastore_id (if used) must be on the
# same storage array and support XCOPY/VAAI primitives for copy-offload to work.
# See forklift vsphere-xcopy-volume-populator code/README for details
# NetApp ONTAP (required for "ontap"):
"ontap_svm": "vserver-name",
# Hitachi Vantara (required for "vantara"):
"vantara_storage_id": "123456789", # Storage array serial number
"vantara_storage_port": "443", # Storage API port
"vantara_hostgroup_id_list": "CL1-A,1:CL2-B,2:CL4-A,1:CL6-A,1", # IO ports and host group IDs
# Pure Storage FlashArray (required for "pureFlashArray"):
# Get with: printf "px_%.8s" $(oc get storagecluster -A -o=jsonpath='{.items[?(@.spec.cloudStorage.provider=="pure")].status.clusterUid}')
"pure_cluster_prefix": "px_a1b2c3d4",
# Dell PowerFlex (required for "powerflex"):
# Get from vxflexos-config ConfigMap in vxflexos or openshift-operators namespace
"powerflex_system_id": "system-id",
# Dell PowerMax (required for "powermax"):
# Get from ConfigMap in powermax namespace used by CSI driver
"powermax_symmetrix_id": "000123456789",
# HPE Primera/3PAR, Dell PowerStore, Infinidat InfiniBox, IBM FlashSystem:
# No additional vendor-specific fields required - use only the common fields above
# ESXi SSH configuration (optional, for SSH-based cloning):
# Can be overridden via environment variables: COPYOFFLOAD_ESXI_HOST, COPYOFFLOAD_ESXI_USER, COPYOFFLOAD_ESXI_PASSWORD
"esxi_clone_method": "ssh", # "vib" (default) or "ssh"
"esxi_host": "your-esxi-host.example.com", # required for ssh method
"esxi_user": "root", # required for ssh method
"esxi_password": "your-esxi-password", # pragma: allowlist secret # required for ssh method
# RDM testing (optional, for RDM disk tests):
# Note: datastore_id must be a VMFS datastore for RDM disk support
"rdm_lun_uuid": "naa.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
The minimum fields that this project validates before running are:
storage_vendor_productdatastore_idstorage_hostnamestorage_usernamestorage_password
Add these when you want advanced scenarios:
secondary_datastore_idfor multi-datastore testsnon_xcopy_datastore_idfor mixed and fallback testsrdm_lun_uuidfor RDM testsdefault_vm_nameif your copy-offload-ready template differs from the default test data
Note: The
# pragma: allowlist secretcomments in.providers.json.exampleare there for repository tooling. They are not valid JSON and must be removed from your real.providers.json.
default_vm_name is especially useful when your environment has a single known-good copy-offload template. The suite applies that override to cloned VM scenarios so you do not have to change every test entry by hand.
Environment Variable Overrides
The repository lets you override any copy-offload credential from the environment, and environment values always win over .providers.json:
env_var_name = f"COPYOFFLOAD_{credential_name.upper()}"
return os.getenv(env_var_name) or copyoffload_config.get(credential_name)
That pattern works for the common storage credentials:
COPYOFFLOAD_STORAGE_HOSTNAMECOPYOFFLOAD_STORAGE_USERNAMECOPYOFFLOAD_STORAGE_PASSWORD
It also works for vendor-specific and ESXi-specific values such as:
COPYOFFLOAD_ONTAP_SVMCOPYOFFLOAD_VANTARA_HOSTGROUP_ID_LISTCOPYOFFLOAD_ESXI_HOSTCOPYOFFLOAD_ESXI_USERCOPYOFFLOAD_ESXI_PASSWORD
Tip: A good pattern is to keep the structural values in
.providers.jsonand inject the sensitive values through environment variables at runtime.
Supported Storage Vendors
Use these exact storage_vendor_product values. They come directly from the repository's copy-offload constants and secret-mapping logic.
storage_vendor_product |
Storage platform | Extra required fields |
|---|---|---|
ontap |
NetApp ONTAP | ontap_svm |
vantara |
Hitachi Vantara | vantara_storage_id, vantara_storage_port, vantara_hostgroup_id_list |
pureFlashArray |
Pure Storage FlashArray | pure_cluster_prefix |
powerflex |
Dell PowerFlex | powerflex_system_id |
powermax |
Dell PowerMax | powermax_symmetrix_id |
powerstore |
Dell PowerStore | None beyond base storage credentials |
primera3par |
HPE Primera / 3PAR | None beyond base storage credentials |
infinibox |
Infinidat InfiniBox | None beyond base storage credentials |
flashsystem |
IBM FlashSystem | None beyond base storage credentials |
Storage Secrets
In mtv-api-tests, you usually do not create the copy-offload storage secret by hand. The suite creates it automatically from the VMware provider's copyoffload block.
That matters because:
- The secret values can come from
.providers.jsonor environment variables. - The secret is created in the same target namespace where the suite creates the
StorageMapandPlan. - The
offloadPlugincan reference the secret by name, without extra manual wiring.
The suite always creates these base secret keys:
STORAGE_HOSTNAMESTORAGE_USERNAMESTORAGE_PASSWORD
It then adds vendor-specific keys such as ONTAP_SVM, STORAGE_ID, HOSTGROUP_ID_LIST, PURE_CLUSTER_PREFIX, POWERFLEX_SYSTEM_ID, or POWERMAX_SYMMETRIX_ID, depending on storage_vendor_product.
After the plan is created, the suite also waits for Forklift to create the plan-specific secret used during copy-offload. If that secret never appears, the run continues long enough to fail with a clearer migration error.
Note: This automatic secret handling is specific to how
mtv-api-testsdrives copy-offload. It removes a lot of manual setup from the test workflow.
StorageMap and Plan Behavior
The core of copy-offload in this project is the offloadPlugin block. The tests build it like this:
offload_plugin_config = {
"vsphereXcopyConfig": {
"secretRef": copyoffload_storage_secret.name,
"storageVendorProduct": storage_vendor_product,
}
}
The storage map entries then attach that plugin to the source datastore mapping and set the destination for block-backed PVCs:
storage_map_list.append({
"destination": {
"storageClass": target_storage_class,
"accessMode": "ReadWriteOnce",
"volumeMode": "Block",
},
"source": {"id": ds_id},
"offloadPlugin": offload_plugin_config,
})
The project also changes plan behavior for copy-offload runs:
if copyoffload:
plan_kwargs["pvc_name_template"] = "pvc"
plan = create_and_store_resource(**plan_kwargs)
if copyoffload:
wait_for_plan_secret(ocp_admin_client, target_namespace, plan.name)
And when the suite creates the VMware Provider for copy-offload, it adds this annotation:
provider_annotations["forklift.konveyor.io/empty-vddk-init-image"] = "yes"
That is the repository's way of steering the provider toward the copy-offload path instead of a VDDK-only setup.
The difference is explicit in the concurrent XCOPY/VDDK scenario: the XCOPY StorageMap must contain offloadPlugin, and the VDDK StorageMap must not.
Tip: If you want a fast sanity check that your environment is wired correctly, start with
test_copyoffload_thin_migration. It uses the sameoffloadPluginstructure as the advanced scenarios, but with fewer moving parts.
Clone Methods
The suite supports both copy-offload clone methods exposed by the populator: vib and ssh.
VIB
vib is the default. If you omit esxi_clone_method, the repository leaves the provider's clone method alone and relies on the default VIB behavior.
Use vib when:
- Your ESXi environment allows community-level VIB installation.
- You do not want the suite to manage ESXi SSH credentials.
Note: The repository does not perform extra VIB-specific setup. It assumes the populator and ESXi host permissions are already ready for the VIB path.
SSH
If you set esxi_clone_method to ssh, the suite patches the VMware Provider so MTV uses SSH-based cloning:
patch = {"spec": {"settings": {"esxiCloneMethod": clone_method}}}
ResourceEditor(patches={self.ocp_resource: patch}).update()
It then retrieves the provider-generated public key from the offload-ssh-keys-<provider>-public secret and installs a restricted key on the ESXi host. The restricted command is taken directly from the ESXi helper:
command_template = (
'command="python /vmfs/volumes/{datastore_name}/secure-vmkfstools-wrapper.py",'
"no-port-forwarding,no-agent-forwarding,no-X11-forwarding {public_key}"
)
For SSH mode, you must provide:
esxi_hostesxi_useresxi_password
Warning: SSH mode temporarily updates
/etc/ssh/keys-root/authorized_keyson the ESXi host. The suite removes the key during teardown, but it is still a real host-side change.Tip: SSH mode is a good choice when you want a fully test-managed setup path. The suite handles the provider patch, key installation, and cleanup for you.
Fallback Modes
Copy-offload is not all-or-nothing in this repository. The existing tests explicitly cover cases where some or all disks live on a datastore that does not support XCOPY/VAAI.
There are two main fallback patterns:
- Mixed-datastore fallback: one disk uses an XCOPY-capable datastore and another disk lives on
non_xcopy_datastore_id. - Full non-XCOPY fallback: the VM is relocated to
non_xcopy_datastore_id, and added disks are placed there too.
The storage-map helper keeps the offloadPlugin on the non-XCOPY mapping so Forklift can exercise fallback behavior:
storage_map_list.append({
"destination": destination_config,
"source": {"id": non_xcopy_datastore_id},
"offloadPlugin": offload_plugin_config,
})
The full fallback case is modeled directly in the repository like this:
"test_copyoffload_fallback_large_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"source_vm_power": "off",
"guest_agent": True,
"clone": True,
"target_datastore_id": "non_xcopy_datastore_id",
"disk_type": "thin",
"add_disks": [
{
"size_gb": 100,
"provision_type": "thin",
"datastore_id": "non_xcopy_datastore_id",
},
],
},
],
"warm_migration": False,
"copyoffload": True,
}
Warning:
non_xcopy_datastore_idmust point to a real datastore that does not support XCOPY/VAAI. If it is missing, the mixed and fallback scenarios fail fast before migration starts.
Advanced Copy-Offload Scenarios
The copy-offload test matrix in this repository is broader than the basic thin-disk path.
| Scenario | What it validates | Key test name(s) |
|---|---|---|
| Basic provisioning | Thin and thick-lazy disk copy-offload | test_copyoffload_thin_migration, test_copyoffload_thick_lazy_migration |
| Multi-disk layouts | Additional disks on the same datastore | test_copyoffload_multi_disk_migration |
| Custom datastore paths | Extra disks placed under a custom folder like shared_disks |
test_copyoffload_multi_disk_different_path_migration |
| Multi-datastore | A VM with disks spanning primary and secondary XCOPY datastores | test_copyoffload_multi_datastore_migration |
| Mixed XCOPY / non-XCOPY | Some disks accelerate while others fall back | test_copyoffload_mixed_datastore_migration |
| Full fallback on non-XCOPY | Large VM and added disk entirely on a non-XCOPY datastore | test_copyoffload_fallback_large_migration |
| RDM | RDM virtual-disk migration using rdm_lun_uuid |
test_copyoffload_rdm_virtual_disk_migration |
| Snapshots | Source snapshots before migration, including a 2 TB case | test_copyoffload_thin_snapshots_migration, test_copyoffload_2tb_vm_snapshots_migration |
| Disk modes | Independent persistent and independent nonpersistent disks | test_copyoffload_independent_persistent_disk_migration, test_copyoffload_independent_nonpersistent_disk_migration |
| Large and dense VMs | 1 TB VM and 10 mixed thin/thick disks | test_copyoffload_large_vm_migration, test_copyoffload_10_mixed_disks_migration |
| Warm copy-offload | Warm migration with cutover | test_copyoffload_warm_migration |
| Name edge cases | Non-Kubernetes VM names with uppercase letters and underscores | test_copyoffload_nonconforming_name_migration |
| Scale | Five copy-offload VMs in one run | test_copyoffload_scale_migration |
| Concurrency | Two copy-offload plans at once | test_simultaneous_copyoffload_migrations |
| XCOPY vs VDDK | One copy-offload plan and one standard VDDK plan running together | test_concurrent_xcopy_vddk_migration |
The repository's multi-datastore scenario uses a symbolic secondary datastore key instead of hardcoding the MoID into every disk entry:
"test_copyoffload_multi_datastore_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"source_vm_power": "off",
"guest_agent": True,
"clone": True,
"disk_type": "thin",
"add_disks": [
{
"size_gb": 30,
"disk_mode": "persistent",
"provision_type": "thin",
"datastore_id": "secondary_datastore_id",
},
],
},
],
"warm_migration": False,
"copyoffload": True,
}
The non-conforming-name scenario is also deliberate. Its source config preserves uppercase letters and underscores in the cloned VMware name, and the suite then verifies that MTV sanitizes the destination VM name to a Kubernetes-safe value.
Note: The project docs currently call out RDM copy-offload support only for Pure Storage, and they require
datastore_idto be a VMFS datastore for RDM scenarios.
What the Suite Automates for You
When you run copy-offload scenarios through mtv-api-tests, the project handles several setup steps automatically:
- It validates that the source provider is vSphere and that the
copyoffloadsection exists. - It creates the copy-offload storage secret from
.providers.jsonor environment variables. - It creates
StorageMapentries withoffloadPlugin.vsphereXcopyConfig. - It annotates the VMware provider with
forklift.konveyor.io/empty-vddk-init-image: "yes". - It patches
esxiCloneMethodtosshwhen you choose SSH cloning. - It waits for Forklift to create the plan-specific copy-offload secret.
- It forces
pvc_name_templatetopvcfor copy-offload plans.
That is why the user-facing setup is mostly about getting the provider config, datastore IDs, storage array credentials, and clone method right.
Running the Copy-Offload Marker
The repository's copy-offload guide uses the copyoffload pytest marker. This command is taken directly from the existing Job example:
uv run pytest -m copyoffload \
-v \
${CLUSTER_HOST:+--tc=cluster_host:${CLUSTER_HOST}} \
${CLUSTER_USERNAME:+--tc=cluster_username:${CLUSTER_USERNAME}} \
${CLUSTER_PASSWORD:+--tc=cluster_password:${CLUSTER_PASSWORD}} \
--tc=source_provider:vsphere-8.0.3.00400 \
--tc=storage_class:my-block-storageclass
Replace the provider key with the exact key from your .providers.json, and replace the storage class with the block-backed class that maps to the same storage array as your vSphere datastores.
Tip: Start with
test_copyoffload_thin_migration, then move totest_copyoffload_multi_datastore_migration,test_copyoffload_mixed_datastore_migration, ortest_concurrent_xcopy_vddk_migrationonce the base path is working.