

# KubeVirt 停机把 RWO 静态 PV 重建为 RWX：kind、Kube-OVN 实验

本文验证：虚拟机关机后，删除历史 RWO PVC 和 PV 对象，保留 NFS 后端数据，再重建为 RWX，能否继续使用 KubeVirt 热迁移。

PVC 的 `accessModes` 不能直接修改。转换前要先确认 PV 和 PVC 都是 `ReadWriteOnce`，再关机并确认 VMI、`virt-launcher` 已经退出。本实验删除 PVC/PV 对象，但保留 NFS 服务和导出路径中的数据，然后用相同路径重建 `ReadWriteMany` PV 和 PVC。系统盘由 CDI `DataVolume` 管理时，还要用 `allowClaimAdoption` 让新 DV 接管已有 PVC。

实验使用三节点 kind、KubeVirt、Kube-OVN 和 NFS，结果如下。

1. RWO PVC 的 VMI `LiveMigratable=False`，热迁移被 KubeVirt admission webhook 拒绝。
2. 停机后删除 PVC 和 PV 对象，NFS 后端路径不变，重建 RWX PV/PVC 后仍能读到同一个哨兵文件。
3. 新 DataVolume 成功接管已有 RWX PVC，PVC 的 ownerReference 指向该 DV，DV 进入 `Succeeded`。
4. VM 改用 DV 后，VMI `LiveMigratable=True`；VMIM 从 `rwo-rwx-lab-worker` 迁移到 `rwo-rwx-lab-worker2`，IP `10.251.0.19` 保持不变。

实验使用的是 NFS `Filesystem`，只用于验证对象重建、后端路径复用和 KubeVirt 共享 PVC 热迁移。它不能替代生产 Ceph CSI/RBD 验证。

<!--more-->

## 1. 资源和实验范围

