2014-09-22

Hyper-V VM Backup 方式, Replica 的替代方案 (自行撰寫 PowerShell 程式)

環境:
 1. 唯一一台 Hyper-V, 無法做複寫或叢集
 2. 只有一座 Hyper-V 叢集, 沒有其他 Hyper-V 主機

描述:
 上述狀況都無法進行 Hyper-V 複寫來作為備份策略
 如果用 Export-VM 的話, 若 VM 的 vhd 很大則會佔用大量 Disk I/O

解決方式:
 自行撰寫 PowerShell 程式, 利用 CheckPoint 功能土砲差異化備份 (與 Replica 雷同)

預設條件:
 VM 本身除了備份 Powershell 所建立的 CheckPoint 外不可以有其他 CheckPoint
 註: Production 環境的 VM 本來就不該有 CheckPoint, 會影響運作效率

程式運作邏輯:
 1. 建立 CheckPoint 使 VM 原始 vhd 停止寫入, 新的變更都會寫在 CheckPoint 所建立的差異磁碟 avhd
  且該 CheckPoint 會將當時的記憶體寫入成檔案, 等於 VM Pause
 2. 將上述 CheckPoint 資料複製出來作為備份, 未來就還原該 CheckPoint, 達到備份還原目的

可達到的目標:
 1. 可進行 VM 線上差異化備份, 不用每次備份都 Export, 佔用大量 I/O
 2. 可以在允許 Down Time 較久的前提下手動做不停機 Migration
  與 Export 後再 Import 是一樣的道理, 只是使用的是差異化磁碟

歷史:
 2014.09.24 加入備份檔案檢查機制
 2014.10.25 1. 程式開始時先檢查備份路徑如果不能存取就結束
         (先嘗試建立, 不能建立就是失敗, 目標如果是網路磁碟機, 可能連線失敗)
        2. 程式開始時先檢查指定的 VM 如果不存在就結束

程式運作流程:

 初始化:
    1. 檢查 VM 任何 CheckPoint, 沒有就可以進行初始備份
    2. Copy VM 的 xml 檔
    3. 建立 CheckPoint (停止寫入原始 vhd 檔)
    4. 複製 VM 原始 vhd 檔
    5. 產生 CheckPoint 版本檢查檔 (純文字放在備份路徑中, 用以日後判斷差異磁碟版本)
    6. 等待下次備份

 備份:
    1. 檢查是否只有一個 CheckPoint
    2. 檢查該版本是否與 CheckPoint 版本檢查檔中的紀錄相同, 相同就可以進行備份
    3. Copy VM 的 xml 檔
    4. 建立 CheckPoint (停止寫入前一次 CheckPoint Snapshot 中的 vhd 檔)
    5. 複製前一次 CheckPoint 的 差異磁碟 avhd
    6. 複製前一次 CheckPoint 的 Snapshot (xml + bin / vsv)
    7. 產生 CheckPoint 版本檢查檔 (純文字放在備份路徑中, 用以日後判斷差異磁碟版本)
    8. 刪除前一次 CheckPoint (使 Hyper-V 自動合併, 避免積存太多 CheckPoint 影響效能)
    9. 檢查備份數量, 如果超過所設定的備份數量, 就從最舊的版本開始進行 vhd 合併
  10. 建立備份歷史檔, 記載備份檔案清單、檔案大小、最後修改日期
  11. 等待下次備份

 還原:
    1. 列出備份資料夾中現有 CheckPoint 版本供選擇
    2. 將原始 vhd 檔複製到  Merge 資料夾中
    3. 從最舊的 CheckPoint 版本開始, 至所選擇 CheckPoint 版本的 "前一個版本" 為止
     將每一個版本所備份的差異化磁碟 avhd 檔複製到  Merge 資料夾中
    4. 將上述 avhd 與原始 vhd 依照版本順序 Merge
    5. 將所選擇 CheckPoint 版本的差異化磁碟 avhd 檔複製到  Merge 資料夾中 (不可以 Merge)
    6. 將所選擇 CheckPoint 版本的 VM xml 複製到 Merge 資料夾中
    7. 將所選擇 CheckPoint 版本的 Snapshot複製到 Merge 資料夾中
    8. 完成, 至 Hyper-V 中 Import 上述 Merge 資料夾
    9. Import 以右鍵點選 CheckPoint 並選擇 Apply 後再開機
  10. 將該 CheckPoint 刪除, 此時會發現 CheckPoint 仍然存在且再次刪除時會發生錯誤
  11. 手動至該還原 VM 的資料夾中將 Snapshot 資料夾中的檔案全部刪除
  12. 重新整理後已沒有任何存在的檢查點, 並且 VM 正以所選擇的 CheckPoint 版本運作中
  13. 重建備份歷史檔, 重新計算原始 vhd 檔案大小、最後修改日期, 刪除已合併的 CheckPoint 歷史資料
  14. 繼續使用 VM, 並重新進行 Backup 策略

