Hyper-V 虚拟机迁移至移动硬盘实战笔记

目标:把 Hyper-V 上的若干台虚拟机从内置盘迁移到移动硬盘上长期运行,腾出内置盘空间;外置盘按 VM 名分目录组织(与你 D: 现存结构对齐),同时保留"可拔盘带去别的 Win11 复用"的特性
核心思路:用 Move-VM 一条命令指定每台 VM 的新路径(VHDX / 配置 / 快照三处都自定义),不动 .vmcx 任何字节,整台 VM 的资产即可在外置盘上重新落位并跑起来
可选副产物:跑完 Move-VM 后还可再跑一次 Export-VM,在外置盘另一路径下生成 Hyper-V 标准便携包——同时拥有一份"插盘即可导入"的全机器版本
工作示例:本文以 7 台 RHEL8.10 集群节点(RHEL8-Node1 ~ RHEL8-Node7)为主线示例,所有步骤对任意 Hyper-V VM 通用
适用系统:Windows 11 / 10 + Hyper-V(Windows Server 同思路);移动硬盘 NTFS 或 exFAT 均可
更新日期:2026-07-31


目录


一、本教程解决什么问题

把 Hyper-V 虚拟机搬到外置盘上,通常背后是这两类诉求——本教程给出的方案对它们同时覆盖

🎯 诉求 📝 说明
本机磁盘空间紧张 内置盘剩余空间告急;让 VM 文件跑到外置盘上、内置盘腾出来
可换机 / 异地使用 拔下盘带到另一台装有 Hyper-V 的 Win11 上插上就能用

Move-VM 是 Hyper-V 为"在 Hyper-V 主机内搬迁 VM 资产"设计的官方命令——它能精确指定每台 VM 在新位置的 VHDX / 配置 / 快照路径,且不修改任何 .vmcx 内容(只是更新 Hyper-V 注册里的路径指针)。整台 VM 在外置盘上的最终目录结构可以和原内置盘完全同形,便于排查和维护。


二、总体方案:Move-VM + 可选 Export-VM

主流程就两步:

🔢 步骤 🛠️ 动作 📦 工具
Move-VM 把 VM 的 VHDX、配置文件、快照一次性移到外置盘的指定路径;移动完 VM 当场可跑 Move-VM
清理源盘 删除内置盘上的旧文件腾出空间 Remove-Item 文件删除

如果还想"换机时即插即用"或者"多一份离线归档",在 ① 之后加一步:

🔢 步骤 🛠️ 动作 📦 工具
(可选)Export-VM 在外置盘另一目录下生成每台 VM 的自包含标准包——本身"插盘即可导入" Export-VM

③ 是可选的——Move-VM 后的结构换机时通过 Import-VM -Register 重建一次即可(多 1 分钟),但不少用户更希望"插盘就能用"。看个人需要。

Move-VM 之后,外置盘上的目录结构长这样

VHDX 部分(你能自定义到这层)

F:\Virtual Hard Disks\
├── RHEL8-Node1\
│   └── RHEL8-Node1.vhdx              ← 和你 D: 上的目录结构对齐
├── RHEL8-Node2\
│   └── RHEL8-Node2.vhdx
├── ...
└── RHEL8-Node7\
    └── RHEL8-Node7.vhdx

VM 配置 / 快照部分(Hyper-V 固定用 GUID 子目录)

F:\Hyper-V\
├── Virtual Machines\
│   └── <VM的 GUID>\
│       ├── <VM的 GUID>.vmcx          ← VM 配置
│       ├── <VM的 GUID>.vmgs          ← 虚机密钥
│       └── <VM的 GUID>.VMRS          ← 状态文件
└── Snapshots\
    └── <快照 GUID>\
        └── <快照 GUID>.avhdx         ← 快照(每台 VM 自己的子目录)

VHDX 部分按 <VM名>\<VM名>.vhdx 排好,是因为这条路径在 Move-VM由你显式指定
VM 配置 / 快照部分是 Hyper-V 自身用 GUID 命名(但一台 VM 一个 GUID 子目录,本质上还是每台 VM 独占自己的子目录)。

直观对比 D: 上的现状

路径类型 📍 D: 上原位置 🎯 F: 上新位置(Move-VM 后)
VHDX D:\Virtual Hard Disks\<VM名>\<VM名>.vhdx F:\Virtual Hard Disks\<VM名>\<VM名>.vhdx
VM 配置 D:\Hyper-V\Virtual Machines\<GUID>\*.vmcx F:\Hyper-V\Virtual Machines\<GUID>\*.vmcx
快照 D:\Hyper-V\Snapshots\<GUID>\*.avhdx F:\Hyper-V\Snapshots\<GUID>\*.avhdx

完成 ① ② 之后:内置盘释放几十 GB、外置盘有"按 VM 名分目录"的整洁结构、拔盘到任何 Win11 上 5 分钟内重建注册可用。


三、准备工作

3.1 关停目标 VM

Move-VM 要求 VM 在 Off 状态(Saved 也可)。Running 状态的 VM 必须先关停。

第 1 步:先看清本机有哪些 VM——避免后面 $vmNames 写错名。

# PowerShell(右键以管理员身份运行)

# 名字 + 状态 + 当前 VHD 路径(足够决定怎么 Move-VM)
Get-VM | Format-Table Name, State,
  @{N='VHDPath';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize -Wrap

参考输出(你的实际列表可能不同):

Name        State VHDPath
----        ----- -------
RHEL8-Node1 Off   D:\Virtual Hard Disks\RHEL8-Node1\RHEL8-Node1.vhdx
...
Win11-Test  Off   D:\Virtual Hard Disks\Win11-Test.vhdx
虚拟机完成初始化 Off ...

不显示"配置目录"列:Hyper-V 的 ConfigurationLocation 属性在本机返回的是"声明路径"(如 D:\Hyper-V),跟 .vmcx 真实位置(D:\Hyper-V\Virtual Machines\<GUID>\)对不上,没参考价值。需要时用 9 章 FAQ 第二条里的命令查。

第 2 步:填好 $vmNames(两种写法选一种):

# 方法 A:手写(适合"我已经知道名字")
$vmNames = @("RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
             "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7")

# 方法 B:按通配符自动提取(适合"按名字模式批量挑")
# 例:挑出所有 RHEL8-* 命名的 VM
$vmNames = (Get-VM | Where-Object { $_.Name -like "RHEL8-*" }).Name

更多写法见 3.4 节

第 3 步:看一眼当前状态

Get-VM -Name $vmNames | Select-Object Name, State | Format-Table -AutoSize

# 一键关停
Get-VM -Name $vmNames | Where-Object State -eq "Running" | Stop-VM -Force

# 再确认一遍
Get-VM -Name $vmNames | Where-Object State -ne "Off" | Select Name, State

最后一条命令没有输出 = 全部 Off,可进入下一步。

3.2 在外置盘上"镜像"出 D: 的目录结构

Move-VM 不会自动在目标位置建中间目录——你要的目标位置必须先建好

# 外置盘根:本文统一用 F: 作演示;你的移动硬盘是哪个盘符就改哪个
# 三个顶层目录镜像 D: 上的命名
New-Item -ItemType Directory -Path "F:\Virtual Hard Disks"       -Force
New-Item -ItemType Directory -Path "F:\Hyper-V\Virtual Machines" -Force
New-Item -ItemType Directory -Path "F:\Hyper-V\Snapshots"        -Force

# 为每台待迁 VM 预先建好 VHDX 子目录(Move-VM 不会自动建)
foreach ($name in $vmNames) {
  New-Item -ItemType Directory -Path "F:\Virtual Hard Disks\$name" -Force | Out-Null
}

Move-VM 后 VHDX 会出现在 <VM名>\<VM名>.vhdx——子目录是 <VM名>\,VHDX 文件名同 VM 名;和你 D: 上的结构一样。

验证目录已建好

"===== F: 顶层 ====="
Get-ChildItem "F:\" | Where-Object PSIsContainer

"===== VM 子目录 ====="
$vmNames | ForEach-Object {
  $exists = Test-Path "F:\Virtual Hard Disks\$_"
  "  $_  →  $exists"
}

3.3 关于快照:先决定保留还是合并

Move-VM完整保留所有检查点(.avhdx),快照结构和父 VHDX 一起迁移,不丢不破。
但如果某些快照只是"临时中间态"可以先合并:

🛤️ 路线 🛠️ 操作 🎯 适合
A. 带快照 Move(默认) 直接 Move-VM,快照自动跟着搬 检查点里有重要中间状态
B. 先合并再 Move Hyper-V 管理器 → 选中 VM → 检查点面板 → 选中检查点 → 删除(合并进父 VHDX) 检查点只是临时态
# Move 前列出所有目标 VM 的检查点(看清状态再做决策)
foreach ($name in @("VM1", "VM2", "VM3")) {
  Write-Host "==== $name ===="
  Get-VMSnapshot -VMName $name | Select Name, SnapshotType, CreationTime | Format-Table -AutoSize
}

3.4 待导出 VM 列表的确定

Move-VM 按 VM 为单位迁移——在写 $vmNames 之前,先看清本机当前 Hyper-V 上有哪些 VM

第一步:先查(看清本机 VM 列表)

3.1 节 第 1 步那个命令直接复用即可:

Get-VM | Format-Table Name, State,
  @{N='VHDPath';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize -Wrap

第二步:筛选(决定要迁哪些)

# 方法 A:手写名单(最适合"我已经知道名字"的情况)
$vmNames = @("RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
             "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7")

# 方法 B:从全量列表按通配符筛(适合"知道名字模式"的情况)
$vmNames = (Get-VM | Where-Object Name -like "RHEL8-Node*").Name

# 方法 C:组合(手动 + 筛选)
$vmNames = @("Win11-Test") + (Get-VM | Where-Object Name -like "RHEL8-Node*").Name

# 方法 D:迁移所有 Off 状态的 VM(适合"先一股脑都带走,到新机器再删")
$vmNames = (Get-VM | Where-Object State -eq "Off").Name

# 排除不要的(比如一堆中文命名的测试 VM):
$vmNames = (Get-VM | Where-Object Name -notmatch "^[一-龥]" -and State -eq "Off").Name

第三步:确认($vmNames 是否符合预期)

# 看看最终决定要导哪些 VM
"===== 待迁移 VM 列表($($vmNames.Count) 台) ====="
$vmNames | ForEach-Object { "  $_" }
""
# 对比"Get-VM -Name $vmNames"匹配到的实际数量
"===== Get-VM 匹配核验 ====="
$matched = Get-VM -Name $vmNames
$matched | Select-Object Name, State | Format-Table -AutoSize
""
"匹配数:$($matched.Count) / $vmNames.Count"
if ($matched.Count -ne $vmNames.Count) {
  Write-Warning "匹配数和列表数量不一致,请检查写错的 VM 名" -WarningAction Continue
}

同名 VM 冲突:如果目标机器(换机场景)已经有同名 VM 在跑,迁过去后需 Import-VM -GenerateNewId 拿新 GUID 避免冲突。

是否每台都要迁?

源机器上若有"克隆自同一母版的多台 VM"(如本文的 7 台 RHEL8 节点全部由 Node1 克隆),存在权衡:

🔢 数量 💰 外置盘占用 🎯 评估
每台全 Move(推荐先这样做) N × 单台 VHDX 每台差异化状态都保留;适合"已经按节点装好不同应用"的场景
只 Move 母版 + 新机器上克隆 1 × 单台 VHDX 仅适合源机器还没做差异化部署

本文示例的 7 台 RHEL8 节点默认全 Move——按 RHEL 8.10 集群笔记规划,Node3-5 装 MongoDB、Node1-2 装 Tomcat、Node6-7 装 Nginx,差异化部署后每台不可重建,必须全 Move。

3.5 盘符策略:提前想好跨机器的稳定性

核心难点:VM 配置里写死的 VHDX 路径包含盘符。移动硬盘在不同机器上盘符可能不一样(从 F: 变 G:),VM 注册的引用就失效了。

推荐做法所有目标机器上手动固定盘符

  1. 右键"此电脑" → 管理磁盘管理
  2. 右键移动硬盘的分区 → 更改驱动器号和路径更改
  3. F:(或你定的固定盘符)→ 确定

简单粗暴,但每次插新机器都要手动分配一次。如果只在固定 2-3 台机器间切换,最省事。
本文统一以 F: 为例;你的实际盘符把脚本里的 F: 整体替换为你的盘符即可。


四、Move-VMStorage 操作

本章使用的 cmdlet 是 Move-VMStorage,不是 Move-VM
Move-VM 设计上是给"跨主机"搬迁用的——同主机搬迁 VHDX 时即使用 -DestinationHost $env:COMPUTERNAME 也无法绕过 VMMS 兼容性校验,会报"序列不包含任何匹配元素"。Move-VMStorage 是为同主机搬迁设计的官方 cmdlet,没有这个问题,且会自动改写 .vmcx 里的 VHDX 路径引用。

4.1 单台 Move-VMStorage 命令模板

前置4.5 节 已合并完所有检查点(VHDX 是 <VM名>.vhdx 无 GUID 后缀)。

RHEL8-Node1 为例,用三参数组合精确控制目标路径:

$name = "RHEL8-Node1"

# 当前 VHDX 在 D: 上的真实路径(脚本里读出来,避免硬编码)
$vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path
Write-Host "源: $vhdCurrent"

# F: 顶层建目标子目录
New-Item -ItemType Directory -Path "F:\Virtual Hard Disks\$name" -Force | Out-Null

# 三参数分开指定:VHDX 平铺 + 配置/快照走标准位置
# 注意:-VirtualMachinePath / -SnapshotFilePath 参数**不要加** `Virtual Machines\` / `Snapshots\` 后缀
#       Move-VMStorage 会自动追加这两个子目录——详见下面的"Magic 子目录"说明
Move-VMStorage -VMName $name `
  -Vhds @(@{
    SourceFilePath      = $vhdCurrent
    DestinationFilePath = "F:\Virtual Hard Disks\$name\$name.vhdx"
  }) `
  -VirtualMachinePath "F:\Hyper-V" `
  -SnapshotFilePath   "F:\Hyper-V"
参数 📝 含义
-VMName 目标 VM 名
-Vhds @{...} hashtable 精确指定每块磁盘的源/目标路径。源和目标文件名必须一致——否则报"源文件名和目标文件名必须匹配"
-VirtualMachinePath .vmcx / .vmgs / .VMRS 新位置(Hyper-V 自动在该目录下建 <GUID>\ 子目录)
-SnapshotFilePath .avhdx 新位置(每个快照一个 <GUID>\ 子目录)

Move-VMStorage 完成后,外置盘上的实际结构是:

F:\
├── Virtual Hard Disks\
│   ├── RHEL8-Node1\
│   │   └── RHEL8-Node1.vhdx
│   ├── RHEL8-Node2\
│   │   └── RHEL8-Node2.vhdx
│   └── ...
└── Hyper-V\
    ├── Virtual Machines\
    │   └── <VM 的 GUID>\
    │       ├── <VM 的 GUID>.vmcx
    │       ├── <VM 的 GUID>.vmgs
    │       └── <VM 的 GUID>.VMRS
    └── Snapshots\
        └── <快照 GUID>\
            └── <快照 GUID>.avhdx

这正是 D: 上的目录结构镜像——Virtual Hard Disks\<VM名>\<VM名>.vhdx 平铺,加上 Hyper-V\Virtual Machines\<GUID>\*.vmcxHyper-V\Snapshots\<GUID>\*.avhdx 三个独立的顶层位置。

单台验证

Get-VM -Name "RHEL8-Node1" | ForEach-Object {
  [PSCustomObject]@{
    VM  = $_.Name
    VHD = (Get-VMHardDiskDrive -VMName $_.Name).Path
  }
}

期望 VHD = F:\Virtual Hard Disks\RHEL8-Node1\RHEL8-Node1.vhdx

启动一台验证

Start-VM -Name "RHEL8-Node1"
Start-Sleep 30
Get-VM -Name "RHEL8-Node1" | Select-Object Name, State

State 应显示 Running


不用 -Vhds hashtable 的简化版(结构会嵌套一层)

如果不在意中间多一层 Virtual Hard Disks\(即 VHDX 最终路径是 F:\Virtual Hard Disks\<VM名>\Virtual Hard Disks\<VM名>.vhdx),可以用更简单的写法:

Move-VMStorage -VMName "RHEL8-Node1" `
  -DestinationStoragePath "F:\Virtual Hard Disks"

但这不是你想要的镜像 D: 结构。本文主推 -Vhds hashtable 写法。

4.2 批量 Move-VMStorage 脚本(推荐)

前置:先合并 7 台快照。

$vmNames = @(
  "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
  "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7"
)

$externalDrive = "F:"   # ← 你的移动硬盘盘符

foreach ($name in $vmNames) {
  $start = Get-Date
  Write-Host "==== 迁移:$name ====" -ForegroundColor Cyan

  $vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path

  # 没合并好的跳过,避免文件名不匹配报错
  if ($vhdCurrent -notlike "*\$name.vhdx") {
    Write-Warning "$name VHDX 还没合并好($vhdCurrent),跳过——先去 [4.5 节](#sec45) 合并" -WarningAction Continue
    continue
  }

  New-Item -ItemType Directory -Path "$externalDrive\Virtual Hard Disks\$name" -Force | Out-Null

  Move-VMStorage -VMName $name `
    -Vhds @(@{
      SourceFilePath      = $vhdCurrent
      DestinationFilePath = "$externalDrive\Virtual Hard Disks\$name\$name.vhdx"
    }) `
    -VirtualMachinePath "$externalDrive\Hyper-V" `
    -SnapshotFilePath   "$externalDrive\Hyper-V" `
    -ErrorAction Stop

  # Move-VMStorage 每次会自动创建一个 saved state 检查点
  # 删掉它,让 VHDX 变回干净的 .vhdx
  Remove-VMSnapshot -VMName $name -Confirm:$false

  Write-Host "  ✅ 完成($((Get-Date) - $start).ToString('mm\:ss'))" -ForegroundColor Green
}

每台 12 GB VHDX 在 USB 3.0 上约 3-5 分钟;7 台整体约 25-40 分钟。不要中途拔盘

反向:从 F: 迁回 D:(用上面的脚本改四行即可)

正向脚本只改四处就能反向——把 SourceFilePath 的 D: 路径去掉(脚本里自动读)、DestinationFilePath 改 D:、-VirtualMachinePathD:\Hyper-V-SnapshotFilePathD:\Hyper-V

$vmNames = "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
           "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7"

foreach ($name in $vmNames) {
  $start = Get-Date
  Write-Host "==== 反向迁回:$name ====" -ForegroundColor Cyan

  $vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path

  if ($vhdCurrent -notlike "*\$name.vhdx") {
    Write-Warning "$name VHDX 不干净($vhdCurrent),跳过——先合并 saved state" -WarningAction Continue
    continue
  }

  # 关停
  Stop-VM -Name $name -Force -ErrorAction SilentlyContinue

  # D: 顶层建目标子目录
  New-Item -ItemType Directory -Path "D:\Virtual Hard Disks\$name" -Force | Out-Null

  # 反向 Move-VMStorage(参数缩短一层 + 目标路径反过来)
  Move-VMStorage -VMName $name `
    -Vhds @(@{
      SourceFilePath      = $vhdCurrent
      DestinationFilePath = "D:\Virtual Hard Disks\$name\$name.vhdx"
    }) `
    -VirtualMachinePath "D:\Hyper-V" `
    -SnapshotFilePath   "D:\Hyper-V" `
    -ErrorAction Stop

  # 删 Move-VMStorage 自带的 saved state
  Remove-VMSnapshot -VMName $name -Confirm:$false

  Write-Host "  ✅ 完成($((Get-Date) - $start).ToString('mm\:ss')))" -ForegroundColor Green
}

Move-VMStorage 的"Magic 子目录"行为

Move-VMStorage-VirtualMachinePath-SnapshotFilePath 参数会自动追加子目录:

参数值 实际生成的目录
-VirtualMachinePath "F:\Hyper-V" F:\Hyper-V\Virtual Machines\<VM的 GUID>\*.vmcx
-VirtualMachinePath "F:\Hyper-V\Virtual Machines" F:\Hyper-V\Virtual Machines\Virtual Machines\<VM的 GUID>\*.vmcx ← 多一层
-SnapshotFilePath "F:\Hyper-V" F:\Hyper-V\Snapshots\<快照 GUID>\*.avhdx

不要在参数里加 Virtual Machines\Snapshots\ 后缀——这是文档早期版本的一个坑,会导致 F:\Hyper-V\Virtual Machines\Virtual Machines\<GUID>\ 这种嵌套结构。

Move-VMStorage 的 saved state 副作用

每次跑 Move-VMStorage 都会自动创建一个 saved state 检查点(为了搬迁过程的数据安全),导致 VHDX 立刻变成 <VM名>_<GUID>.avhdx

每次 Move-VMStorage 之后必须删 saved state,VHDX 才回到 <VM名>.vhdx

Remove-VMSnapshot -VMName "X" -Confirm:$false

4.3 Hyper-V 管理器方式

Hyper-V 管理器 → 选中 VM → 右侧"操作" → 移动...

步骤 ✅ 选择
1 移动虚拟机的所有数据
2 路径 1(VM 配置):F:\Hyper-V\ ← 不加 Virtual Machines\ 后缀(Move-VMStorage 会自动加)
3 路径 2(快照):F:\Hyper-V\ ← 不加 Snapshots\ 后缀
4 路径 3(VHDX):F:\Virtual Hard Disks\<VM名>\
5 摘要核对 → 完成

GUI 对单台直观,但 7+ 台逐台点效率低,PowerShell 批量脚本更适合。

4.4 (可选)再跑一次 Export-VM 生成便携归档

这一步可选——只为"换机时插盘就能用"加一份 Hyper-V 标准便携包。
你想等换机时再 Import-VM -Register 重建注册(多 1 分钟),可以跳过。

$vmNames = @(
  "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
  "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7"
)
$archiveRoot = "F:\Hyper-V-Archive"   # 与 Move-VM 落地的 F:\Hyper-V\ 平级,互不干扰

New-Item -ItemType Directory -Path $archiveRoot -Force | Out-Null

foreach ($name in $vmNames) {
  $start = Get-Date
  Write-Host "==== 导出便携包:$name ====" -ForegroundColor Cyan
  # 注意:Export-VM 永远从当前 Hyper-V 注册的路径(即 F: 上 Move-VM 后的位置)出发
  Export-VM -Name $name -Path $archiveRoot -ErrorAction Stop
  Write-Host "==== 完成:$name($((Get-Date) - $start).ToString('mm\:ss')))====" -ForegroundColor Green
}

双产物目录对照

📦 类型 📁 路径 ⚡ 作用
Move-VM 落地(活存储) F:\Hyper-V\ + F:\Virtual Hard Disks\ 当前机器 Hyper-V 直接跑
Export-VM 便携包 F:\Hyper-V-Archive\ 换机时复制即导入,无需再重新导出

这一步会再花 25-40 分钟复制一份。要不要做取决于你"对插盘即用的需求有多强"。

4.5 关键前置:合并检查点后再 Move

这一步默认强烈推荐先做——Move-VM 跑之前,所有待迁 VM 应当处于"快照全部合并回主磁盘"的干净状态。
如果 VM 还有活动检查点(.avhdx),Move-VM 仍然能迁,但搬过去的磁盘是"快照态"而不是"主态"——迁移后磁盘文件命名也不直观。

为什么需要合并

Get-VMHardDiskDrive 返回的是 VM 当前活动磁盘的路径——如果 VM 有活动检查点,这个路径会指向 .avhdx(带 GUID 后缀的快照磁盘),而不是 <VM名>.vhdx(主磁盘)。这两种状态的差别:

📊 状态 🛠️ 路径形态 📝 Move-VM 后效果
已合并 D:\Virtual Hard Disks\<VM名>\<VM名>.vhdx 迁移过去是干净的"主磁盘",文件名直观
有快照 D:\Virtual Hard Disks\<VM名>\<VM名>_<GUID>.avhdx 迁移过去是"快照态磁盘",主磁盘 .vhdx 留在 D: 成为孤儿

检查当前快照状态

# 列出所有目标 VM 当前的"活动磁盘路径"
Get-VM | Where-Object Name -in $vmNames |
  Select-Object Name,
    @{N='CurrentDisk';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} |
  Format-Table -AutoSize -Wrap

如果 CurrentDisk 列含 <VM名>_<GUID>.avhdx(带 GUID 后缀的 .avhdx)——需要先合并。

合并检查点

# 直接按 VMName 删——不用 Get-VMSnapshot 枚举,避开 pipeline quirk
foreach ($name in $vmNames) {
  Write-Host "==== $name:合并检查点 ====" -ForegroundColor Yellow
  Remove-VMSnapshot -VMName $name -Confirm:$false
}

PowerShell 提示Get-VMSnapshot -VMName "X" 在某些环境下 pipeline 行为异常——返回其他 VM 的快照或报"通配符模式无效"。最稳的是直接给 Remove-VMSnapshot -VMName X 不带 -Name 参数——-Name 默认通配,删除该 VM 的所有 saved state。

合并是 Hyper-V 后台异步操作,不要立即 Move-VM,先等合并完成。

等待合并完成

# 方法 1:粗略轮询(VM State 会在合并中短暂变为 Merging / Saving)
while ($true) {
  $inProgress = Get-VM | Where-Object State -in "Merging", "Saving", "Stopping"
  if (-not $inProgress) { break }
  Write-Host "等待合并完成:$($inProgress.Name -join ', ')"
  Start-Sleep 30
}
Write-Host "所有合并完成 ✅" -ForegroundColor Green
# 方法 2:看事件日志确认合并完成
Get-WinEvent -LogName "Microsoft-Windows-Hyper-V-VMMS-Admin" -MaxEvents 200 |
  Where-Object { $_.Message -match 'merged|merge' } |
  Select-Object TimeCreated, Message | Format-List

验证合并结果

# 期望:每台 VM 的活动磁盘都回到 "<VM名>.vhdx"(不带 GUID 后缀)
Get-VM | Where-Object Name -in $vmNames |
  Select-Object Name,
    @{N='CurrentDisk';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} |
  Format-Table -AutoSize -Wrap

✅ 看到 CurrentDisk = D:\Virtual Hard Disks\<VM名>\<VM名>.vhdx 即可以进入 4.1 节 / 4.2 节Move-VM

不要跳过这一步直接 Move-VMStorage——Move-VMStorage -Vhds hashtable 要求源/目标文件名完全一致(否则报"源文件名和目标文件名必须匹配")。如果源 VHDX 是 <VM名>_<GUID>.avhdx,目标路径写 <VM名>.vhdx 就会报错。


五、验证

5.1 确认所有路径已迁移到外置盘

Get-VM | Where-Object Name -in $vmNames | ForEach-Object {
  [PSCustomObject]@{
    VM  = $_.Name
    VHD = (Get-VMHardDiskDrive -VMName $_.Name).Path
  }
} | Format-Table -AutoSize

期望

期望内容
Config 全是 F:\Hyper-V\Virtual Machines
VHD 全是 F:\Virtual Hard Disks\<VM名>\<VM名>.vhdx

任一台 VHD 还显示 D::回去对 4.1 节 检查 -DestinationStoragePath 写对,或源 VM 是否真的 Off。

5.2 启动一台 VM 验证

Start-VM -Name "RHEL8-Node1"
Start-Sleep 30
Get-VM -Name "RHEL8-Node1" | Select-Object Name, State

State 应显示 Running。VM 内核对一下:

# Linux VM
ip addr show eth0       # 应有原配置 IP(如 192.168.100.101)
lsblk                  # 磁盘标识不变

# Windows VM
Get-Disk
Get-NetIPAddress

跑通一台再批量启动剩余 VM:

Get-VM -Name $vmNames | Where-Object State -eq "Off" | Start-VM

六、清理源机器内置盘

先验证 第 5 章 全部通过、所有 VM 跑起来都没问题,再删源机器上的旧文件。
这一步决定 D: 上的空间能不能真的腾出来。

# 1. 再次确认:要删的 VM 的 VHDX 已在 F:(不在 D:)
foreach ($name in $vmNames) {
  $vhd = (Get-VMHardDiskDrive -VMName $name).Path
  $onE = $vhd -like "F:\*"
  "  $name  →  VHD 在 F: = $onE   ($vhd)"
}

# 2. 关闭源机器上的所有 VM(防止删注册时报 in use)
Get-VM -Name $vmNames | Stop-VM -Force

# 3. 删除源机器上的 VM 注册(仅删 D: 上的那份,不会影响外置盘上 Move-VM 的注册)
#    注意:因为 Move-VM 在当前机器上"重新注册"了 VM,注册只此一份,路径已是 F:
#    所以此时 Get-VM 看到的"原始 VM 注册"其实就是 F: 上的注册,**不能直接 Remove-VM**
#
#    实际做法:源注册 = F: 上的注册,**你不需要 Remove-VM**。直接跳到删文件即可。
#    Hyper-V 看到 VHDX 在 F:,注册也指向 F:,移除 D: 文件不影响 VM 运行。

# 4. 删除 D: 上的 VM 配置目录
Remove-Item -Recurse -Force "D:\Hyper-V\Virtual Machines\*"

# 5. 删除 D: 上的快照目录
Remove-Item -Recurse -Force "D:\Hyper-V\Snapshots\*"

# 6. 删除 D: 上的 VHDX
foreach ($name in $vmNames) {
  Remove-Item -Recurse -Force "D:\Virtual Hard Disks\$name"
}

验证 D: 已腾出空间

Get-ChildItem "D:\Hyper-V\Virtual Machines" -ErrorAction SilentlyContinue
Get-ChildItem "D:\Virtual Hard Disks" -ErrorAction SilentlyContinue

两条命令都没有输出(或只剩 D:\ISOs\ 不需要迁的目录)= 内置盘已释放。

⚠️ 重要:Move-VM 后源机器 Hyper-V 注册直接指向 F:,不需要 Remove-VM。D: 上残留的配置文件和 VHDX 已经是"垃圾",可以安全删除。
如果发现 Remove-Item 报"文件被占用",那是源机器快照还残留——回 3.3 节 把快照先合并。


七、偶尔换机:到新机器上重建 Hyper-V 注册

Move-VM 落地后的结构不是 Hyper-V 标准 Export 包——它在 F: 上放着"漂亮的分目录"VM 资产,但每台 VM 的 Hyper-V 注册还留在当前机器上。
换到新机器时,必须通过 Import-VM -Register 让新机器"认识"这些 VM。

7.1 把盘带过去 + 固定盘符

  1. 拔下外置盘插到新机器
  2. 确认移动硬盘加载成功
  3. 盘符不是 F: → 见 3.5 节 在新机器手动固定为 F:
  4. 盘符已经是 F: → 直接 7.2

7.2 Import-VM -Register 重建注册

新机器上首次操作:先确认 Hyper-V 角色已启用,然后用 PowerShell(管理员)跑:

$vmsPath = "F:\Hyper-V\Virtual Machines"

# 收齐所有 .vmcx,遍历注册
Get-ChildItem $vmsPath -Directory | ForEach-Object {
  $vmcx = Get-ChildItem $_.FullName -Filter "*.vmcx" | Select-Object -First 1
  if ($vmcx) {
    Import-VM -Path $vmcx.FullName -Register -VirtualMachinePath $vmsPath
    Write-Host "已注册:$($vmcx.BaseName)"
  }
}

⚠️ 上面的 Get-ChildItem -First 1 是简化演示——多台 VM 时按"每台 VM 的 GUID 子目录"配对注册,更精确。

更精确方式:在源机器上跑 Get-VM | Where Name -in $vmNames | Select Id, Name,把每台 VM 的 GUID 记下来带走;换机后按 GUID 找对应 .vmcx

# 源机器:记 GUID
"GUID 列表"
Get-VM | Where-Object Name -in $vmNames | Select-Object Id, Name | Format-Table -AutoSize

# 新机器:按 GUID 找 .vmcx 注册
foreach ($vmId in @("GUID1","GUID2","GUID3")) {
  $vmcx = Get-ChildItem "F:\Hyper-V\Virtual Machines\$vmId" -Filter "$vmId.vmcx" -ErrorAction SilentlyContinue |
           Select-Object -First 1
  if ($vmcx) {
    Import-VM -Path $vmcx.FullName -Register -VirtualMachinePath $vmsPath
  }
}

如果新机器已经在跑同名 VM

# 用新 GUID 导入(避免冲突)
Import-VM -Path $vmcx.FullName -Register -GenerateNewId -VirtualMachinePath $vmsPath

# 或先删同名 VM(如果新机器上同名 VM 不重要)
Get-VM -Name "VM1" | Remove-VM -Force
Import-VM -Path $vmcx.FullName -Register -VirtualMachinePath $vmsPath

7.3 启动验证 + 网络收尾

Start-VM -Name "RHEL8-Node1"
Get-VM -Name "RHEL8-Node1"
# State 应显示 Running

网络收尾(跟 Export-VM 流程同样要做):

🛠️ 现象 📝 处理
"找不到虚拟交换机 InternalSwitch" 新机器 Hyper-V 管理器建同名交换机
VM 网卡 "Identifying..." 持续数十秒 正常:Hyper-V 在重新生成虚拟 MAC
VM 内网络配置丢失 / 上不了网 重建宿主侧 InternalSwitch + NAT(参考本机原 Hyper-V 集群笔记)
7 台 VM 启动后 IP 冲突 / 网关不通 宿主 NAT 没在新机器复刻,按集群笔记复刻

具体的虚拟交换机、NAT、nmcli reinit 等步骤在原 RHEL 8.10 安装笔记的第五章有完整流程,新机器上同样要走一遍。


八、移动硬盘使用注意事项

⚠️ 风险 📝 说明 💡 缓解
活外置化后拔出 = VM 立即丢失 第六章 完成后,本机 VM 全跑在外置盘上;拔盘当机(数据可能损坏) 日常使用盘不要拔;每次拔盘前 Stop-VM -Force 所有 VM
Move-VM 过程中拔出 USB 突然断电,正在写入的 VHDX 可能损坏 Move-VM 期间盘不动
机械硬盘掉盘 老式移动硬盘 USB 取电不足,重负载时可能识别失败 Y 线或独立供电硬盘盒;带盘换机器前先 chkdsk F: /f
不同机器盘符变化 拔插顺序变了,盘符从 F: 变 G: 3.5 节 方案——每台机器手动固定 F:
速度:移动硬盘比内置 NVMe 慢 USB 3.0 HDD 写盘 ~30-80 MB/s,内置 NVMe 可达 3000 MB/s I/O 密集 VM(数据库、备份)仍跑外置盘;性能敏感可改外置 SSD
碎片化 移动硬盘上的大文件长期写入会比较碎 NTFS 移动硬盘定期碎片整理;exFAT 不需要
导出包 / 数据完整性 80-100 GB 数据复制中途可能出错 Move-VM 后做 第五章 验证;另一块盘同步 robocopy /MIR 备份

文件系统选择:exFAT 跨平台好;NTFS 支持稀疏文件、ACL、VHDX 元数据略快。
新出厂移动硬盘通常是 exFAT——不必为了迁 VM 重格式化


九、常见问题排查

❌ 问题 🔍 原因 🛠️ 解决
Move-VM 报 "VM is not in Off state" 还有 VM 没关停 3.1 节;逐台 Stop-VM -Force
Move-VM 报 "Insufficient permissions" PowerShell 没以管理员身份运行 重开 PowerShell(右键 → "以管理员身份运行")
Move-VM 报 "Path not found" 目标子目录 <VM名>\ 没建 3.2 节,先 New-Item
Move-VM 报 "File already exists" 目标路径已有同名文件 删除目标文件再 Move;或换一个目标文件名
Move-VM 后某台 VM 还是 Off 看不到路径变更 -DestinationStoragePath 路径写错(含空格 / 大小写不对) 对照 VM 设置 → 硬盘里的真实 VHD 路径修正
Move-VM 卡住或超时 USB 中途断开 / 移动硬盘 IO 抖动 别动;30 分钟以上无进展可 Ctrl+C 终止重试
启动 VM 报错"虚拟硬盘找不到" 跨机盘符变化 3.5 节;在新机器固定盘符
启动时报"找不到虚拟交换机 XXX" 新机器没建同名虚拟交换机 在新机器 Hyper-V 管理器建同名交换机
Import-VM -Register 报 "VM with same name already exists" 新机器同名 VM 已在注册 7.2 节GenerateNewId 或先删再导入
7 台 VM 启动后 IP 冲突 / 网关不通 原机器 NAT / 虚拟交换机没在新机器复刻 按本机原 Hyper-V 集群笔记复刻 InternalSwitch + 宿主 NAT
Move-VM 后 D: 上的文件删不掉,报"in use" 源机器快照还残留 / VM 还在运行 Stop-VM -Force 全部 → 重试;先合并快照再删
想把已经在 F: 上的 VM 移回 D: 活外置化的"反向操作" 4.2 节 末尾的反向脚本——把 4 个路径参数里的 F: 换成 D: 即可
想"在线 Move 不停 VM" Move-VM 不支持 把 VM 短暂 Off(维护窗口内);或用 Hyper-V 复制(Replica)替代
想"换机器插盘即用,不要 5 分钟 Import" Move-VM 模式做不到"零操作插盘" 4.4 节 加做一次 Export-VM,便携包可零操作导入
Get-VM | Select ConfigurationLocation 返回的路径和真实 .vmcx 位置对不上(如显示 D:\Hyper-V 但实际在 D:\Hyper-V\Virtual Machines\<GUID>\ ConfigurationLocation 在自定义默认 VM 路径后,会显示 Hyper-V 设置中的"声明位置"而不是 .vmcx 真实位置 不要把 ConfigurationLocation 当真实路径用。真实位置用 Get-ChildItem "D:\Hyper-V\Virtual Machines" -Recurse -Filter "*.vmcx" 找;Move-VM 也用 -VirtualMachinePath 参数指定目标,不需要读这个属性
Get-VMHardDiskDrive 返回的是 .avhdx 而不是 .vhdx VM 当前正指向"快照磁盘"(活动检查点未合并) 4.5 节 合并检查点;或脚本里把源路径改为 (Get-VMHardDiskDrive -VMName $name).Path 动态拿
Move-VMAmbiguousParameterSet序列不包含任何匹配元素 Move-VM 设计上只服务"跨主机搬迁"——同主机场景(包括加 -DestinationHost $env:COMPUTERNAME)都会触发问题;前者是参数集挑不出来,后者是 VMMS 兼容性校验的内部异常 改用 Move-VMStorage——专为同主机搬迁设计,自动改写 .vmcx 里的 VHDX 路径引用,没有上述问题。例:Move-VMStorage -VMName "X" -DestinationStoragePath "F:\..."
Move-VMStorage -Vhds 报"源文件名和目标文件名必须匹配" -Vhds hashtable 要求源/目标绝对路径里文件名部分完全一致——例如源 RHEL8-Node1.vhdx 目标也得是 RHEL8-Node1.vhdx,不能改名 先把 VHDX 路径里文件名搞清楚;如果源是 <VM名>_<GUID>.avhdx(快照未合并),先跑 4.5 节 合并成 <VM名>.vhdx 再迁
跑完 Move-VMStorage 后,F:\Hyper-V\Virtual Machines\Virtual Machines\<GUID>\*.vmcx(多一层 Virtual Machines\Virtual Machines\ -VirtualMachinePath 参数不要带 Virtual Machines\ 后缀——Move-VMStorage 会自动追加。Snapshots 同理 参数改成 -VirtualMachinePath "F:\Hyper-V"(不带后缀);快照改成 -SnapshotFilePath "F:\Hyper-V"
跑完 Move-VMStorage 后,VHDX 立刻变成 <VM名>_<GUID>.avhdx(带 GUID 后缀) Move-VMStorage 自动创建一个 saved state 检查点保证搬迁事务安全——副作用是 VHDX 变成快照态 每次跑完 Move-VMStorage 立刻删 saved state:Remove-VMSnapshot -VMName X -Confirm:$false(不带 -Name 参数,让 cmdlet 用默认通配)

十、命令速查

10.1 全流程("开箱即用"模板)

保存为 migrate-vms-to-usb.ps1,按需修改 $vmNames$externalDrive

# ===== 0. 先看清本机有哪些 VM(决定 $vmNames 怎么写) =====
Get-VM | Format-Table Name, State,
  @{N='VHDPath';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize -Wrap

# ===== 1. 配置 =====
# $vmNames 两种写法选一种:
#   方法 A:手写名单(适合"我已经知道名字")
#   方法 B:按通配符自动提取(适合"按名字模式批量挑")

# 方法 A 写法:
# $vmNames = @("RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
#              "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7")

# 方法 B 写法(按名字模式自动挑):
$vmNames = (Get-VM | Where-Object { $_.Name -like "RHEL8-*" }).Name

$externalDrive = "F:"                                  # ← 移动硬盘盘符(见 3.5 节)

# ===== 1.5 确认 $vmNames 与 Get-VM 一致 =====
$matched = Get-VM -Name $vmNames
""
"匹配数:$($matched.Count) / $($vmNames.Count)"
if ($matched.Count -ne $vmNames.Count) {
  Write-Warning "数量不一致!请回 3.4 节检查写错的 VM 名" -WarningAction Continue
  return
}

# ===== 2. 关停所有目标 VM =====
Get-VM -Name $vmNames | Where-Object State -eq "Running" | Stop-VM -Force

# ===== 3. 合并所有快照([4.5 节](#sec45))=====
#    必备前置:VHDX 必须是 "<VM名>.vhdx" 无 GUID 后缀
foreach ($name in $vmNames) {
  Remove-VMSnapshot -VMName $name -Confirm:$false
}
Write-Host "快照合并已发起,等合并完成再继续(看 Hyper-V 管理器)"
Write-Host "Press Enter when ready..." -ForegroundColor Yellow
Read-Host

# ===== 4. Move-VMStorage 批量:三参数分开指定 =====
foreach ($name in $vmNames) {
  $start = Get-Date
  Write-Host "==== 迁移:$name ====" -ForegroundColor Cyan

  $vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path

  # 没合并好的跳过,避免 Move-VMStorage -Vhds 报"文件名不匹配"
  if ($vhdCurrent -notlike "*\$name.vhdx") {
    Write-Warning "$name VHDX 还没合并好($vhdCurrent),跳过——回 [4.5 节](#sec45)" -WarningAction Continue
    continue
  }

  # F: 顶层建目标子目录
  New-Item -ItemType Directory -Path "$externalDrive\Virtual Hard Disks\$name" -Force | Out-Null

  Move-VMStorage -VMName $name `
    -Vhds @(@{
      SourceFilePath      = $vhdCurrent
      DestinationFilePath = "$externalDrive\Virtual Hard Disks\$name\$name.vhdx"
    }) `
    -VirtualMachinePath "$externalDrive\Hyper-V" `
    -SnapshotFilePath   "$externalDrive\Hyper-V" `
    -ErrorAction Stop

  # Move-VMStorage 自动创建 saved state,删掉它让 VHDX 回到干净的 .vhdx
  Remove-VMSnapshot -VMName $name -Confirm:$false

  Write-Host "  ✅ 完成($((Get-Date) - $start).ToString('mm\:ss'))" -ForegroundColor Green
}

# ===== 5. 验证所有 VM 路径都在 F: =====
Get-VM | Where-Object Name -in $vmNames | ForEach-Object {
  [PSCustomObject]@{
    VM  = $_.Name
    VHD = (Get-VMHardDiskDrive -VMName $_.Name).Path
  }
} | Format-Table -AutoSize

# ===== 6. 启动一台验证 =====
Start-VM -Name $vmNames[0]
Start-Sleep 30
Get-VM -Name $vmNames[0] | Select-Object Name, State

Write-Host "`n✅ Move-VMStorage 完成。下一阶段:'" -ForegroundColor Green -NoNewline
Write-Host "第六章清理源盘" -ForegroundColor Yellow -NoNewline
Write-Host "' 或 '" -ForegroundColor Green -NoNewline
Write-Host "第七章换机" -ForegroundColor Yellow -NoNewline
Write-Host "'。" -ForegroundColor Green

10.2 反向 Move-VMStorage:从 F: 移回 D: (如需)

$vmNames = @("VM1", "VM2", "VM3")

foreach ($name in $vmNames) {
  $vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path
  Get-VM -Name $name | Stop-VM -Force

  Move-VMStorage -VMName $name `
    -Vhds @(@{
      SourceFilePath      = $vhdCurrent
      DestinationFilePath = "D:\Virtual Hard Disks\$name\$name.vhdx"
    }) `
    -VirtualMachinePath "D:\Hyper-V" `
    -SnapshotFilePath   "D:\Hyper-V"
}

10.3 新机器重建注册

$vmsPath = "F:\Hyper-V\Virtual Machines"

# 收齐所有 .vmcx,遍历注册
Get-ChildItem $vmsPath -Directory | ForEach-Object {
  $vmcx = Get-ChildItem $_.FullName -Filter "*.vmcx" | Select-Object -First 1
  if ($vmcx) {
    Import-VM -Path $vmcx.FullName -Register -VirtualMachinePath $vmsPath
    Write-Host "已注册:$($vmcx.BaseName)"
  }
}

10.4 关键点速查

🔢 阶段 ✅ 必做 🚫 不做
准备 全部 VM Off;建 F: 镜像目录结构;规划好盘符跨机器一致性 直接 Move
Move 4.5 合并快照使 VHDX 变 .vhdx;用 Move-VMStorage -Vhds @{...} hashtable + -VirtualMachinePath + -SnapshotFilePath 三参数组合精确指定路径 想改 VHDX 文件名(Move-VMStorage 不允许改名,会报"源/目标文件名必须匹配")
验证 Get-VM 输出路径已全在 F:;启动一台 VM 跑通再批量 不验证直接删 D: 旧文件
清源盘 验证全过后再删Remove-Item -Recurse -Force 在 VM 还 Running 时删 D: 文件
换机 在新机器固定盘符为 F:;用 Import-VM -Register 重建;如冲突用 -GenerateNewId 假设盘符自动一致
拔盘 拔之前 Stop-VM -Force 全部 拔盘期间 VM 在 Running

10.5 一次性脚本:阶段 0 到阶段 3

把整个迁移流程压成 4 个顺序执行的代码块。别跳步骤——先跑阶段 0 确认环境 OK,再单台试阶段 1,验证后跑阶段 2 批量,最后阶段 3 清理。

每跑完一段把输出贴回来确认,再跑下一段。

阶段 0:环境预检

# A) 看 VM 列表 + 当前 VHD 状态
Get-VM | Where-Object Name -like "RHEL8-Node*" |
  Format-Table Name, State,
    @{N='VHD';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize -Wrap

# B) 合并所有检查点(必备前置:VHDX 必须变 `.vhdx` 无 GUID 后缀)
foreach ($name in "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
                 "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7") {
  Remove-VMSnapshot -VMName $name -Confirm:$false
}
# 等合并完成(看 Hyper-V 管理器或事件日志)——5-15 分钟

# C) 在 F: 上建好顶层目录(脚本里后面会自动建 <VM名> 子目录)
$externalDrive = "F:"
New-Item -ItemType Directory -Path "$externalDrive\Virtual Hard Disks" -Force | Out-Null

# D) 关停所有目标 VM
$vmNames = (Get-VM | Where-Object { $_.Name -like "RHEL8-Node*" }).Name
Get-VM -Name $vmNames | Where-Object State -eq "Running" | Stop-VM -Force

"✅ 阶段 0 完成"

阶段 1:单台测试(先迁 RHEL8-Node1 一台验证链路)

$testName = "RHEL8-Node1"

# 当前 VHDX 在 D: 上的真实路径(避免硬编码)
$vhdCurrent = (Get-VMHardDiskDrive -VMName $testName).Path
Write-Host "源: $vhdCurrent"

# F: 顶层建目标子目录
New-Item -ItemType Directory -Path "F:\Virtual Hard Disks\$testName" -Force | Out-Null

# 三参数分开指定:VHDX 平铺 + 配置/快照走标准位置
# 参数**不要加** Virtual Machines\ / Snapshots\ 后缀——Move-VMStorage 会自动追加
Move-VMStorage -VMName $testName `
  -Vhds @(@{
    SourceFilePath      = $vhdCurrent
    DestinationFilePath = "F:\Virtual Hard Disks\$testName\$testName.vhdx"
  }) `
  -VirtualMachinePath "F:\Hyper-V" `
  -SnapshotFilePath   "F:\Hyper-V"

# Move-VMStorage 自动创建 saved state,删掉它让 VHDX 回到干净的 .vhdx
Remove-VMSnapshot -VMName $testName -Confirm:$false

# 验证:路径已迁到 F:
Get-VM -Name $testName | Format-Table Name, State,
  @{N='VHD';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize

# 启动验证
Start-VM -Name $testName
Start-Sleep 30
Get-VM -Name $testName | Select Name, State

期望VHD = F:\Virtual Hard Disks\RHEL8-Node1\RHEL8-Node1.vhdx,State 显示 Running

阶段 2:批量迁移(单台 OK 后跑)

$vmNames = "RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
           "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7"

foreach ($name in $vmNames) {
  $start = Get-Date
  Write-Host "==== $name ====" -ForegroundColor Cyan

  $vhdCurrent = (Get-VMHardDiskDrive -VMName $name).Path

  # 没合并好的跳过——避免 Move-VMStorage -Vhds 报"文件名不匹配"
  if ($vhdCurrent -notlike "*\$name.vhdx") {
    Write-Warning "$name VHDX 还没合并好($vhdCurrent),跳过——回 4.5" -WarningAction Continue
    continue
  }

  New-Item -ItemType Directory -Path "F:\Virtual Hard Disks\$name" -Force | Out-Null

  Move-VMStorage -VMName $name `
    -Vhds @(@{
      SourceFilePath      = $vhdCurrent
      DestinationFilePath = "F:\Virtual Hard Disks\$name\$name.vhdx"
    }) `
    -VirtualMachinePath "F:\Hyper-V" `
    -SnapshotFilePath   "F:\Hyper-V" `
    -ErrorAction Stop

  # Move-VMStorage 自动创建 saved state,删掉它让 VHDX 回到干净的 .vhdx
  Remove-VMSnapshot -VMName $name -Confirm:$false

  Write-Host "  ✅ 完成($((Get-Date) - $start).ToString('mm\:ss'))" -ForegroundColor Green
}

# 全部验证
Get-VM | Where-Object Name -like "RHEL8-Node*" |
  Format-Table Name, State,
    @{N='VHD';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize

阶段 3:清理 D: 上的旧文件(全部 OK 后跑)

# 先确认 7 台 VHD 都在 F:
$inF = Get-VM | Where-Object Name -like "RHEL8-Node*" |
  Where-Object { (Get-VMHardDiskDrive -VMName $_.Name).Path -like "F:\*" }
"在 F: 的 VM 数量: $($inF.Count) / 7"
if ($inF.Count -ne 7) {
  Write-Warning "还没全部迁完,先别清 D:" -WarningAction Continue
  return
}

# 全 OK,清 D:
Remove-Item -Recurse -Force "D:\Hyper-V\Virtual Machines\*"
Remove-Item -Recurse -Force "D:\Hyper-V\Snapshots\*" -ErrorAction SilentlyContinue
foreach ($name in "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
                 "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7") {
  Remove-Item -Recurse -Force "D:\Virtual Hard Disks\$name"
}

"D: 清理完成,$($inF.Count) 台 VM 已完全迁到 F:"

进度回报点

每跑完一个阶段后,把这段输出贴回来确认状态:

Get-VM | Where-Object Name -like "RHEL8-Node*" |
  Format-Table Name, State,
    @{N='VHD';E={(Get-VMHardDiskDrive -VMName $_.Name).Path}} -AutoSize -Wrap

十一、总结

🔢 步骤 🛠️ 操作 📌 关键点
1 关停目标 VM Stop-VM -Force 全部目标 VM
2 在 F: 上建镜像目录 F:\Hyper-V\Virtual Machines\ / Snapshots\ / Virtual Hard Disks\ + 每台 <VM名>\
3 Move-VM 逐台迁 -DestinationHost $env:COMPUTERNAME -IncludeStorage -DestinationStoragePath 搬 VHDX,再 Copy 配置 + Remove-VM + Import-VM -Register 重建
4 验证路径全在 F: Get-VMHardDiskDrive 输出对照检查
5 启动一台 VM 跑通 确认 IP / 网关 / 磁盘
6 删除 D: 上的旧文件 验证全过后再删;不删注册(Move-VM 已重指)
7 (可选)Export-VM 加做便携包 换机时插盘即用,省去 Import-VM 步骤
8 (偶尔)换机 拔盘到新机器 → 固定盘符 → Import-VM -Register

核心要点Move-VM 是 Hyper-V 官方"在主机内搬迁 VM"命令,配合 -DestinationHost $env:COMPUTERNAME 绕过单主机场景的参数集歧义;VM 配置目录通过 Copy + Remove/Import 的组合搬移到新位置。
主风险点:Move 期间不要拔盘跨机器前要固定盘符删除 D: 旧文件前先验证 F: 跑通


附录:本文工作示例速查(7 台 RHEL8 节点)

仅给"恰好是 7 台 RHEL8 集群节点"的读者参考。VM 场景不一样时跳过本节。

🖥️ VM 名 🎭 角色(按 RHEL 8.10 集群笔记规划) 🌐 原 IP
RHEL8-Node1 Tomcat-1 192.168.100.101
RHEL8-Node2 Tomcat-2 192.168.100.102
RHEL8-Node3 MongoDB-1 192.168.100.103
RHEL8-Node4 MongoDB-2 192.168.100.104
RHEL8-Node5 MongoDB-3 192.168.100.105
RHEL8-Node6 Nginx-1 192.168.100.106
RHEL8-Node7 Nginx-2 192.168.100.107

代入本文主线的具体参数

$vmNames = @(
  "RHEL8-Node1","RHEL8-Node2","RHEL8-Node3","RHEL8-Node4",
  "RHEL8-Node5","RHEL8-Node6","RHEL8-Node7"
)
$externalDrive = "F:"   # 你的移动硬盘盘符

7 台 RHEL8 节点的特殊注意事项

  • VHDX 体量:约 80-100 GB(每台 ~12 GB VHDX)
  • 快照:通常已清理过(克隆完成后),如有保留无影响
  • 网络:源机器的 InternalSwitch + 宿主 NAT(192.168.100.1 网关)需在每台新机器复刻——只跟外置盘走的是 VM,宿主侧网络配置(虚拟交换机、NAT)不跟
  • Red Hat 订阅:Developer Subscription 支持 16 台机器,每台 VM 独立 system identity。换机后用同一账号即可,subscription-manager identity 在 VM 内能验证

更新时间:2026-07-31
适用环境:Windows 11 / 10 + Hyper-V;移动硬盘 NTFS 或 exFAT 均可
工作示例:7 台 RHEL 8.10 集群节点(RHEL8-Node1 ~ RHEL8-Node7
主推方案:Move-VM + 路径自定义(<VM名>\<VM名>.vhdx

posted @ 2026-08-18 10:47  RK5123153  阅读(5)  评论(0)    收藏  举报