Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 310 additions & 0 deletions content/en/docs/next/virtualization/vmware-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
---
title: "Migrating Virtual Machines from VMware vSphere"
linkTitle: "VMware Migration"
description: "Migrating virtual machines from VMware vSphere into Cozystack tenants with the VM import API"
weight: 64
---

This guide describes how to migrate virtual machines from VMware vSphere into a Cozystack tenant using the `forklift.cozystack.io` import API. The migration is driven by the cluster rather than by hand: you register a connection to vCenter, name the machines you want, and the platform transfers each disk and turns it into a Cozystack `VMDisk` and `VMInstance`.

{{< note >}}
The transfer is a **cold migration**: the source VM is powered off before its disks are read, and stays off until you start it again in vSphere. Plan a maintenance window, and never point an import at a machine you cannot afford to stop.
{{< /note >}}

The engine underneath is [Konveyor Forklift](https://github.com/kubev2v/forklift). You do not interact with it directly — Cozystack owns the tenant-facing API and drives Forklift on your behalf.

## Prerequisites

**Packages.** VM import is opt-in. Add all three to the platform configuration:

```yaml
bundles:
enabledPackages:
- cozystack.forklift-operator
- cozystack.forklift
- cozystack.migration-controller
```

**The VDDK image.** VMware's Virtual Disk Development Kit is proprietary and Cozystack can neither ship nor mirror it. An operator who holds a licence builds the image once, pushes it somewhere the cluster can pull from, and names it at the platform level — see [Building the VDDK image](#building-the-vddk-image).

```yaml
vmImport:
vddkImage: registry.example.com/vddk:8.0.3
```

Leaving it empty is a supported state: a vSphere import source then reports `Ready=False` with reason `VDDKNotConfigured` the moment it is created, rather than failing halfway through a transfer.

**A vCenter account.** The account needs read access to the inventory and the privileges Forklift uses to snapshot and read disks. Note that vCenter silently ignores an unknown privilege name, so a typo produces a role that looks correct and is quietly incomplete.

## Building the VDDK image

This is an administrator task, done once per cluster. It exists because the Virtual Disk Development Kit is licensed software: VMware distributes it to registered users only, and no one may redistribute it — which is why Cozystack ships no image and cannot mirror one for you.

{{< note >}}
Read the VDDK licence before you start. It permits an internal build for your own use; it does not permit publishing the resulting image to a registry other people can pull from. Push it to a private registry, not Docker Hub.
{{< /note >}}

### Download the kit

Sign in to the [Broadcom support portal](https://support.broadcom.com/) and download the **Virtual Disk Development Kit** for **Linux**, matching your vSphere version — a 8.0.x kit for vSphere 8, 7.0.x for vSphere 7. The file is named like `VMware-vix-disklib-8.0.3-24091160.x86_64.tar.gz`.

Matching matters: a kit older than the vCenter it talks to may fail to open disks, and the error appears at transfer time rather than at connection time.

### Build

The image is a plain filesystem carrying the kit at `/vddk-lib` — the engine mounts it into its transfer pod and reads the libraries from there. There is nothing to run inside it, so a scratch-like base is enough:

```dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal
USER 1001
COPY vmware-vix-disklib-distrib /vddk-lib
ENTRYPOINT ["/bin/bash"]
```

```bash
tar -xzf VMware-vix-disklib-8.0.3-24091160.x86_64.tar.gz
podman build -t registry.example.com/vddk:8.0.3 .
podman push registry.example.com/vddk:8.0.3
```

Build for **linux/amd64**: the transfer pod runs on the cluster's worker nodes, and an image built on an arm64 laptop without an explicit platform will be pulled and then fail to execute. With `podman` or `docker buildx`, pass `--platform linux/amd64`.

### Make the cluster able to pull it

A private registry needs credentials in the namespaces that pull the image — the Forklift namespace and every tenant namespace an import runs in. Create the pull secret and reference it from the service account, or use whatever registry-credential mechanism your cluster already has.

An image the cluster cannot pull produces a transfer that never starts, with the reason on the pod rather than on the import task, so it is worth confirming the pull works before the first migration:

```bash
kubectl -n cozy-forklift run vddk-pull-check --rm -it --restart=Never \
--image=registry.example.com/vddk:8.0.3 --command -- ls /vddk-lib
```

### Point the platform at it

```yaml
vmImport:
vddkImage: registry.example.com/vddk:8.0.3
```

Only the reference travels to the controller — never a credential. The value is not a tenant setting and does not appear on any tenant-facing object: naming an image the cluster will run is an operator's decision.

When the kit is upgraded, change the tag here and the next import uses it. Running imports are unaffected, since the transfer pod already holds its copy.

## Before the first import

Four checks. Each of them, skipped, produces a failure that appears late and names something other than its cause.

### The storage class must bind Immediate

An import populates a volume before anything consumes it, so a `WaitForFirstConsumer` class deadlocks: the claim waits for a consumer that never arrives. The import task refuses such a class up front, but the **cluster default is frequently `WaitForFirstConsumer`**, so name the class explicitly:

```bash
kubectl get storageclass
```

Pick one whose `VOLUMEBINDINGMODE` is `Immediate` and put it in `spec.storageClass`.

### The ESXi hosts must be reachable from the cluster

Disk data does not travel through vCenter. The VDDK opens its connection straight to the ESXi host holding the VM, at whatever address vCenter advertises for that host.

That address must be routable from the worker nodes and — this is the one that catches people — **must not fall inside the cluster's Service CIDR**. An address in that range is claimed by Kubernetes service routing: the packets never leave the node, and the transfer dies after validation has already passed.

```bash
# The cluster's service network, via the address of the kubernetes service
kubectl get svc -n default kubernetes -o jsonpath='{.spec.clusterIP}'
```

Compare it with the addresses your ESXi hosts are advertised at. If they overlap, or the advertised address is simply unreachable, redirect the transfer with `spec.hosts` — see [Redirecting the transfer](#redirecting-the-transfer) below.

### The vCenter username needs its domain

vCenter expects the SSO domain: `migration@vsphere.local`, not `migration`. Given a bare account name it answers *"Cannot complete login due to an incorrect user name or password"* — the same message it uses for a wrong password, which sends you looking in the wrong place.

### The engine's certificates must be current

Forklift rotates its own serving certificates and updates the secret, but does not restart its pods. A long-lived deployment can therefore serve a certificate that its own published CA no longer matches, and the import controller — which verifies that CA — will refuse the connection. If a source will not become ready and the logs mention `certificate signed by unknown authority`:

```bash
kubectl -n cozy-forklift rollout restart deploy/forklift-controller
```

## Step 1: Register the source

A `VMImportSource` is a long-lived connection, reusable across many imports. Credentials go on the spec — tenants cannot create Secrets in Cozystack, so the controller materializes one for the engine to consume:

```yaml
apiVersion: forklift.cozystack.io/v1alpha1
kind: VMImportSource
metadata:
name: vcenter-prod
namespace: tenant-example
spec:
type: vsphere
url: https://vcenter.example.com/sdk
credentials:
username: migration@vsphere.local
password: "..."
caCert: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
```

Either `caCert` or `insecureSkipVerify: true` must be set. A SHA-1 **thumbprint does not work here**: a thumbprint is what the engine wants for a direct ESXi host connection, and supplying one in place of a CA leaves the source stuck reporting `SecretNotValid`.

Wait for the connection to be tested:

```bash
kubectl -n tenant-example get vmimportsource
```

```console
NAME TYPE URL READY AGE
vcenter-prod vsphere https://vcenter.example.com/sdk True 45s
```

### Redirecting the transfer

When vCenter advertises an ESXi address the cluster cannot use, add an override. Each entry carries its own credentials because the ESXi host authenticates the transfer connection itself rather than honouring the vCenter session:

```yaml
spec:
hosts:
- id: host-10 # the host's managed-object id
address: 10.0.30.29 # an address the cluster can actually reach
credentials:
username: root
password: "..."
insecureSkipVerify: true
```

The host id is the one the VM's inventory record names, not the hostname.

## Step 2: Find the VMs to migrate

Machines are named by their vSphere managed-object reference — `vm-1234`, not the display name. The reference appears in the vSphere client URL when the VM is selected, and `govc ls -i` prints it:

```bash
govc ls -i /DC/vm/web-01
```

## Step 3: Run the import

A `VMImportTask` is a one-shot operation. It names a source, the machines, and the storage class every disk lands on:

```yaml
apiVersion: forklift.cozystack.io/v1alpha1
kind: VMImportTask
metadata:
name: import-web-tier
namespace: tenant-example
spec:
sourceRef:
name: vcenter-prod
storageClass: replicated
vms:
- id: vm-1234
name: web-01
- id: vm-1235
name: web-02
```

Watch it:

```bash
kubectl -n tenant-example get vmimporttask -w
```

```console
NAME SOURCE PHASE AGE
import-web-tier vcenter-prod Validating 20s
import-web-tier vcenter-prod Transferring 1m
import-web-tier vcenter-prod Succeeded 6m
```

Per-VM progress, including the percentage of each disk transferred, is on `status.vms`:

```bash
kubectl -n tenant-example get vmimporttask import-web-tier -o jsonpath='{.status.vms}'
```

Each VM in a task is independent: one that fails does not stop its siblings.

## Step 4: What you get

The import produces ordinary Cozystack objects — one `VMDisk` per source disk and one `VMInstance` over them:

```bash
kubectl -n tenant-example get vmdisk,vminstance
```

Three properties are worth knowing:

**Nothing is copied twice.** The transferred volume is re-pointed into the disk the `VMInstance` expects, rather than cloned into it. A 16 GiB machine occupies 16 GiB when the import finishes.

**The imported VM starts `Halted`.** A freshly imported guest usually needs its network reviewed before it runs, and starting it automatically would put a second copy of a machine on the network. Start it when you are ready:

```bash
kubectl -n tenant-example patch vminstance web-01 --type merge -p '{"spec":{"runStrategy":"Always"}}'
```

**The results outlive the task.** Deleting the `VMImportTask` removes the migration machinery and leaves the disks and instances untouched — they carry no owner reference back to it. Deleting the source deregisters the connection and touches nothing already imported.

CPU topology, memory and firmware are carried across from the source: a UEFI guest is imported as a UEFI guest, with Secure Boot preserved.

## Migration checklist

- [ ] The three packages are enabled and `vmImport.vddkImage` is set
- [ ] A storage class with `volumeBindingMode: Immediate` is chosen
- [ ] ESXi transfer addresses are reachable and do not overlap the Service CIDR
- [ ] The vCenter username includes its SSO domain
- [ ] The source reports `Ready=True`
- [ ] A maintenance window exists: the source VM is powered off during transfer
- [ ] The tenant has quota for the imported disks
- [ ] After import: network reviewed, then the VM started

## Troubleshooting

### The source stays `Ready=False` with an authentication error

Check the username first. vCenter reports a missing SSO domain with the same message it uses for a wrong password. If the account is correct, confirm that `caCert` holds a CA certificate rather than a thumbprint.

### The task reaches `Transferring` and then fails with an NBD error

```console
Unable to connect to vddk data source: nbd_connect_uri: the server has no export named ''
```

This sounds like a missing disk and is almost always an unreachable ESXi host: the VDDK could not open its data connection to the address vCenter advertised. Check the address against your Service CIDR and add a `spec.hosts` override.

### Progress sits at 0 for a long time

A stalled transfer is not reported as an error on the migration objects. Look at the events on the target namespace's claims — a VDDK failure to reach the host surfaces there and nowhere else.

### The task fails with `no imported VM was found`

Something removed the machine the engine built before the platform could adopt it. The usual cause is a second controller acting on the same objects — for instance an older VM adoption controller left running from a previous Cozystack version. Ensure only one is active.

### An import failed and something is still transferring

A failed transfer can leave the engine's own `DataVolume` behind, retrying indefinitely. Look for one whose labels name the plan of the failed import and remove it:

```bash
kubectl -n tenant-example get dv -l vmID=vm-1234
```

## Limitations

The first version of this API is deliberately narrow:

- **vSphere only.** Other providers the engine already supports — oVirt, OpenStack, OVA, Hyper-V — arrive additively.
- **Cold migration only.** Warm, change-block-tracking migration is not offered; the source is powered off for the transfer.
- **One storage class per task.** Disks are not split across classes by source datastore.
- **Pod networking.** The imported `VMInstance` attaches to the pod network; richer placement arrives with the network-placement design.
- **Guest conversion is not offered.** Disks are copied as-is, so the guest must already carry virtio drivers. A Windows guest that has never seen virtio will not boot after import.

## Planning notes

An import needs roughly **twice the disk size in free space** while it runs: the transfer target plus scratch space. A 16 GiB machine transfers in a few minutes on a local network; plan larger machines proportionally, and remember the source is unavailable for that whole window.
Loading