管理裸金属主机上的数据磁盘

使用 MachineInventory.spec.storage 管理已经附加到裸金属主机上的数据磁盘。存储声明属于长期存在的 MachineInventory,而不是临时的 Cluster API Machine,因此当节点被移除后,provider 可以保留文件系统和数据,并在之后复用同一个 inventory。

此功能不会创建、附加、分离、迁移、备份或复制物理磁盘或 SAN LUN。它只管理 spec.storage.volumes[] 中显式列出的设备。所有未列出的设备都会被观测到,但不会被管理。

DANGER

InitializeIfBlank 可能会格式化磁盘

仅在你已经验证目标 inventory 上确切的稳定设备 ID、确认该设备是可丢弃的空白数据磁盘,并且已独立备份任何重要数据之后,才使用 InitializeIfBlank。切勿从其他主机复制设备 ID,也不要将 /dev/sdX/dev/vdX/dev/nvmeXnY/dev/mapper/mpathX 作为持久标识。

可用性与升级边界

受管存储需要来自兼容的 Bare Metal Storage 版本的全部三个组件:

  • Bare Metal provider,包括其 storage controller 和 fail-closed 的 MachineInventory admission webhook。
  • 匹配的 elemental-operator CRD 和 observer API。
  • 一个主机 OS 镜像,该镜像会安装并启用 elemental-storage-observer.service 以及早期启动阶段的存储集成。
WARNING

在启用受管存储之前,请从 Alauda 技术支持获取匹配的 base-imagebase-image-isobase-image 用于主机重新配置,base-image-iso 用于 SeedImage / live installation。这些镜像是目标版本的受支持配对;不要从其中一个镜像引用推导另一个镜像,也不要用独立构建的镜像替代。

仅在 global 集群上升级 provider 和 elemental-operator,不会在由旧 OS 镜像创建的主机上安装 observer 服务。

  • spec.storage 仍然不存在或其 volumes: [] 时,现有集群在管理侧升级后可以继续使用非受管存储。此路径不会创建 storage plan,也不会修改磁盘。
  • 要在旧主机上启用受管存储,首先使用已批准且具备 observer 能力的镜像重新安装、重置或替换该主机。在添加任何非空存储声明之前,请先确认 observer 服务可用,并且存在新的 status.observedStorage 报告。
  • 不要向正在运行或已分配的 inventory 添加 spec.storage.volumes[]。只有在 inventory 未分配且所有受管 volume 都处于非活动状态时,才可以更改存储声明。

存储生命周期

阶段触发条件期望状态主机行为
Observe主机注册并且 observer 报告设备。UnmanagedObserved只读发现;不会自动选择任何设备。
Prepareoperator 在可用 inventory 上保存了一个非空的存储声明。PreparedStoragePrepared=True采用现有文件系统,或初始化一个显式授权的空白设备。业务路径保持未挂载。
ActivateCluster API 将已准备好的 inventory 分配给 BaremetalMachineActiveStorageActive=True重新配置 plan 会安装激活状态;所需的挂载单元会在 kubelet 之前启动。
Deactivate拥有该 inventory 的 Cluster API Machine 被删除。PreparedStorageActive=False清理 plan 会卸载受管 volume。文件系统、存储所有权和数据都会保留。
Release在 inventory 可用且非活动时,从 spec.storage 中移除 volume。UnmanagedStoragePrepared=True/NoManagedVolumes会移除存储所有权和激活元数据,但不会擦除文件系统或数据。

删除 Machine 执行的是 Deactivate,不是 Release。因此,正常的 scale-down 会保留 inventory 的存储声明和数据,以便以后复用。

前提条件

  1. 安装包含 Storage 支持的 Bare Metal provider 版本,并等待其 manager deployment 和 admission webhook 变为 Ready。
  2. 从 Alauda 技术支持获取匹配的、具备 observer 能力的 base-imagebase-image-iso。使用 ISO 镜像安装主机,并配置配对的 base image 以便后续重新配置。
  3. 在 Prepare 成功之前,将目标 MachineInventory 保持在生产池之外。它必须是 Ready=True、未分配,并且具有默认 plan Secret。
  4. 独立备份现有数据。存储保留不等同于应用一致性备份。
  5. 获取由完全相同的 provider revision 构建的 storagectl 二进制文件。storagectl 会复用 provider 的规范化、验证和 hash 代码;不要手工计算初始化批准 hash,也不要使用其他版本的二进制文件。
  6. 提交 InitializeIfBlank 的身份必须被授权对虚拟的 machineinventories/storageinitialize 子资源执行 update。纯 Adopt 声明不需要此额外权限。