程式共兩支:

 Hyper-V VM Backup.ps1

    1. 用來進行備份, 需要帶入 4 個參數:
     -VM [VM名稱]
     -Path [備份目標路徑]
     -RV [備份保留版本數, 數字, 未輸入則預設為 10]
     -CI [CheckPoint 檢查名稱, 如有空格前後要加引號, 未輸入預設為 'Backup Circle By Dino9021']

    2. 用排程執行就好了, 目前沒有撰寫背景等待執行的功能
    *. 執行語法範例:
     "C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe"
       -command ". 'D:\Hyper-V VM Backup.ps1' -VM '*' -Path '*' -RV '*' -CI '*';"

 Hyper-V VM Restore.ps1

    1. 用來還原, 需要帶入 2 個參數:
     -Path [備份所在路徑, 未輸入的話程式執行時會要求輸入]
     -CI [CheckPoint 檢查名稱, 如有空格前後要加引號, 未輸入預設為 'Backup Circle By Dino9021']

已知問題:
   1. 還原後移除 CheckPoint 要按重新整理才會從 CheckPoint 列表中清除 (應該算 Hyper-V Bug?)
   2. 除了本程式建立的 CheckPoint 之外不可以有其他的 CheckPoint
    要想辦法寫出備份所有的 CheckPoint 也不是不行, 但是很麻煩
    Production 環境的 VM 本來就不該有一堆 CheckPoint
    所以目前並不打算進行這方面的研究

以下為程式碼:

 Hyper-V VM Backup.ps1

===== 程式開始 =====

Param(
[String]$VM,
[String]$Path,
[int]$RV,
[String]$CI
);

$TestRun = 0

$ScriptName = $MyInvocation.MyCommand.Name
if ($RV -eq '') {
$RV = 10;
};
if ($CI -eq '') {
$CI = 'Backup Circle By Dino9021';
};
if ( ($VM -eq '') -or ($Path -eq '') ) {
write-output "";
write-output "Parameter missing!";
write-output "";
write-output "Use: $ScriptName -VM [VM Name] -Path [Backup Path] -RV [Backup Remain Versions] -CI [CheckPoint Identification]";
write-output "";
exit;
};
$DateTimeString = Get-Date -format yyyyMMdd_HHmmss

$ProcessVMName = $VM
$TargetPath = $Path
$RemainVersion = $RV
$CheckPointIdentification = $CI
$NewCheckPointName = "$CheckPointIdentification $DateTimeString"