KubeVirt 要求使用 PVC 的热迁移具备共享的 RWX 访问模式；VMI 的 `LiveMigratable` 条件也会反映卷是否满足要求。[KubeVirt Live Migration](https://kubevirt.io/user-guide/compute/live_migration/)

完整清单和验证结果已经提交到 [k8sdev `cce508d0`](https://github.com/jimyag/k8sdev/tree/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind)：

```text
rwo-to-rwx-kind/
├── kind-config.yaml
├── 01-vpc-subnet.yaml
├── 02-nfs.yaml
├── 03-rwo-pv-pvc.yaml
├── 04-seed-pod.yaml
├── 05-vm-rwo.yaml
├── 06-rwx-pvc.yaml
├── 07-dv-adoption.yaml
├── 08-vm-rwx.yaml
├── 09-rwx-reader-pod.yaml
└── evidence/
```

本次实际版本如下：

| 组件 | 版本或配置 |
| --- | --- |
| kind | `v0.31.0` |
| Kubernetes | `v1.35.0`，1 个 control-plane、2 个 worker |
| Kube-OVN | `v1.14.3` |
| KubeVirt | `v1.9.0` |
| CDI | `v1.66.0` |
| 存储 | 静态 NFS PV，`Retain`，`Filesystem`；数据位于 NFS 导出路径 |
| 网络 | Kube-OVN VPC `rwo-rwx-vpc`、Subnet `rwo-rwx-subnet` |

kind 集群配置：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/kind-config.yaml" >}}

Kube-OVN VPC 和 Subnet：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/01-vpc-subnet.yaml" >}}

NFS 服务：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/02-nfs.yaml" >}}

kind 集群关闭默认 CNI。先下载固定提交中的实验目录，以下命令均在该目录执行。

```bash
export EXPERIMENT_ROOT=/tmp/rwo-to-rwx-kind
git clone https://github.com/jimyag/k8sdev.git "$EXPERIMENT_ROOT"
git -C "$EXPERIMENT_ROOT" checkout cce508d0bc28d88d01c71b1378ee176f449b5f5c
cd "$EXPERIMENT_ROOT/rwo-to-rwx-kind"
kind create cluster --config kind-config.yaml
```

安装 Kube-OVN 时使用和清单匹配的版本，并显式打开热迁移优化：

```bash
cd "$EXPERIMENT_ROOT/rwo-to-rwx-kind"
curl -fsSL https://raw.githubusercontent.com/kubeovn/kube-ovn/v1.14.3/dist/images/install.sh \
  -o kube-ovn-install.sh
ENABLE_LIVE_MIGRATION_OPTIMIZE=true bash kube-ovn-install.sh
```

安装脚本可能因无法写入 `/usr/local/bin` 返回非零；应检查 `ovs-ovn`、`kube-ovn-cni`、`kube-ovn-controller` 和节点状态。VM 还需要设置 bridge 网络热迁移 annotation。[Kube-OVN Live Migration](https://kubeovn.github.io/docs/v1.14.x/en/kubevirt/live-migration/)

安装 KubeVirt 和 CDI：

```bash
export KUBEVIRT_VERSION=v1.9.0
kubectl --context kind-rwo-rwx-lab apply -f \
  https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml
kubectl --context kind-rwo-rwx-lab apply -f \
  https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml

export CDI_VERSION=v1.66.0
kubectl --context kind-rwo-rwx-lab apply -f \
  https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-operator.yaml
kubectl --context kind-rwo-rwx-lab apply -f \
  https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-cr.yaml
```

CDI 最终为 `Deployed`。KubeVirt 的 VM 和 VMIM 主链路正常，但 `virt-exportproxy` 未启动，KubeVirt CR 仍为 `Deploying`；本文不把它当作完整安装成功的证据。

## 2. 转换前：RWO PVC 不能直接热迁移

创建 Kube-OVN VPC/Subnet、NFS 服务、静态 PV 和 RWO PVC。

```bash
kubectl --context kind-rwo-rwx-lab apply -f 01-vpc-subnet.yaml
kubectl --context kind-rwo-rwx-lab apply -f 02-nfs.yaml
kubectl --context kind-rwo-rwx-lab apply -f 03-rwo-pv-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pvc/rwo-rwx-disk \
  --for=jsonpath="{.status.phase}"=Bound --timeout=5m
```

实验 PV 使用 `Retain`，NFS 的 `/` 作为固定后端路径。完整配置见固定提交中的 `03-rwo-pv-pvc.yaml`。

RWO PV/PVC：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/03-rwo-pv-pvc.yaml" >}}

用临时 Pod 写入固定内容，然后删除这个 Pod，避免它继续占用 RWO PVC：

写入哨兵文件的 Pod：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/04-seed-pod.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 04-seed-pod.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pod/seed-rwo-disk \
  --for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test logs pod/seed-rwo-disk
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pod seed-rwo-disk --wait=true
```

写入结果为：

```text
rwo-rwx-sentinel-20260919
8aa6c4c3f25107038ce031ffcd90a83071388e315b2d3049666e900fcc975b70  /data/sentinel.txt
```

然后创建引用 RWO PVC 的 VM。VM 使用 Kube-OVN bridge 网络，并带上允许 Pod bridge 网络热迁移的 annotation：

RWO 阶段的 VM：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/05-vm-rwo.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 05-vm-rwo.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
  --for=condition=Ready --timeout=10m
```

此时 VMI 为 `Running`，但 `LiveMigratable=False`：

```text
LiveMigratable=False reason=DisksNotLiveMigratable message=cannot migrate VMI: PVC rwo-rwx-disk is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
```

执行迁移也会在 admission 阶段失败：

```text
Error migrating VirtualMachine Internal error occurred: admission webhook "migration-create-validator.kubevirt.io" denied the request: Cannot migrate VMI, Reason: DisksNotLiveMigratable, Message: cannot migrate VMI: PVC rwo-rwx-disk is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
virtctl_rc=1
```

请求在创建 VMIM 前就被 admission webhook 拒绝。

## 3. 用户关机后转换 PVC/PV

### 3.1 先确认实例真的停干净

不能只看 VM 的 `runStrategy=Halted`，还要等待 VMI 删除并检查 Pod 和卷挂载：

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test patch vm rwo-rwx-vm \
  --type=merge -p='{"spec":{"runStrategy":"Halted"}}'

kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
  --for=delete --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get vm,vmi,pod,pvc -o wide
```

删除前先验证 PV 和 PVC 都还是 RWO，并记录 PV 与 PVC 的绑定关系：

```bash
kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv \
  -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ACCESS:.spec.accessModes[*],VOLUME_MODE:.spec.volumeMode,RECLAIM:.spec.persistentVolumeReclaimPolicy'
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get pvc rwo-rwx-disk \
  -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ACCESS:.spec.accessModes[*],VOLUME_MODE:.spec.volumeMode,VOLUME:.spec.volumeName'
```

本次实验的预期结果是 PV、PVC 均为 `Bound`、`ReadWriteOnce`，PVC 的
`volumeName` 指向 `rwo-rwx-disk-pv`。生产静态 Ceph Block PV 还要额外确认
`volumeMode=Block`，而不是把 `Filesystem` 卷当成 Block 卷处理。

生产实现还应确认 `virt-launcher`、`VolumeAttachment`、备份任务和 CDI worker 都不再占用该卷，并确认 guest 已 flush 文件系统。

### 3.2 删除 PVC/PV，保留后端数据并重建 RWX 对象

这里同时删除 PVC 和 PV 对象。NFS 服务和导出路径不删除，因此数据仍然保留。重新创建的 PV 不是原来的 Kubernetes 对象，但它指向同一个 NFS 后端路径。

这里的“分离”是指 PV/PVC 对象和 NFS 数据路径分开管理。实验中的 NFS 服务使用 `emptyDir`，删除 NFS 服务或整个 kind 集群仍会删除实验数据；本实验只验证删除 PV/PVC 不会删除 NFS 路径中的数据。

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pvc rwo-rwx-disk --wait=true
kubectl --context kind-rwo-rwx-lab delete pv rwo-rwx-disk-pv --wait=true
kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv --ignore-not-found
```

删除后旧 PV 对象不存在：

```text
没有输出
```

接着用相同的 NFS server/path 创建新的 RWX PV 和 PVC：

RWX PV/PVC：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/06-rwx-pvc.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 06-rwx-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pvc/rwo-rwx-disk \
  --for=jsonpath="{.status.phase}"=Bound --timeout=5m
kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv -o wide
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get pvc rwo-rwx-disk -o wide
```

结果：

```text
PV phase=Bound modes=ReadWriteMany claim=rwo-rwx-disk
PVC phase=Bound modes=ReadWriteMany volume=rwo-rwx-disk-pv
```

在第二个 worker 挂载 PVC，读取哨兵文件：

RWX 读盘 Pod：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/09-rwx-reader-pod.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 09-rwx-reader-pod.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pod/check-rwx-disk \
  --for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test logs pod/check-rwx-disk
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pod check-rwx-disk --wait=true
```

读取到的内容和转换前完全一致：

```text
rwo-rwx-sentinel-20260919
8aa6c4c3f25107038ce031ffcd90a83071388e315b2d3049666e900fcc975b70  /data/sentinel.txt
```

内容和 SHA-256 与转换前一致，说明重建对象没有覆盖后端数据。

### 3.3 生产静态 Ceph Block PV 的删建流程

PV 只是 Kubernetes 中描述 Ceph image 的对象，真正的 block 数据在 Ceph image
中。删除 PV/PVC 前，先确认这是静态 PV，回收策略为 `Retain`，并保存下面这些
信息：

- PV 和 PVC 当前都是 `ReadWriteOnce`，并且都处于 `Bound`；
- PVC 的 `spec.volumeName` 指向目标 PV；
- `volumeMode=Block`、容量和 `storageClassName`；
- CSI driver、`volumeHandle`、`clusterID`、`pool`、`imageName`、镜像特性和格式；
- Node Stage、Controller Expand 等 Secret 引用，以及其他节点阶段参数。

停止虚拟机并确认没有挂载后，删除 PVC 和 PV，分别等待两个对象真正消失。然后
使用同一个 `volumeHandle` 和同一组 CSI 字段重建 PV，只把访问模式改成 RWX：

```yaml
# 使用原来的 Ceph image 重建静态 PV，只修改访问模式，不创建新镜像。
apiVersion: v1
kind: PersistentVolume
metadata:
  # PV 名称需要与后续 PVC 的 volumeName 对应。
  name: <pv-name>
spec:
  capacity:
    # 容量必须和原 PV 保持一致。
    storage: <same-capacity>
  accessModes:
  # 重建后允许多个节点同时读写。
  - ReadWriteMany
  # Ceph RBD 以原始块设备提供给虚拟机。
  volumeMode: Block
  # 静态卷删除 Kubernetes 对象时保留 Ceph image。
  persistentVolumeReclaimPolicy: Retain
  # 静态 PV 不使用动态 StorageClass。
  storageClassName: ""
  csi:
    # CSI driver 和后端标识必须与原 PV 一致。
    driver: rbd.csi.ceph.com
    volumeHandle: <same-volume-handle>
    volumeAttributes:
      # 下面的 Ceph 参数必须指向原来的 image。
      clusterID: <same-cluster-id>
      pool: <same-pool>
      imageName: <same-image-name>
      imageFormat: "2"
      imageFeatures: <same-image-features>
      staticVolume: "true"
    # 节点挂载阶段使用的 Secret 必须继续有效。
    nodeStageSecretRef:
      name: <same-node-stage-secret>
      namespace: <same-secret-namespace>
    # 如果支持扩容，扩容阶段的 Secret 也要保留。
    controllerExpandSecretRef:
      name: <same-controller-expand-secret>
      namespace: <same-secret-namespace>
```

再创建同名 RWX PVC，并通过 `volumeName` 绑定刚重建的 PV。不能只看到 PVC
进入 `Bound` 就结束，还要检查 PV/PVC 都是 `ReadWriteMany`、`volumeMode=Block`、
`volumeHandle` 没变，并实际挂载读取数据。任何 CSI 字段缺失，都可能出现 PVC
显示 `Bound`、实际挂载失败的情况。

## 4. 系统盘的 DataVolume 与 Claim Adoption

系统盘通常由 CDI `DataVolume` 管理。先创建同名 RWX PVC，再创建同名 DV，并在 DV 的 metadata 上设置 annotation：

DataVolume 和 Claim Adoption 配置：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/07-dv-adoption.yaml" >}}

本次执行：

```bash
kubectl --context kind-rwo-rwx-lab apply -f 07-dv-adoption.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait dv/rwo-rwx-disk \
  --for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get dv,pvc -o wide
```

结果：

```text
DataVolume phase=Succeeded
PVC phase=Bound accessModes=[ReadWriteMany] volumeName=rwo-rwx-disk-pv
PVC ownerReference=DataVolume/rwo-rwx-disk
```

Claim Adoption 只恢复 `DataVolume -> PVC` 的管理关系，不会修改访问模式、复制数据或格式化磁盘。DataVolume Claim Adoption

{{< github-file url="https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/datavolume-claim-adoption.md" >}}

目标 PVC 已经存在时，annotation 必须随 DV 创建请求一起发送；没有 annotation 时，CDI admission webhook 会拒绝请求，DV 不会创建。

## 5. 改成 RWX 后执行 KubeVirt/Kube-OVN 热迁移

将 VM 置为 `Halted`，应用引用 DataVolume 的最终规格，再启动 VM：

RWX 阶段的 VM：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind/08-vm-rwx.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 08-vm-rwx.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test patch vm rwo-rwx-vm \
  --type=merge -p='{"spec":{"runStrategy":"Always"}}'

kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
  --for=condition=Ready --timeout=10m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get vm,vmi,pvc,dv -o wide
```

此时 VMI 的 `LiveMigratable=True`，执行迁移：

```bash
virtctl \
  --context kind-rwo-rwx-lab -n rwo-rwx-test migrate rwo-rwx-vm
```

迁移成功后，同时检查 VMIM、VMI、launcher、PVC 和 Kube-OVN IP。

| 检查项 | 结果 |
| --- | --- |
| VMIM | `kubevirt-migrate-vm-jcrtl`, `Succeeded`, `completed=true` |
| VMIM 源/目标 | `rwo-rwx-lab-worker` -> `rwo-rwx-lab-worker2` |
| VMIM 阶段 | `Pending -> Scheduling -> Scheduled -> PreparingTarget -> TargetReady -> Running -> Succeeded` |
| VMI | `Running`, `Ready=True`, `LiveMigratable=True` |
| IP | 迁移前后均为 `10.251.0.19` |
| Kube-OVN IP | 归属目标节点 `rwo-rwx-lab-worker2` 和 `rwo-rwx-subnet` |
| PVC | `Bound`, `ReadWriteMany`, `volumeName=rwo-rwx-disk-pv` |
| DV | `Succeeded` |
| 数据 | 哨兵文件内容和 SHA-256 不变 |

源 launcher 可能短暂处于 `Completed`。应等 VMIM 有 `completed=true` 和 `endTimestamp`，VMI 切到目标节点，旧 launcher 被清理，PVC/DV 状态稳定后再返回成功。

## 6. 生产注意事项

### 后端和访问模式

只允许复用后端数据仍然存在的卷，例如静态 Ceph RBD PV。转换前核对 CSI driver、`volumeHandle`、RBD image、pool、clusterID、`volumeMode=Block`、节点阶段参数、Secret 引用和 `persistentVolumeReclaimPolicy: Retain`。节点本地盘、临时盘、`local-path` 和依赖原节点目录的 RWO 卷不在范围内。

本地 RWO 不能靠重建 Kubernetes 对象变成共享存储，需要使用 KubeVirt Volume Migration 或应用层复制。[KubeVirt Volume Migration](https://kubevirt.io/user-guide/storage/volume_migration/)

### 停机和任务状态

转换期间要阻止启动、挂卸盘、快照、恢复、扩容、删除和再次迁移。任务应保存原 PV/PVC/DV UID、后端句柄、旧新规格和当前阶段；删除对象时使用 UID precondition。PVC `Bound` 只能说明绑定完成，不能单独作为任务成功条件。

### DataVolume 和热迁移验收

系统盘要同时检查 DV `Succeeded`、PVC `Bound + RWX`、PVC ownerReference、新 DV 的后端句柄，再恢复 VM。只重建 PVC 可能让 CDI 按旧 DV 规格重建 PVC，或触发错误的镜像导入。

### `RWX` 不是存储能力证明

PV/PVC 显示 `RWX` 只代表对象匹配成功，不代表 CSI、块设备和 KubeVirt 设备组合都支持多节点读写。本文 kind fixture 使用 NFS `Filesystem` 验证对象重建和热迁移链路；生产静态 Ceph RBD `Block` 还必须按上一节保留完整 CSI 信息，并做真实挂载验证。

### 网络和终态

共享盘满足后，CPU、资源、VMI 网络、OVN 端口或目标节点状态仍可能导致迁移失败。Kube-OVN 需要：

```yaml
# 允许 KubeVirt bridge Pod 网络在热迁移中切换节点端口。
annotations:
  kubevirt.io/allow-pod-bridge-network-live-migration: "true"
```

验收要检查 VMIM `Succeeded + completed=true + completedAt/endTimestamp`、VMI 目标节点、目标 launcher、Kube-OVN IP、PVC/DV、旧资源清理和 guest 探针。HTTP `202`、VMIM 创建、PVC `Bound` 或 Pod `Running` 都不是终态。

## 7. 清理

实验结束后清理 kind 集群：

```bash
kind delete cluster --name rwo-rwx-lab
```

实验清单、原始结果和固定提交仍保留在 [k8sdev `cce508d0`](https://github.com/jimyag/k8sdev/tree/cce508d0bc28d88d01c71b1378ee176f449b5f5c/rwo-to-rwx-kind)。