设置操作相关变量:

export BM_NS=cpaas-system
export BM_INVENTORY='<machineinventory-name>'
export BM_STORAGECTL='<path-to-matching-storagectl>'
export BM_STORAGE_FILE=/tmp/baremetal-storage.yaml
export BM_INVENTORY_FILE=/tmp/baremetal-machineinventory.json
export BM_PATCH_FILE=/tmp/baremetal-storage.patch.json

验证管理组件和授权:

kubectl -n "$BM_NS" rollout status \
  deployment/cluster-api-provider-baremetal-manager --timeout=5m
kubectl get mutatingwebhookconfiguration \
  cluster-api-provider-baremetal-mutating-webhook-configuration
kubectl get validatingwebhookconfiguration \
  cluster-api-provider-baremetal-validating-webhook-configuration
kubectl auth can-i update machineinventories.elemental.cattle.io \
  --subresource=storageinitialize -n "$BM_NS"

在该身份可以提交或更改初始化批准之前,最后一个命令必须返回 yes。仅将此权限授予被授权格式化磁盘的特定管理员用户或 ServiceAccount。

第 1 步:验证 inventory 和 observer

确认该 inventory 可用且当前没有所有者:

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  -o jsonpath='uid={.metadata.uid}{"\n"}allocation={.metadata.annotations.baremetal\.alauda\.io/allocation-state}{"\n"}baremetalMachine={.metadata.annotations.baremetal\.alauda\.io/owner-baremetalmachine}{"\n"}machine={.metadata.annotations.baremetal\.alauda\.io/owner-machine}{"\n"}cluster={.metadata.annotations.baremetal\.alauda\.io/owner-cluster}{"\n"}plan={.status.plan.secretRef.name}{"\n"}'

allocation 值必须为空或为 Available;所有 owner 值都必须为空。如果 inventory 已分配,请删除拥有它的 CAPI Machine,并等待正常的清理和 Deactivate 流程完成。不要手动移除 owner annotation 或 finalizer。

在主机上验证 observer:

sudo systemctl is-enabled elemental-storage-observer.service
sudo systemctl is-active elemental-storage-observer.service
sudo systemctl cat elemental-storage-observer.service

