Hyper-V checkpointはWhatIfだけで完了にせず、一意なVMId、既存checkpoint ID集合、checkpoint policy、保存volumeのcapacity evidenceを固定してから実作成します。作成後は新規IDがexactly oneであることを確認し、削除・mergeは別承認とownership再bindでのみ実行します。checkpointの自動applyは行いません。
VMId・既存ID・capacity/policyを保存する
VM名から一件へ絞った後はVMIdを主identityにします。既存snapshot ID、CheckpointType、AutomaticCheckpointsEnabled、各VHD pathとvolume free spaceをstateへCreateNewで保存します。capacity不足やproduction policy不明なら停止します。
$ErrorActionPreference='Stop'
$vmName='APP-VM01'
$vms=@(Get-VM -Name $vmName -ErrorAction Stop)
if($vms.Count -ne 1){throw "VMを一意にできません: $($vms.Count)"}
$vm=$vms[0];$vmId=[guid]$vm.Id
$beforeSnapshots=@(Get-VMSnapshot -VM $vm -ErrorAction Stop)
$beforeIds=@($beforeSnapshots.Id|ForEach-Object{$_.ToString()}|Sort-Object)
$snapshotName="ITtrip-$((Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ'))-$([guid]::NewGuid().ToString('N').Substring(0,8))"
if(@($beforeSnapshots|Where-Object Name -eq $snapshotName).Count){throw '同名checkpointが既にあります。'}
$diskEvidence=@(Get-VMHardDiskDrive -VM $vm -ErrorAction Stop|ForEach-Object{
$item=Get-Item -LiteralPath $_.Path -ErrorAction Stop
$volume=Get-Volume -FilePath $item.FullName -ErrorAction Stop
[pscustomobject]@{Path=$item.FullName;Length=$item.Length;Volume=$volume.Path;SizeRemaining=$volume.SizeRemaining}
})
$operationId=[guid]::NewGuid().ToString('D')
$stateDir='C:\ProgramData\ITtrip\HyperVCheckpoint';$null=New-Item -ItemType Directory -Path $stateDir -Force -ErrorAction Stop
$statePath=Join-Path $stateDir "$operationId.json"
$state=[ordered]@{OperationId=$operationId;Stage='Planned';VMName=$vm.Name;VMId=$vmId.ToString();SnapshotName=$snapshotName;BeforeIds=$beforeIds;CheckpointType=[string]$vm.CheckpointType;AutomaticCheckpointsEnabled=[bool]$vm.AutomaticCheckpointsEnabled;DiskEvidence=$diskEvidence;CreatedUtc=(Get-Date).ToUniversalTime().ToString('o')}
$bytes=[Text.Encoding]::UTF8.GetBytes(($state|ConvertTo-Json -Depth 8));$fs=[IO.File]::Open($statePath,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None);try{$fs.Write($bytes,0,$bytes.Length);$fs.Flush($true)}finally{$fs.Dispose()}
[pscustomobject]@{StatePath=$statePath;VMId=$state.VMId;ExistingCheckpointIds=$beforeIds;Policy="$($state.CheckpointType)/Auto=$($state.AutomaticCheckpointsEnabled)";Capacity=$diskEvidence}
明示承認後に実Checkpoint-VMを1回実行する
承認直前にVMId・name・policyと既存ID集合を再確認します。WhatIfではなく実Checkpoint-VMを一回だけ実行し、差分IDが一件、unique name、VMId、CreationTimeがapproval以後であることを必須にします。
$approvalUtc=(Get-Date).ToUniversalTime()
$token="CREATE-CHECKPOINT VMID=$($state.VMId) NAME=$($state.SnapshotName) OP=$($state.OperationId)"
if((Read-Host "capacity/policyを確認し、実作成する場合だけ $token を入力") -ne $token){throw '中止しました。'}
$rebound=Get-VM -Id ([guid]$state.VMId) -ErrorAction Stop
if($rebound.Name -cne $state.VMName -or [string]$rebound.CheckpointType -cne $state.CheckpointType){throw 'VM identity/policyが変わりました。'}
$nowIds=@(Get-VMSnapshot -VM $rebound -ErrorAction Stop|ForEach-Object{$_.Id.ToString()}|Sort-Object)
if(($nowIds -join ',') -cne (@($state.BeforeIds|Sort-Object) -join ',')){throw '承認前にcheckpoint集合が変わりました。'}
Checkpoint-VM -VM $rebound -SnapshotName ([string]$state.SnapshotName) -ErrorAction Stop
$after=@(Get-VMSnapshot -VM $rebound -ErrorAction Stop)
$new=@($after|Where-Object{$_.Id.ToString() -notin @($state.BeforeIds)})
if($new.Count -ne 1){throw "新規checkpointが1件ではありません: $($new.Count)"}
$owned=$new[0]
if($owned.Name -cne $state.SnapshotName -or [guid]$owned.VMId -ne [guid]$state.VMId -or $owned.CreationTime.ToUniversalTime() -lt $approvalUtc){throw '作成checkpointのName/VMId/timeが一致しません。'}
$state.Stage='Created';$state.OwnedSnapshotId=$owned.Id.ToString();$state.ApprovedUtc=$approvalUtc.ToString('o');$state|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $statePath -Encoding UTF8 -ErrorAction Stop
[pscustomobject]@{CreatedId=$state.OwnedSnapshotId;Name=$owned.Name;VMId=$owned.VMId;CreationTime=$owned.CreationTime;Exact=$true}
stateからowned checkpointを再検証する
別sessionでもstateのOwnedSnapshotId・SnapshotName・VMId・ApprovedUtcを全て照合します。Get-VMSnapshotの一覧順やnameだけではownershipとみなしません。Apply-VMSnapshotはこのworkflowに含めません。
$statePath='PASTE_STATE_PATH'
$state=Get-Content -LiteralPath $statePath -Raw -ErrorAction Stop|ConvertFrom-Json
if($state.Stage -ne 'Created'){throw 'verify対象stageではありません。'}
$vm=Get-VM -Id ([guid]$state.VMId) -ErrorAction Stop
$owned=@(Get-VMSnapshot -VM $vm -ErrorAction Stop|Where-Object{$_.Id.ToString() -eq $state.OwnedSnapshotId})
if($owned.Count -ne 1 -or $owned[0].Name -cne $state.SnapshotName -or [guid]$owned[0].VMId -ne [guid]$state.VMId -or $owned[0].CreationTime.ToUniversalTime() -lt ([datetime]$state.ApprovedUtc).ToUniversalTime()){throw 'owned checkpointをexact verifyできません。'}
[pscustomobject]@{Status='CheckpointVerified';SnapshotId=$owned[0].Id;Name=$owned[0].Name;VMId=$owned[0].VMId;ApplyPerformed=$false}
別承認でowned checkpointだけを削除・mergeする
削除直前にID・name・VMIdを再bindし、Remove-VMSnapshot後は最大5分bounded readでID消失を確認します。不一致やtimeoutでは他snapshotを削除せず、VM停止やcheckpoint applyも自動実行しません。
$removeToken="REMOVE-OWNED-CHECKPOINT ID=$($state.OwnedSnapshotId) VMID=$($state.VMId) OP=$($state.OperationId)"
if((Read-Host "別承認で削除・mergeする場合だけ $removeToken を入力。保持するならEnter") -eq $removeToken){
$vm=Get-VM -Id ([guid]$state.VMId) -ErrorAction Stop
$owned=@(Get-VMSnapshot -VM $vm -ErrorAction Stop|Where-Object{$_.Id.ToString() -eq $state.OwnedSnapshotId})
if($owned.Count -ne 1 -or $owned[0].Name -cne $state.SnapshotName -or [guid]$owned[0].VMId -ne [guid]$state.VMId){throw 'remove直前ownershipが一致しません。'}
Remove-VMSnapshot -VMSnapshot $owned[0] -Confirm:$false -ErrorAction Stop
$remaining=$true
for($i=0;$i -lt 60;$i++){if(@(Get-VMSnapshot -VM $vm -ErrorAction Stop|Where-Object{$_.Id.ToString() -eq $state.OwnedSnapshotId}).Count -eq 0){$remaining=$false;break};Start-Sleep -Seconds 5}
if($remaining){throw '削除・merge完了を確認できません。VMを停止・applyせずHyper-V eventを確認します。'}
$state.Stage='RemovedVerified';$state|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $statePath -Encoding UTF8 -ErrorAction Stop
[pscustomobject]@{Status='OwnedCheckpointRemoved';SnapshotId=$state.OwnedSnapshotId;ApplyPerformed=$false;Exact=$true}
}
受入条件
既存snapshot集合が承認前に変わるfixtureでは作成前に停止します。clean runではnew ID一件だけがname/time/VMIdに一致し、任意のcleanupでは同じIDだけが消失します。
checkpointとbackupを区別する
checkpointは同一storage上の短期rollback aidで、独立backupではありません。merge中のI/O、free space、replication、guest consistency、production checkpoint policyを確認し、長期保持しないでください。

コメント