if (!(Test-Path $TargetPath)) {New-Item -ItemType directory -Path $TargetPath | Out-Null;}
if (!(Test-Path $TargetPath)) {
$TranscriptLog = ($ProcessVMName + "_" + $DateTimeString + ".log")
start-transcript -path $TranscriptLog
write-output ""
write-output "[Backup Failed]"
write-output ""
write-output ("Cannot create target path: """ + $TargetPath + """")
write-output ""
stop-transcript
exit;
};
if (!(Get-VM –VMName $ProcessVMName)) {
$TranscriptLog = ($ProcessVMName + "_" + $DateTimeString + ".log")
start-transcript -path $TranscriptLog
write-output ""
write-output "[Backup Failed]"
write-output ""
write-output ("VM """ + $ProcessVMName + """ does not exists")
write-output ""
stop-transcript
exit;
};

$NowParentshotName = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotName"
if ($NowParentshotName -ne $Null) {
$TranscriptLog = "$TargetPath\$NowParentshotName.log"
[String]$HistoryLog = $NowParentshotName + "`n"
} else {
$TranscriptLog = "$TargetPath\Initial.log"
[String]$HistoryLog = "Initial`n"
};
start-transcript -path $TranscriptLog

$HistoryFileName = "$TargetPath\History.log"

Function CompareFile {

Param(
[String]$FileOne,
[String]$FileTwo
);

[int]$Return = 9021;

if (!(Test-Path $FileOne)) {$Return = 2;}
if (!(Test-Path $FileTwo)) {$Return = 3;}

if ($Return -eq 9021) {
$FileOneLength = Get-ItemProperty -Path $FileOne | Select -ExpandProperty "Length"
$FileTwoLength = Get-ItemProperty -Path $FileTwo | Select -ExpandProperty "Length"

$FileOneLastWriteTime = Get-ItemProperty -Path $FileOne | Select -ExpandProperty "LastWriteTime"
$FileTwoLastWriteTime = Get-ItemProperty -Path $FileTwo | Select -ExpandProperty "LastWriteTime"

$FileOneLastWriteTime = $FileOneLastWriteTime.ToString("yyyyMMdd_HHmmss")
$FileTwoLastWriteTime = $FileTwoLastWriteTime.ToString("yyyyMMdd_HHmmss")

$FileOneLastWriteTime = $FileOneLastWriteTime -Replace "`n",""
$FileTwoLastWriteTime = $FileOneLastWriteTime -Replace "`n",""
};

if ( ($FileOneLength -ne $FileTwoLength) -or ($FileOneLastWriteTime -ne $FileTwoLastWriteTime) ) {
$Return = 0;
};

if ($Return -eq 9021) {
$Return = 1;
};

$ReturnMessage = ""
if ($Return -eq 0) {
$ReturnMessage = $ReturnMessage + "===> Failed. $FileOne and $FileTwo Size and LastWriteTime are not equal.`n"
$ReturnMessage = $ReturnMessage + "===> $FileOne`n"
$ReturnMessage = $ReturnMessage + "===>   Size: $FileOneLength`n"
$ReturnMessage = $ReturnMessage + "===>   LastWriteTime: $FileOneLastWriteTime`n"
$ReturnMessage = $ReturnMessage + "===> $FileTwo`n"
$ReturnMessage = $ReturnMessage + "===>   Size: $FileTwoLength`n"
$ReturnMessage = $ReturnMessage + "===>   LastWriteTime: $FileTwoLastWriteTime"
} elseif ($Return -eq 2) {
$ReturnMessage = $ReturnMessage + "===> Failed. The following file are missing.`n"
$ReturnMessage = $ReturnMessage + "===> $FileOne"
} elseif ($Return -eq 3) {
$ReturnMessage = $ReturnMessage + "===> Failed. The following file are missing.`n"
$ReturnMessage = $ReturnMessage + "===> $FileOne"
} elseif ($Return -eq 1) {
$ReturnMessage = $ReturnMessage + "$FileTwo#@#Dino9021#@#$FileTwoLength#@#Dino9021#@#$FileTwoLastWriteTime"
};

return $Return,$ReturnMessage;
};

Function BackupFailedDelete {
Param(
[String]$ProcessVMName,
[String]$NewCheckPointName,
[String]$TargetPath,
[String]$LastProcessName
);

write-output ""
write-output "[Backup Failed]"
write-output ""

$NowParentshotName = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotName"
if ( ($NowParentshotName -ne $Null) -and ($NowParentshotName -eq $NewCheckPointName) ) {
write-output 'Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotId" | Get-VMSnapshot | Remove-VMSnapshot'
Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotId" | Get-VMSnapshot | Remove-VMSnapshot
};

if (Test-Path "$TargetPath\$LastProcessName") {
write-output "Remove-Item $TargetPath\$LastProcessName -Recurse -Confirm:$False;"
Remove-Item "$TargetPath\$LastProcessName" -Recurse -Confirm:$False;
};

write-output ""
write-output "Finish. Please check what is going wrong!"
exit;
};
write-output "Gathering VM [$ProcessVMName] Information..."

# 取得 VMID、Path
$ProcessVMID = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "VMId" | Select -ExpandProperty "Guid"
$ProcessVMPath = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ConfigurationLocation"
$ProcessVMConfigurationXML = "$ProcessVMPath\Virtual Machines\$ProcessVMID.xml"
if (!(Test-Path $ProcessVMConfigurationXML)) {write-output 'Error! No VM XML!'; exit}

write-output ""
write-output "[Check Backup Environment]"
write-output ""

write-output "Checking VM's Checkpoint status..."

# 取得硬碟資訊
$ProcessVMParentVHD = Get-VHD –VMId $ProcessVMID | Select -ExpandProperty "ParentPath"
$ProcessVMPresentVHD = Get-VHD –VMId $ProcessVMID | Select -ExpandProperty "Path"

# 取得 CheckPoint 資訊
$ProcessParentSnapshotID = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotId" | Select -ExpandProperty "Guid"
$ProcessParentSnapshotName = Get-VM –VMName $ProcessVMName | Select -ExpandProperty "ParentSnapshotName"

# 定義備份資料夾路徑
$BackupVMXMLPath = "$TargetPath\Virtual Machines\"
$BackupVMVHDPath = "$TargetPath\Virtual Hard Disks\"
$BackupVMSNSPath = "$TargetPath\Snapshots\"
$CheckPointVersionFile = "$BackupVMVHDPath\CheckPointVersion.txt"

if ($ProcessParentSnapshotID -eq $Null) {
write-output "===> Done, No CheckPoint."
write-output ""
write-output "Checking if All VHDs has no ParentDisk..."

if ($ProcessVMParentVHD -eq $Null) {
write-output "===> Done, No ParentDisk."
write-output ""
write-output "Now we can create a CheckPoint and initial Backup!"
write-output ""
} else {
write-output "===> Failed. At least one VHD has ParentDisk which would fail the Backup, Please Check!"
exit;
};

# 建立備份資料夾
if ($TestRun -ne 1) {
if ((Test-Path $BackupVMXMLPath)) {
write-output "===> Failed. Folder Already Exists! Cannot run a new Backup!"
exit;
} else {
New-Item -ItemType directory -Path $BackupVMXMLPath | Out-Null;
New-Item -ItemType directory -Path $BackupVMVHDPath | Out-Null;
New-Item -ItemType directory -Path $BackupVMSNSPath | Out-Null;
};
};
write-output ""
write-output "Copy VM XML File..."
write-output ""

# 備份設定檔
write-output "Copy-Item $ProcessVMConfigurationXML $BackupVMXMLPath$ProcessVMID.xml;"
if ($TestRun -ne 1) {
Copy-Item $ProcessVMConfigurationXML "$BackupVMXMLPath$ProcessVMID.xml";
$CheckCopySuccess = CompareFile $ProcessVMConfigurationXML "$BackupVMXMLPath$ProcessVMID.xml"
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""

write-output ""
write-output "Create a CheckPoint..."
write-output ""

# 建立 CheckPoint
write-output "Checkpoint-VM -Name $ProcessVMName -SnapshotName $NewCheckPointName"
if ($TestRun -ne 1) {Checkpoint-VM -Name $ProcessVMName -SnapshotName $NewCheckPointName;};
write-output "===> Done."
write-output ""

write-output ""
write-output "Copy Parent VHD Files..."
write-output ""

# 複製初始硬碟
Foreach ($VHD in $ProcessVMPresentVHD) {

$ParentFileSplit = $VHD.split("\")
$ParentFile = $ParentFileSplit[$ParentFileSplit.Count-1]
$ParentFile = "$BackupVMVHDPath$ParentFile"

write-output "Copy-Item $VHD $ParentFile -Recurse"
if ($TestRun -ne 1) {
Copy-Item $VHD $ParentFile -Recurse;
$CheckCopySuccess = CompareFile $VHD $ParentFile
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Make Checkpoint Version Mark..."
write-output ""

write-output "write-output $NewCheckPointName > $CheckPointVersionFile"
if ($TestRun -ne 1) {write-output $NewCheckPointName > $CheckPointVersionFile;};

$HistoryLog = $HistoryLog + "###EndOfRecord###"
write-output "====================================="
write-output "write-output $HistoryLog > $HistoryFileName;"
write-output $HistoryLog >> $HistoryFileName;
write-output "====================================="

write-output "Initial Done! Wait for next task to backup!"

stop-transcript
exit;
} else {

$CheckParentSnapshotID = Get-VMSnapshot -ID $ProcessParentSnapshotID | Select -ExpandProperty "ParentSnapshotId" | Select -ExpandProperty "Guid";
if ($CheckParentSnapshotID -eq $Null) {
write-output "===> Done, Only one CheckPoint."
write-output ""
write-output "Now check Checkpoint Version Mark!"
} else {
write-output "===> Failed. More then one CheckPoint, Can't proceed!"
exit;
};
};

if (!(Test-Path $CheckPointVersionFile)) {
write-output "===> Failed. CheckPointVersion File Doesn't Exist. Please Re-Initial Backup!";
exit;
};

$CheckPointVersionFileContent = Get-Content $CheckPointVersionFile
$CheckPointVersionFileContent = $CheckPointVersionFileContent.Split("`n")
$LastProcessName = $CheckPointVersionFileContent[0]
if ($ProcessParentSnapshotName -ne $LastProcessName) {
write-output "===> Failed. Checkpoint Version does not match. Please Re-Initial Backup!";
exit;
} else {
write-output "===> Done, CheckPoint Version Correct."
write-output ""
write-output "Now check Double Backup"
};

if ((Test-Path "$TargetPath\$LastProcessName\")) {
write-output "===> Failed. BackupFolder Exists, Don't backup too frequently."
exit
} else {
write-output "===> Done."
write-output ""
write-output "Now we can start backup process"
};

$LastProcessXMLPath = "$TargetPath\$LastProcessName\Virtual Machines\"
$LastProcessVHDPath = "$TargetPath\$LastProcessName\Virtual Hard Disks\"
$LastProcessSNSPath = "$TargetPath\$LastProcessName\Snapshots\"
if ($TestRun -ne 1) {
if (!(Test-Path $LastProcessXMLPath)) {New-Item -ItemType directory -Path $LastProcessXMLPath | Out-Null}
if (!(Test-Path $LastProcessVHDPath)) {New-Item -ItemType directory -Path $LastProcessVHDPath | Out-Null}
if (!(Test-Path $LastProcessSNSPath)) {New-Item -ItemType directory -Path $LastProcessSNSPath | Out-Null}
};
write-output ""
write-output "Copy VM XML File..."
write-output ""

# 備份設定檔
write-output "Copy-Item $ProcessVMConfigurationXML $LastProcessXMLPath$ProcessVMID.xml;"
if ($TestRun -ne 1) {
Copy-Item $ProcessVMConfigurationXML "$LastProcessXMLPath$ProcessVMID.xml";
$CheckCopySuccess = CompareFile $ProcessVMConfigurationXML "$LastProcessXMLPath$ProcessVMID.xml"
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""

# 建立 CheckPoint
write-output ""
write-output "Create a CheckPoint..."
write-output ""

write-output "Checkpoint-VM -Name $ProcessVMName -SnapshotName $NewCheckPointName"
if ($TestRun -ne 1) {Checkpoint-VM -Name $ProcessVMName -SnapshotName $NewCheckPointName;};
write-output "===> Done."
write-output ""

write-output ""
write-output "Copy CheckPoint Child VHD Files..."
write-output ""

$NewCheckPointVersionFileContent = $NewCheckPointName + "`n" + $ProcessParentSnapshotID
Foreach ($VHD in $ProcessVMPresentVHD) {

$ChildFileSplit = $VHD.split("\")
$ChildFile = $ChildFileSplit[$ChildFileSplit.Count-1]
$ChildFile = "$LastProcessVHDPath$ChildFile"

write-output "Copy-Item $VHD $ChildFile -Recurse"
if ($TestRun -ne 1) {
Copy-Item $VHD $ChildFile -Recurse;
$CheckCopySuccess = CompareFile $VHD $ChildFile
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""
$VHDFileSplit = $VHD.split("\")
$VHDFile = $VHDFileSplit[$VHDFileSplit.Count-1]
$NewCheckPointVersionFileContent = $NewCheckPointVersionFileContent + "`n" + $VHDFile
};

write-output ""
write-output "Copy Snapshot Files..."
write-output ""

$ProcessVMSnapshotsPath = "$ProcessVMPath\Snapshots\"

write-output "Copy-Item $ProcessVMSnapshotsPath$ProcessParentSnapshotID.xml $LastProcessSNSPath$ProcessParentSnapshotID.xml -Recurse;"
if ($TestRun -ne 1) {
Copy-Item "$ProcessVMSnapshotsPath$ProcessParentSnapshotID.xml" "$LastProcessSNSPath$ProcessParentSnapshotID.xml" -Recurse;
$CheckCopySuccess = CompareFile "$ProcessVMSnapshotsPath$ProcessParentSnapshotID.xml" "$LastProcessSNSPath$ProcessParentSnapshotID.xml"
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""

$SnapshotFiles = Get-ChildItem "$ProcessVMSnapshotsPath$ProcessParentSnapshotID" | Select -ExpandProperty "FullName"
foreach ($VHD in $SnapshotFiles) {

$SnapchotFileSplit = $VHD.split("\")
$SnapchotFile = $SnapchotFileSplit[$SnapchotFileSplit.Count-1]
$SnapchotFile = "$LastProcessSNSPath$ProcessParentSnapshotID\$SnapchotFile"
if (!(Test-Path $LastProcessSNSPath$ProcessParentSnapshotID)) {New-Item -ItemType directory -Path $LastProcessSNSPath$ProcessParentSnapshotID | Out-Null}

write-output "Copy-Item $VHD $SnapchotFile -Recurse;"
if ($TestRun -ne 1) {
Copy-Item $VHD $SnapchotFile -Recurse;
$CheckCopySuccess = CompareFile $VHD $SnapchotFile
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
BackupFailedDelete $ProcessVMName $NewCheckPointName $TargetPath $LastProcessName;
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Make Checkpoint Version Mark..."
write-output ""

write-output "write-output $NewCheckPointVersionFileContent > $CheckPointVersionFile"
if ($TestRun -ne 1) {write-output $NewCheckPointVersionFileContent > $CheckPointVersionFile;};
write-output "===> Done."
write-output ""

write-output ""
write-output "Update History File..."
write-output ""

$HistoryLog = $HistoryLog + "###EndOfRecord###"
write-output "====================================="
write-output "write-output $HistoryLog >> $HistoryFileName;"
write-output $HistoryLog >> $HistoryFileName;
write-output "====================================="

$HistoryFileName = "$TargetPath\History.log"

$HistoryFileContent = Get-Content $HistoryFileName
$HistoryRecord = @();
$HistoryContentTemp = '';
foreach ($HistoryContent in $HistoryFileContent) {
if ($HistoryContent -eq '###EndOfRecord###') {
$HistoryRecord += $HistoryContentTemp;
$HistoryContentTemp = '';
} else {
$HistoryContentTemp += $HistoryContent + "`n"
};
};
$HistoryRecordCount = $HistoryRecord.Count

$NewHistoryRecord = @()
$DeleteHistoryRecord = @()
$RemainHistoryRecord = @()
$InitialHistoryRecord = $HistoryRecord[0]

for ($i=1;$i -lt $HistoryRecordCount;$i++) {
if ($i -lt ($HistoryRecordCount - $RemainVersion)) {
$History = $HistoryRecord[$i].split("`n")
$DeleteHistoryRecord += $History[0]
} else {
$NewHistoryRecord += $HistoryRecord[$i]
$History = $HistoryRecord[$i].split("`n")
$RemainHistoryRecord += $History[0]
};
};
$RemainHistoryRecord += $NowParentshotName;

write-output ""
write-output "Delete previous CheckPoint..."
write-output ""

write-output "Get-VMSnapshot -ID $ProcessParentSnapshotID | Remove-VMSnapshot"
if ($TestRun -ne 1) {Get-VMSnapshot -ID $ProcessParentSnapshotID | Remove-VMSnapshot;};
write-output "===> Done."
write-output ""
write-output "Now Check Oldest CheckPoint Backup to merge."

$ChildVHDToMerge = @()
$RemoveCheckPoint = @()

foreach ($Record in $DeleteHistoryRecord) {
$RemoveCheckPointVHDPath = "$TargetPath\$Record\Virtual Hard Disks\"
$VHDToMerge = Get-ChildItem $RemoveCheckPointVHDPath | Select -ExpandProperty "FullName"
foreach ($VHD in $VHDToMerge) {
$ChildVHDToMerge += $VHD
};
$RemoveCheckPoint += $Record
};

write-output ""
write-output "Merge Old Backup VHD Files..."
write-output ""

# Merge VHD Files
foreach ($VHD in $ChildVHDToMerge) {

$ChildVHDFileSplit = $VHD.split("\")
$ChildVHDFile = $ChildVHDFileSplit[$ChildVHDFileSplit.Count-1]
$ChildVHDFile = "$BackupVMVHDPath$ChildVHDFile"

$ParentVHD = Get-VHD -Path $VHD | Select -ExpandProperty "ParentPath"
$ParentVHDFileSplit = $ParentVHD.split("\")
$ParentVHDFile = $ParentVHDFileSplit[$ParentVHDFileSplit.Count-1]
$ParentVHDFile = "$BackupVMVHDPath$ParentVHDFile"

write-output "Copy-Item $VHD $BackupVMVHDPath -Confirm:$False"
if ($TestRun -ne 1) {
Copy-Item $VHD $ChildVHDFile -Confirm:$False;
$CheckCopySuccess = CompareFile $VHD $ChildVHDFile
if ($CheckCopySuccess[0] -ne 1) {
write-output $CheckCopySuccess[1];
exit;
};
$HistoryLog = $HistoryLog + $CheckCopySuccess[1] + "`n"
};
write-output "===> Done."
write-output ""

write-output "Merge-VHD -Path $ChildVHDFile -DestinationPath $ParentVHDFile"
if ($TestRun -ne 1) {Merge-VHD -Path $ChildVHDFile -DestinationPath $ParentVHDFile;};
write-output "===> Done."
write-output ""
};


$InitialHistoryRecordSplit = $HistoryRecord[0].split("`n")
$InitialHistoryRecordSplitCount = $InitialHistoryRecordSplit.Count-1;

$RenewInitialHistoryRecord = "Initial`n"
for ($i=1;$i -lt $InitialHistoryRecordSplitCount;$i++) {
if ($InitialHistoryRecordSplit[$i] -eq '###EndOfRecord###') {break;};
$EachFile = $InitialHistoryRecordSplit[$i] -split '#@#Dino9021#@#'
$EachFileName = $EachFile[0]
$EachFileLength = Get-ItemProperty -Path $EachFileName | Select -ExpandProperty "Length"
$EachFileWriteTime = Get-ItemProperty -Path $EachFileName | Select -ExpandProperty "LastWriteTime"
$EachFileWriteTime = $EachFileWriteTime.ToString("yyyyMMdd_HHmmss")
$RenewInitialHistoryRecord = $RenewInitialHistoryRecord + "$EachFileName#@#Dino9021#@#$EachFileLength#@#Dino9021#@#$EachFileWriteTime`n"
};
$RenewInitialHistoryRecord = ($RenewInitialHistoryRecord + "###EndOfRecord###`r`n");


write-output ""
write-output "Remove oldest Backup CheckPoint..."
write-output ""

foreach ($Item in $RemoveCheckPoint) {
write-output "1 Remove-Item $Item -Recurse -Confirm:$False;"
if ($TestRun -ne 1) {
if (Test-Path "$TargetPath\$Item") {
Remove-Item "$TargetPath\$Item" -Recurse -Confirm:$False;
};
if (Test-Path "$TargetPath\$Item.log") {
Remove-Item "$TargetPath\$Item.log" -Recurse -Confirm:$False;
};
};
write-output "===> Done."
write-output ""
};

$SourceChildFiles = Get-ChildItem "$TargetPath\" | ?{ $_.PSIsContainer } | Where-Object {$_.Name -like "$CheckPointIdentification*"} | Sort-Object "FullName" | Select -ExpandProperty "FullName"
foreach ($Folder in $SourceChildFiles) {
$DeleteFolder = 1
foreach($RemainFolder in $RemainHistoryRecord) {
if ($Folder -eq "$TargetPath\$RemainFolder") {
$DeleteFolder = 0
break;
};
};
if ($DeleteFolder -eq 1) {
write-output "2 Remove-Item $Folder -Recurse -Confirm:$False;"
if ($TestRun -ne 1) {
Remove-Item $Folder -Recurse -Confirm:$False;
if (Test-Path "$Folder.log") {
Remove-Item "$Folder.log" -Recurse -Confirm:$False;
};
};
};
};

write-output ""
write-output "Write NewHistoryRecord"
write-output ""

$FinalHistoryRecord = $NewHistoryRecord -Join "###EndOfRecord###`n"

$FinalHistoryRecord = ($RenewInitialHistoryRecord + $FinalHistoryRecord + "###EndOfRecord###")

write-output "====================================="
write-output "write-output $FinalHistoryRecord > $HistoryFileName;"
if ($TestRun -ne 1) {write-output $FinalHistoryRecord > $HistoryFileName;};
write-output "====================================="

write-output ""
write-output "Backup Done..."
write-output ""


stop-transcript
exit;
===== 程式結束 =====


 Hyper-V VM Restore.ps1

===== 程式開始 =====

Param(
[String]$Path,
[String]$CI
);

if ($IC -eq '') {
$IC = 'Backup Circle By Dino9021';
};
$CheckPointIdentification = $IC

if ($Path -eq '') {
$Path = read-host "Provide the path where VM's backup at"
};

if (!(Test-Path $Path)) {"The path doesn't exist!";exit;}

$TargetPath = $Path
$HistoryFileName = "$TargetPath\History.log"
$FinalMergePath = "$TargetPath\Merge"
$MergeLog = "$FinalMergePath\Merge.log"

if (Test-Path "$FinalMergePath\") {
$Title = "Confirm:"
$Message = "`r`n`rDelete Old Merge ?`n`r`n"
$Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes"
$No = New-Object System.Management.Automation.Host.ChoiceDescription "&No"
$Options = [System.Management.Automation.Host.ChoiceDescription[]]($No, $Yes)
$Result = $host.ui.PromptForChoice($Title, $Message, $Options, 0)
switch ($Result) {
0 {write-output "`r`nYou selected No.";exit;};
1 {
write-output "`r`nYou selected Yes.";
if (Test-Path $FinalMergePath) {Remove-Item $FinalMergePath -Recurse -Confirm:$False;}
};
};
};

$MergeProcessXMLPath = "$FinalMergePath\Virtual Machines\"
$MergeProcessVHDPath = "$FinalMergePath\Virtual Hard Disks\"
$MergeProcessSNSPath = "$FinalMergePath\Snapshots\"

if (!(Test-Path $MergeProcessXMLPath)) {New-Item -ItemType directory -Path $MergeProcessXMLPath | Out-Null}
if (!(Test-Path $MergeProcessVHDPath)) {New-Item -ItemType directory -Path $MergeProcessVHDPath | Out-Null}
if (!(Test-Path $MergeProcessSNSPath)) {New-Item -ItemType directory -Path $MergeProcessSNSPath | Out-Null}

start-transcript -path $MergeLog

if (!(Test-Path $HistoryFileName )) {"History.log doesn't exist!";exit;}

$BackupVMXMLPath = "$TargetPath\Virtual Machines\"
$BackupVMVHDPath = "$TargetPath\Virtual Hard Disks\"
$BackupVMSNSPath = "$TargetPath\Snapshots\"

if (!(Test-Path $BackupVMXMLPath)) {"VM's XML Path doesn't exist!";exit;}
if (!(Test-Path $BackupVMVHDPath)) {"VM's VHD Path doesn't exist!";exit;}
if (!(Test-Path $BackupVMSNSPath)) {"VM's Snapshot Path doesn't exist!";exit;}

$HistoryFileContent = Get-Content $HistoryFileName
$HistoryRecord = @();
$HistoryContentTemp = '';
foreach ($HistoryContent in $HistoryFileContent) {
if ($HistoryContent -eq '###EndOfRecord###') {
$HistoryRecord += $HistoryContentTemp;
$HistoryContentTemp = '';
} else {
$HistoryContentTemp += $HistoryContent + "`n"
};
};

$HistoryRecordName = @()
foreach ($HistorySplit in $HistoryRecord) {
$History = $HistorySplit.split("`n")
$HistoryRecordName += $History[0];
$HistoryCount = $History.Count -1
for ($i=1; $i -lt $HistoryCount;$i++) {
$EachFile = $History[$i] -split '#@#Dino9021#@#'
$EachFileName = $EachFile[0]
if (!(Test-Path $EachFileName)) {"$EachFileName doesn't exist!";exit;}
$EachFileLength = Get-ItemProperty -Path $EachFileName | Select -ExpandProperty "Length"
$EachFileWriteTime = Get-ItemProperty -Path $EachFileName | Select -ExpandProperty "LastWriteTime"
$EachFileWriteTime = $EachFileWriteTime.ToString("yyyyMMdd_HHmmss")
if ( ($EachFileLength -ne $EachFile[1]) -or ($EachFileWriteTime -ne $EachFile[2]) ) {
$ReturnMessage = ""
$ReturnMessage = $ReturnMessage + "===> Failed. $EachFileName Size and LastWriteTime are not correct.`n"
$ReturnMessage = $ReturnMessage + "===> $EachFileName`n"
$ReturnMessage = $ReturnMessage + "===>   Size: #$EachFileLength#`n"
$ReturnMessage = $ReturnMessage + "===>   LastWriteTime: #$EachFileWriteTime#`n"
$ReturnMessage = $ReturnMessage + "===> Should be`n"
$ReturnMessage = $ReturnMessage + "===>   Size: #" + $EachFile[1] + "#`n"
$ReturnMessage = $ReturnMessage + "===>   LastWriteTime: #" + $EachFile[2] + "#"
write-output $ReturnMessage
exit;
};
};
};

Write-Host "Select CheckPoint"
for ($i=1;$i -lt $HistoryRecord.Count;$i++) {
$History = $HistoryRecord[$i].split("`n")
Write-Host ("$i - " + $History[0])
};
$CheckPointSelect = (Read-Host ("Choose from options 1-" + ($i-1))) -as [int]
if ( ($CheckPointSelect -lt 1) -or ($CheckPointSelect -gt ($i-1)) ) {
write-output "Select out of Range!"
exit
};

$CheckPointSelectName = $HistoryRecordName[$CheckPointSelect]
write-output ("Select $CheckPointSelect - [$CheckPointSelectName]")

$SnapshotsXMLToCopy = @()
$SnapshotsItemToCopy = @()
$ChildVHDToMerge = @()
$ParentVHDToMerge = @()
$CheckPointVHDToCopy = @()

for ($i=1;$i -le $CheckPointSelect;$i++) {

$LastProcessXMLPath = "$TargetPath\" + $HistoryRecordName[$i] + "\Virtual Machines\"
$LastProcessVHDPath = "$TargetPath\" + $HistoryRecordName[$i] + "\Virtual Hard Disks\"
$LastProcessSNSPath = "$TargetPath\" + $HistoryRecordName[$i] + "\Snapshots\"

$VHDToMerge = Get-ChildItem $LastProcessVHDPath | Select -ExpandProperty "FullName"

foreach ($VHD in $VHDToMerge) {
if ($i -eq 1) {
$ParentVHD = Get-VHD -Path $VHD | Select -ExpandProperty "ParentPath"
$ParentVHDFileSplit = $ParentVHD.split("\")
$ParentVHDFile = $ParentVHDFileSplit[$ParentVHDFileSplit.Count-1]
$ParentVHDFile = "$BackupVMVHDPath$ParentVHDFile"
$ParentVHDToMerge += $ParentVHDFile
};
if ($i -eq $CheckPointSelect) {
$CheckPointVHDToCopy += $VHD
} elseif ($i -ne $CheckPointSelect) {
$ChildVHDToMerge += $VHD
};

};

if ($i -eq $CheckPointSelect) {
$XMLToCopyName = Get-ChildItem $LastProcessXMLPath | Select -ExpandProperty "Name"
$XMLToCopyFullName = Get-ChildItem $LastProcessXMLPath | Select -ExpandProperty "FullName"
$XMLToReplaceName = Get-ChildItem $BackupVMXMLPath | Select -ExpandProperty "Name"
if ($XMLToCopyName -eq $XMLToReplaceName) {
$SnapshotsXMLToCopy += $XMLToCopyFullName
} else {
write-output "XML File Compare Error, Merge Failed!"
};
$SnapshotsToCopy = Get-ChildItem $LastProcessSNSPath | Select -ExpandProperty "FullName"
foreach ($Item in $SnapshotsToCopy) {
$SnapshotsItemToCopy += $Item
};
};

};
if ($SnapshotsXMLToCopy.Count -ne 1) {
write-output "XML File more than one, Error."
exit
};

<#
write-output "SnapshotsXMLToCopy"
write-output $SnapshotsXMLToCopy
write-output "============================"
write-output "SnapshotsItemToCopy"
write-output $SnapshotsItemToCopy
write-output "============================"
write-output "ChildVHDToMerge"
write-output $ChildVHDToMerge
write-output "============================"
write-output "ParentVHDToMerge"
write-output $ParentVHDToMerge
write-output "============================"
write-output "CheckPointVHDToCopy"
write-output $CheckPointVHDToCopy
write-output "============================"
#>

write-output ""
write-output "Copy Parent VHD Files..."
write-output ""

#Copy Parent VHD
foreach ($ParentVHD in $ParentVHDToMerge) {
write-output "Copy-Item $ParentVHD $MergeProcessVHDPath -Confirm:$False"
Copy-Item $ParentVHD $MergeProcessVHDPath -Confirm:$False
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Merge VHD Files..."
write-output ""

# Merge VHD Files
foreach ($VHD in $ChildVHDToMerge) {

$ChildVHDFileSplit = $VHD.split("\")
$ChildVHDFile = $ChildVHDFileSplit[$ChildVHDFileSplit.Count-1]
$ChildVHDFile = "$MergeProcessVHDPath$ChildVHDFile"

$ParentVHD = Get-VHD -Path $VHD | Select -ExpandProperty "ParentPath"
$ParentVHDFileSplit = $ParentVHD.split("\")
$ParentVHDFile = $ParentVHDFileSplit[$ParentVHDFileSplit.Count-1]
$ParentVHDFile = "$MergeProcessVHDPath$ParentVHDFile"

write-output "Copy-Item $VHD $MergeProcessVHDPath -Confirm:$False"
Copy-Item $VHD $ChildVHDFile -Confirm:$False
write-output "===> Done."
write-output ""
write-output "Merge-VHD -Path $ChildVHDFile -DestinationPath $ParentVHDFile"
Merge-VHD -Path $ChildVHDFile -DestinationPath $ParentVHDFile
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Copy CheckPoint VHD Files..."
write-output ""

# Copy CheckPoint VHD Files
foreach ($VHD in $CheckPointVHDToCopy) {
write-output "Copy-Item $VHD $MergeProcessVHDPath -Confirm:$False"
Copy-Item $VHD $MergeProcessVHDPath -Confirm:$False
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Copy VM XML File..."
write-output ""

# Copy VM XML File
write-output "Copy-Item $SnapshotsXMLToCopy $MergeProcessXMLPath -Confirm:$False"
Copy-Item $SnapshotsXMLToCopy $MergeProcessXMLPath  -Confirm:$False
write-output "===> Done."
write-output ""

write-output ""
write-output "Copy Snapshot..."
write-output ""

# Copy Snapshot
foreach ($Item in $SnapshotsItemToCopy) {
write-output "Copy-Item $Item $MergeProcessSNSPath -Recurse -Confirm:$False"
Copy-Item $Item $MergeProcessSNSPath -Recurse -Confirm:$False
write-output "===> Done."
write-output ""
};

write-output ""
write-output "Done!"
write-output ""
write-output "The merged VM is store in : $FinalMergePath"
write-output "Go import it with 'Copy' method is recommended."
write-output ""

stop-transcript


exit;

===== 程式結束 =====

沒有留言: