This section provides the complete step-by-step execution framework for running a Forklift-based migration from VMware vSphere to OpenShift Virtualization using Everpure XCopy Offload.
Prerequisites and Cluster Preparation
Tool Verification
# Verify oc and kubectl are installed and authenticated
oc login --server=https://<openshift-api>:6443
oc whoami
# Expected: system:admin or your cluster-admin user
# Verify Forklift operator is installed
oc get pods -n openshift-mtv | grep forklift
# Expected: forklift-controller, forklift-api, forklift-ui pods in Running state
# Verify Portworx StorageCluster is healthy
oc get storagecluster -n portworx
# Expected: Status = Online
# Verify StorageClass exists
oc get sc | grep px-pure
# Expected: px-pure-iscsi-sc (or nvmetcp / fc variant) listed
-
Enable XCopy Offload.
XCopy Offload must be explicitly enabled on the Forklift controller. This is a one-time configuration per cluster. Without this step, all migrations fall back to the standard VDDK path regardless of the StorageMap configuration.
oc patch forkliftcontrollers.forklift.konveyor.io forklift-controller \ --type merge \ -p '{"spec": {"feature_copy_offload": "true"}}' \ -n openshift-mtv # Verify the patch was applied oc get forkliftcontroller forklift-controller -n openshift-mtv \ -o jsonpath='{.spec.feature_copy_offload}' # Expected: true -
Enable SCSI Persistent Reservation (if required).
Required only for VMs that use SCSI Persistent Reservation — for example, Windows Server Failover Clustering. Skip this step for standard VM workloads.
oc patch hco kubevirt-hyperconverged \ -n openshift-cnv --type json \ -p '[{"op": "replace", "path": "/spec/featureGates/persistentReservation", "value": true}]' # Verify oc get hco kubevirt-hyperconverged -n openshift-cnv -o yaml | grep persistentReservation # Expected: persistentReservation: true -
Set Environment Variables.
Set these variables in the shell session before running any subsequent commands. They are referenced throughout this section.
export FORKLIFT_NAMESPACE="forklift-migration" export PROVIDER_NAME="vcenter-source" export SOURCE_VCENTER_USER="administrator@vsphere.local" export SOURCE_VCENTER_PASSWORD="<vcenter-password>" export SOURCE_VCENTER_URL="https://<vcenter-ip-or-fqdn>" -
Configure Image Registry (VDDK / Ephemeral Registry).
Note: This step is required only when running migrations without XCopy Offload (the standard VDDK path).
-
Configure Ephemeral Image Registry (VDDK Path Only):
# Enable the internal OpenShift image registry with ephemeral storage oc patch configs.imageregistry.operator.openshift.io/cluster \ --patch '{"spec":{"managementState":"Managed","storage":{"emptyDir":{}},"defaultRoute":true}}' \ --type=merge # Wait for the registry deployment to become available oc wait --for=condition=Available --timeout=5m deployment/image-registry -n openshift-image-registry # Retrieve the registry route hostname REGISTRY_ROUTE=$(oc get route default-route -n openshift-image-registry -o jsonpath='{.spec.host}') echo "Registry route: ${REGISTRY_ROUTE}" -
Build and Push VDDK Image (VDDK Path Only):
export VDDK_IMAGE_NAME="vddk" export VDDK_IMAGE_TAG="8.0.3" FULL_IMAGE_PATH="${REGISTRY_ROUTE}/${FORKLIFT_NAMESPACE}/${VDDK_IMAGE_NAME}:${VDDK_IMAGE_TAG}" # Create a service account with image push permissions SERVICE_ACCOUNT="forklift-builder" oc create sa "$SERVICE_ACCOUNT" -n "$FORKLIFT_NAMESPACE" --dry-run=client -o yaml | oc apply -f - oc policy add-role-to-user system:image-builder -z "$SERVICE_ACCOUNT" -n "$FORKLIFT_NAMESPACE" TOKEN=$(oc create token "$SERVICE_ACCOUNT" -n "$FORKLIFT_NAMESPACE") # Log in to the internal registry podman login --tls-verify=false -u "$SERVICE_ACCOUNT" -p "$TOKEN" "$REGISTRY_ROUTE" -
Download VDDK tarball from developer.broadcom.com, then build and push:
cat > Dockerfile <<EOF FROM registry.access.redhat.com/ubi8/ubi-minimal USER 1001 COPY vmware-vix-disklib-distrib /vmware-vix-disklib-distrib RUN mkdir -p /opt ENTRYPOINT ["cp", "-r", "/vmware-vix-disklib-distrib", "/opt"] EOF tar -xzf ./*.tar.gz podman build . -t "$FULL_IMAGE_PATH" podman push --tls-verify=false "$FULL_IMAGE_PATH"
-
Configure Ephemeral Image Registry (VDDK Path Only):
-
Configure the Source Provider.
The Provider CR defines the connection to the vCenter source environment. Forklift uses this connection to discover VM inventory, retrieve disk metadata, and obtain the FlashArray volume identifiers needed for XCopy Offload operations.
-
Create the vCenter Credentials Secret:
PROVIDER_SECRET_NAME="${PROVIDER_NAME}-creds" oc create secret generic "$PROVIDER_SECRET_NAME" \ --from-literal=user="$SOURCE_VCENTER_USER" \ --from-literal=password="$SOURCE_VCENTER_PASSWORD" \ --from-literal=insecureSkipVerify=true \ -n "$FORKLIFT_NAMESPACE" --dry-run=client -o yaml | oc apply -f - # Verify oc get secret "$PROVIDER_SECRET_NAME" -n "$FORKLIFT_NAMESPACE" -
Create the Provider CR: For XCopy Offload, the vddkInitImage field is omitted. Including it forces VDDK-based migration regardless of the offloadPlugin configuration in the StorageMap:
cat <<EOF | oc apply -f - apiVersion: forklift.konveyor.io/v1beta1 kind: Provider metadata: name: ${PROVIDER_NAME} namespace: ${FORKLIFT_NAMESPACE} spec: type: vsphere url: ${SOURCE_VCENTER_URL} secret: name: ${PROVIDER_NAME}-creds namespace: ${FORKLIFT_NAMESPACE} # vddkInitImage is intentionally omitted for XCopy Offload migrations EOF # Monitor provider readiness oc get provider ${PROVIDER_NAME} -n ${FORKLIFT_NAMESPACE} -w # Expected: STATUS = Ready
-
Create the vCenter Credentials Secret:
-
Configure the Network Mapping between VMware and OpenShift.
The NetworkMap CR maps VMware port groups to OpenShift network destinations. Every port group that migrated VMs are connected to must be mapped. Unmapped networks cause the migration plan to fail validation.
Destination Type When to Use Configuration pod Standard VM traffic — maps to the OpenShift cluster SDN Name: pod
Type: pod
No additional config required
multus VMs requiring dedicated network interfaces (storage, backup, replication) Name: <multus-network-attachment-name>
Type: multus
NetworkAttachmentDefinition must exist.
-
Retrieve VMware Network IDs:
# List available networks from the provider inventory oc get -n openshift-mtv networks.forklift.konveyor.io \ --selector="migration.openshift.io/provider-uid=${PROVIDER_UID}" \ -o custom-columns='ID:.spec.id,NAME:.spec.name' -
Create the NetworkMap:
cat <<EOF | oc apply -f - apiVersion: forklift.konveyor.io/v1beta1 kind: NetworkMap metadata: name: default-network-map namespace: ${FORKLIFT_NAMESPACE} spec: map: - destination: name: pod type: pod # Maps to OpenShift pod network (default) source: id: <vm-network-id> name: <vm-network-name> # Add additional entries for each unique port group used by migrated VMs # - destination: # name: multus-network # type: multus # Use multus for VMs requiring dedicated networks # source: # id: <second-network-id> # name: <second-network-name> provider: source: name: ${PROVIDER_NAME} namespace: ${FORKLIFT_NAMESPACE} destination: name: host namespace: openshift-mtv EOF
-
Retrieve VMware Network IDs:
-
Configure the Storage Mapping between the Datastore and the StorageClass.
The StorageMap CR maps VMware datastores to Kubernetes StorageClasses. When XCopy Offload is enabled, the StorageMap also contains the FlashArray credential reference and storage vendor identifier that Forklift uses to issue XCOPY commands.
-
Create FlashArray Credentials Secret for XCopy:
# This secret authorises Forklift to issue XCOPY commands to FlashArray oc create secret generic pure-storage-creds \ --from-literal=apiToken=<flasharray-api-token> \ --from-literal=managementEndpoint=<flasharray-mgmt-ip-or-fqdn> \ -n ${FORKLIFT_NAMESPACE} --dry-run=client -o yaml | oc apply -f - -
Retrieve VMware Datastore IDs:
# List datastores from provider inventory oc get -n openshift-mtv datastores.forklift.konveyor.io \ --selector="migration.openshift.io/provider-uid=${PROVIDER_UID}" \ -o custom-columns='ID:.spec.id,NAME:.spec.name,TYPE:.spec.type' -
Create the StorageMap with XCopy Offload:
cat <<EOF | oc apply -f - apiVersion: forklift.konveyor.io/v1beta1 kind: StorageMap metadata: name: pure-storage-map namespace: ${FORKLIFT_NAMESPACE} spec: map: - destination: accessMode: ReadWriteOnce storageClass: px-pure-iscsi-sc # Target Portworx + FlashArray StorageClass offloadPlugin: vsphereXcopyConfig: secretRef: pure-storage-creds # FlashArray API credentials storageVendorProduct: pureFlashArray source: id: <source-datastore-id> name: <source-datastore-name> # Add additional entries for each source datastore # Multiple datastores can map to the same or different StorageClasses provider: source: name: ${PROVIDER_NAME} namespace: ${FORKLIFT_NAMESPACE} destination: name: host namespace: openshift-mtv EOF
-
Create FlashArray Credentials Secret for XCopy:
-
Define the Migration Plan:
The Plan CR selects which VMs to migrate and binds together the Provider, NetworkMap, and StorageMap. A single Plan can contain one VM or hundreds. Plans are reusable — the same Plan can be executed multiple times to retry failed VMs.
Standard VM Migration Plan
cat <<EOF | oc apply -f - apiVersion: forklift.konveyor.io/v1beta1 kind: Plan metadata: name: migration-plan-wave1 namespace: ${FORKLIFT_NAMESPACE} spec: # Use generateName for PVC names to avoid conflicts across migration runs pvcNameTemplateUseGenerateName: true targetNamespace: ${FORKLIFT_NAMESPACE} warm: false provider: source: name: ${PROVIDER_NAME} namespace: ${FORKLIFT_NAMESPACE} destination: name: host namespace: openshift-mtv map: network: name: default-network-map namespace: ${FORKLIFT_NAMESPACE} storage: name: pure-storage-map namespace: ${FORKLIFT_NAMESPACE} vms: - id: vm-1001 name: app-server-01 - id: vm-1002 name: app-server-02 - id: vm-1003 name: db-server-01 EOFShared Disk Migration Plan Variant
# For VMs with shared RDM volumes — Stage 1 only # migrateSharedDisks: true migrates shared volumes alongside the first VM spec:migrateSharedDisks: true # Stage 1: first VM + shared disks
vms: - id: vm-cluster-lead name: cluster-node-01 # For Stage 2 — remaining VMs without re-migrating shared disks spec: migrateSharedDisks: false # Stage 2: remaining VMs only vms: - id: vm-cluster-02 name: cluster-node-02 # ... remaining cluster nodesWarm Migration Variant
Warm migration pre-copies VM disk data while the VM is still running in VMware, then performs a final sync and cutover. This minimises downtime for VMs that cannot tolerate an extended outage. With XCopy Offload, the initial copy and the final sync are both performed as FlashArray internal operations — the warm migration benefit is a near-zero final sync window.
# Enable warm migration in the Plan spec spec: warm: true # Enables pre-copy phase while source VM is running -
Start the Migration.
cat <<EOF | oc apply -f - apiVersion: forklift.konveyor.io/v1beta1 kind: Migration metadata: name: migration-wave1-run1 namespace: ${FORKLIFT_NAMESPACE} spec: plan: name: migration-plan-wave1 namespace: ${FORKLIFT_NAMESPACE} EOF -
Monitor the Migration Progress:
# Check overall migration status oc get migration migration-wave1-run1 -n ${FORKLIFT_NAMESPACE} # Watch for Succeeded condition oc get migration migration-wave1-run1 -n ${FORKLIFT_NAMESPACE} \ -o jsonpath='{.status.conditions[?(@.type=="Succeeded")].status}' # Detailed status including per-VM progress oc describe migration migration-wave1-run1 -n ${FORKLIFT_NAMESPACE} # Watch migrated VM pods appear in the target namespace oc get pods -n ${FORKLIFT_NAMESPACE} -w # Verify migrated VMs oc get -
Validate Post-Migration Results:
# Confirm VM is Running oc get vm <vm-name> -n ${FORKLIFT_NAMESPACE} # Expected: STATUS = Running # Confirm PVC is Bound oc get pvc -n ${FORKLIFT_NAMESPACE} # Expected: STATUS = Bound for all migrated VM disks # Confirm VM is accessible (requires virtctl or console access) virtctl console <vm-name> -n ${FORKLIFT_NAMESPACE} # For failed VMs — create a new Migration referencing the same Plan # with only the failed VM IDs in the vms list
Handling Partial Failures:
Forklift migrations are non-destructive with respect to the source environment. Source VMs remain powered on and unmodified throughout the migration. If a subset of VMs in a batch fail:
- The successfully migrated VMs continue running on OpenShift.
- The failed VMs remain running on VMware, with no data loss or service disruption.
- Create a new Migration CR referencing the same plan, with the vms list updated to include only the failed VM IDs.
- Apply the new Migration CR to retry only the failed VMs.