这两个 state 命令都必须成功。在 global 集群上,检查观测水位线和设备事实:

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  -o jsonpath='{.status.observedStorage.observedAt}{" epoch="}{.status.observedStorage.reportEpoch}{" sequence="}{.status.observedStorage.sequence}{" bootID="}{.status.observedStorage.bootID}{"\n"}'

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -o json \
  | jq -r '.status.observedStorage.devices[] |
      [.id,.kind,.systemRole,(.sizeBytes|tostring),(.filesystem.type//"-"),
       (.filesystem.uuid//"-"),([.mounts[].path]|join(",")),
       ((.health.activePaths//0)|tostring),((.health.totalPaths//0)|tostring)] | @tsv'

仅使用报告是最新且其 systemRoleData 的设备。

设备类型稳定 ID支持的策略
直连磁盘wwn:nvme-eui:nvme-nguid:,或 observer 批准的 serial: IDAdoptInitializeIfBlank
现有分区partuuid:Adopt
Multipath 聚合映射wwid:Adopt,或在整个映射为空白时使用 InitializeIfBlank

不要选择 system 或 Unknown 设备、Multipath 成员、只读或可移动设备,或具有意外分区表、文件系统、LVM、RAID、LUKS、swap、consumer 或挂载的设备。不要同时选择整盘和其某个分区。

第 2 步:定义受管 volume

创建存储声明。示例中故意使用占位符;请将每个设备 ID、UUID、容量和挂载路径替换为来自同一目标 inventory 的实际信息。删除任何不适用于该主机的 volume。

baremetal-storage.yaml
storage:
  retryNonce: 0
  volumes:
    - name: cpaas-data
      source:
        deviceID: <blank-direct-disk-id>
        minimumSize: 100Gi
      filesystem:
        policy: InitializeIfBlank
        type: xfs
      mount:
        path: /var/cpaas
        required: true
        options:
          - noatime

    - name: application-data
      source:
        deviceID: <existing-partition-id>
        minimumSize: 100Gi
      filesystem:
        policy: Adopt
        type: ext4
        expectedUUID: <existing-filesystem-uuid>
      mount:
        path: /data/application
        required: true

    - name: registry-data
      source:
        deviceID: <multipath-map-id>
        multipath:
          minimumActivePaths: 2
      filesystem:
        policy: Adopt
        type: xfs
        expectedUUID: <existing-filesystem-uuid>
      mount:
        path: /srv/registry
        required: true
        options:
          - noatime

Storage v2 每个 inventory 最多支持 32 个 volume。第一个版本支持整设备 XFS 或 ext4、通过 Adopt 使用现有的 XFS/ext4 分区,以及整条 multipath 映射。它不会创建分区,也不会管理 LVM、mdraid、LUKS、swap、共享集群文件系统,或多主机读写访问。

required 默认值为 true。可选 volume 仍然会被准备和激活;变化的只是其失败到 Ready 的行为。/var/cpaas 始终必须是必需的。provider 会为受管挂载添加 nodev,nosuid,并使用 allowlist 验证每个额外的挂载选项。

在本地验证该声明:

"$BM_STORAGECTL" hash --storage "$BM_STORAGE_FILE"

第 3 步:渲染并应用原子补丁

始终从当前的 live object 开始,以便补丁包含最新的 UID 和 resourceVersion

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -o json \
  >"$BM_INVENTORY_FILE"

对于包含 InitializeIfBlank 的声明,请选择一个严格大于 status.storage.lastConsumedInitializationApprovalCounter 的正整数计数器,并渲染补丁:

jq '.status.storage.lastConsumedInitializationApprovalCounter // 0' \
  "$BM_INVENTORY_FILE"

"$BM_STORAGECTL" render-patch \
  --inventory "$BM_INVENTORY_FILE" \
  --storage "$BM_STORAGE_FILE" \
  --counter <next-initialization-counter> \
  >"$BM_PATCH_FILE"

生成的 merge patch 会将批准绑定到当前的 Inventory UID、规范化后的 storage hash、计数器和 resourceVersion。对于仅 Adopt 的声明,请省略 --counter

运行 server-side dry-run,再次检查生成的 spec.storage、device ID、path 和批准,然后再应用:

kubectl -n "$BM_NS" patch machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  --type=merge --patch-file="$BM_PATCH_FILE" --dry-run=server -o yaml

kubectl -n "$BM_NS" patch machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  --type=merge --patch-file="$BM_PATCH_FILE"

如果 API 报告 resourceVersion 冲突,请再次获取 inventory,重新生成补丁,重复 server dry-run,然后再应用。切勿从生成的补丁中移除 resourceVersion

第 4 步:等待 Prepare

监视存储状态:

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -w

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -o json \
  | jq '.status.storage | {
      phase,appliedSpecHash,pendingSpecHash,
      lastConsumedInitializationApprovalCounter,conditions,appliedVolumes,operation}'

在 inventory 可以被分配之前,预期结果是:

phase=Prepared
StoragePrepared=True/AllRequiredVolumesPrepared
StorageActive=False/Inactive
operation=null

在此阶段,已初始化的 volume 具有文件系统 UUID,已采用的 volume 保留其原始 UUID,但业务挂载路径尚未激活。在 Prepare 完成之前,不要将该 inventory 加入生产 MachineInventoryPool

第 5 步:分配并验证激活

将已准备好的 inventory 添加到目标 MachineInventoryPool,然后按照 在裸金属上创建集群管理裸金属节点 中的说明创建或扩展 CAPI machine group。

在重新配置完成后,验证 machine 和 storage 状态:

kubectl -n "$BM_NS" get baremetalmachines.infrastructure.cluster.x-k8s.io -o json \
  | jq -r --arg inventory "$BM_INVENTORY" \
      '.items[] | select(.status.machineInventoryRef.name==$inventory) |
       [.metadata.name,.status.phase,.status.ready,
        ([.status.conditions[]|select(.type=="StorageReady")|(.status+"/"+.reason)]|join(","))] | @tsv'

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -o json \
  | jq '.status.storage | {phase,conditions,appliedVolumes,operation}'

预期结果为 BaremetalMachine.status.phase=RunningStorageReady=True/AllVolumesReadyMachineInventory.status.storage.phase=ActiveStorageActive=True/AllRequiredVolumesActive

在主机上,确认每个源 UUID、文件系统类型、挂载路径和挂载选项。所需的挂载单元必须在 kubelet 之前进入 active 状态:

sudo findmnt --mountpoint /var/cpaas
sudo systemctl is-active "$(systemd-escape --path --suffix=mount /var/cpaas)"
sudo systemctl show "$(systemd-escape --path --suffix=mount /var/cpaas)" \
  -p ActiveEnterTimestamp
sudo systemctl show kubelet.service -p ActiveEnterTimestamp

第 6 步:scale down 并复用 inventory

通过正常的 scale-down 或替换工作流删除拥有该 inventory 的 CAPI Machine。不要删除 MachineInventory,也不要移除其 storage finalizer。

在 drain、clean 和 Deactivate 完成后,预期如下:

allocation-state=Available
status.storage.phase=Prepared
StoragePrepared=True
StorageActive=False/Inactive
business mount paths are inactive
filesystem UUIDs and data are retained

当同一个 inventory 再次被分配时,Activate 会复用现有的所有权记录和文件系统 UUID;它不会再次格式化该设备。通用 pool 分配并不保证替换的 Machine 会获得相同的 inventory。如果某个应用必须回到特定 inventory 上的主机本地数据,请先将该 inventory 放在受控的有序 pool 中,或使用专用 pool,并在依赖这些数据之前验证所选的 BaremetalMachine.status.machineInventoryRef

第 7 步:释放存储所有权

仅当 inventory 未分配且处于非活动状态时,才释放 volume。移除 volume 是非破坏性的:它会删除 Storage v2 所有权和激活元数据,但会保留文件系统 UUID 和数据。

要释放所有 volume,请创建一个显式的空声明:

baremetal-storage-empty.yaml
storage:
  volumes: []

获取 live inventory,并在不使用初始化计数器的情况下渲染、dry-run 和应用补丁:

kubectl -n "$BM_NS" get machineinventory.elemental.cattle.io "$BM_INVENTORY" -o json \
  >"$BM_INVENTORY_FILE"

"$BM_STORAGECTL" render-patch \
  --inventory "$BM_INVENTORY_FILE" \
  --storage /tmp/baremetal-storage-empty.yaml \
  >"$BM_PATCH_FILE"

kubectl -n "$BM_NS" patch machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  --type=merge --patch-file="$BM_PATCH_FILE" --dry-run=server -o yaml
kubectl -n "$BM_NS" patch machineinventory.elemental.cattle.io "$BM_INVENTORY" \
  --type=merge --patch-file="$BM_PATCH_FILE"

等待 phase=UnmanagedStoragePrepared=True/NoManagedVolumes、空的 appliedVolumes[] 以及 operation=null。在需要时,通过只读挂载验证保留的数据。live inventory 上可能仍会保留 storage finalizer,以便后续对象删除仍按正确的清理顺序执行;不要手动移除它。

失败操作与重试

存储操作采用 fail-closed 策略。缺失、过期、繁忙、过小、降级、从未知来源挂载,或不再匹配其声明 UUID 的必需设备,都会阻止分配或 Machine 就绪。controller 不会自动选择替代磁盘。

在纠正 status.storage.operation.phaseFailed 的操作外部原因后,请在保持 volumes[] 不变的情况下递增 spec.storage.retryNonce,并使用 storagectl 渲染新的补丁。retryNonce 会为同一个逻辑操作创建另一次执行尝试。它不会授权首次格式化、绕过预检查,也不会允许不同的存储声明。

不要将重试用作未知或部分变更声明的恢复机制。当 pendingSpecHash 非空,或失败操作报告 mutationState=DurableStateChangedUnknown 时,请保留原始声明并允许同一操作日志收敛。在考虑任何 break-glass finalizer 或所有权操作之前,请联系技术支持。

限制

  • 当 inventory 已分配或任何受管 volume 处于活动状态时,volumes[] 不能更改。
  • 已准备好的 volume 的 deviceID、文件系统策略/类型或名称不能原地修改。请先释放旧 volume,等待完成,然后再添加替换声明。
  • Storage v2 不会在 inventory 之间移动数据。由 inventory 拥有的数据会保留在该 inventory 所标识的物理主机或 LUN 上。
  • Multipath 支持仅管理聚合的 wwid: 映射。它不会配置 multipath、SAN zoning、LUN masking、fencing,或多主机读写文件系统。
  • /var/lib/kubelet/var/lib/containerd/var/lib/etcd/etc/usr/boot 以及其他系统路径都受保护。请使用业务路径,例如 /var/cpaas/data/*/srv/*/mnt/*,它们不得与受保护路径重叠。
  • 不要通过 MachineInventory.spec.storage 和 kubeadm/cloud-init disk_setupfs_setupmounts、自定义 systemd 单元或主机脚本同时管理同一个设备或路径。

相关文档