通過VMware vCenter利用腳本備份VM

This artical will be published in English as well: http://www.cnblogs.com/LarryAtCNBlog/p/4613320.html

如果你的環境裏剛好有VMware vCenter,裏面加了一堆ESX(i)服務器的話,那以下的腳本是有幫助的。

vCenter本身是有計劃備份的功能的,但是比較遺憾的是功能並不能滿足我使用的需求,我也更偏向於自己做腳本控制。

以下是tree的一個sample輸出,腳本只有3個,都以ps1結尾。

│  Backup-VM.ps1
│  Starter.ps1
│
└─vCenter01
        _Configuration.ps1

下面是腳本的作用描述,

Starter.ps1 - 由task scheduler調用,在其所屬的目錄搜索所有_Configuration.ps1文件,然後異步調用Backup-VM.ps1,將_Configuration.ps1的目錄傳入
Backup-VM.ps1 - 接收唯一參數-vCenterFolder,指向_Configuration.ps1所在的文件夾
_Configuration.ps1 - 配置文件,可以有多個,依據其配置文件不同指向不同的vCenter server,備份不同的VM,多個vCenter的job建多個_Configuration.ps1即可

下面是腳本內容及解釋

Starter.ps1

# 進入script所在的文件夾
Set-Location (Get-Item $MyInvocation.MyCommand.Definition).Directory

# 搜索目錄下所有的_Configuration.ps1文件
Get-ChildItem -Filter '_Configuration.ps1' -Recurse | %{
    # 調用Backup-VM.ps1,將_Configuration.ps1的目錄傳入
    Start-Process -FilePath 'powershell.exe' -ArgumentList @('-File', 'Backup-VM.ps1', '-vCenterFolder', "`"$($_.DirectoryName)`"")
}

_Configuration.ps1

# 該_Configuration.ps1是否啓用,置成$false腳本則不會執行
$Enable = $true

# 下面只是一個VM配備格式的樣本
# VM表示該VM在vCenter裏的名字
# Host表示要把該VM往哪個ESX(i)上備份
# Datastore表示目標ESX(i)上的storage名稱
<#
Sample:
$Entries = @(
@{VM = 'VMName01'; Host = 'TargetESXiServerName02'; Datastore = 'TargetESXiServerName02:storage1'; Reserve = 1;},
@{VM = 'VMName02'; Host = 'TargetESXiServerName01'; Datastore = 'TargetESXiServerName01:storage2'; Reserve = 1;}
)
#>

# 實際數據填在下面,sample如上
$Entries = @(

)

# vCenter服務器名或ip地址
$vCenter = 'vCenter01'

# 郵件設置部分,只有當出現需要關注的錯誤時,郵件部分纔會被觸發,正常備份完成不會觸發
$From =  "$($env:COMPUTERNAME)@test.com"
$To = "[email protected]"
$Subject = "VMs backup completed with errors - $vCenter"
$SmtpServer = 'mailgateway'

Backup-VM.ps1 - 自動判斷VM的網絡,如果使用vSphere Distributed Switch,會切換成API備份,如果是普通的vSphere Standard Switch網絡,用New-VM命令即可完成備份。(使用VDS後,New-VM命令會失敗,除非目標ESXi服務器上的網絡配置和當前ESXi包括同樣的VDS)

PARAM(
    [parameter(Mandatory=$true)]
    [string]$vCenterFolder
)

# 進入_Configuration.ps1所在的目錄
Set-Location -Path $vCenterFolder

$Date = Get-Date
$strDate = $Date.ToString("yyyy-MM-dd")
$strLogFile = "${strDate}.log"

# 載入_Configuration.ps1裏的variable
. '.\_Configuration.ps1'

# 定義一個寫日誌的函數
function Add-Log
{
    PARAM(
        [String]$Path,
        [String]$Value,
        [String]$Type = 'Info'
    )
    $Type = $Type.ToUpper()
    $Date = Get-Date
    Write-Host "$($Date.ToString('[HH:mm:ss] '))[$Type] $Value" -ForegroundColor $(
        switch($Type)
        {
            'WARNING' {'Yellow'}
            'Error' {'Red'}
            default {'White'}
        }
    )
    if($Path){
        Add-Content -LiteralPath $Path -Value "$($Date.ToString('[HH:mm:ss] '))[$Type] $Value" -ErrorAction:SilentlyContinue
    }
}

Add-Log -Path $strLogFile -Value 'New backup started'
# 判斷該_Configuration.ps1是否配置爲啓用
if(!$Enable)
{
    Add-Log -Path $strLogFile -Value 'Repository disabled'
    exit
}

# $vCenter是必要信息,沒有它腳本沒有辦法執行
if(!$vCenter)
{
    Add-Log -Path $strLogFile -Value 'vCenter variable is null, can not continue' -Type Error
    $Alert = $true
}
else
{
    Add-Log -Path $strLogFile -Value "vCenter: [$vCenter]"
}

# 找必要的snapin,早期版本的PowerCli沒有VDS的操作snapin,顯示一些提示以確保運行環境正常
if(!(Get-PSSnapin -Name '*VMware.VimAutomation.Vds*' -Registered -ErrorAction:SilentlyContinue))
{
    Add-Log -Path $strLogFile -Value 'This script is built from [VMware vSphere PowerCLI 5.5], suggest to run on the version' -Type Error
    Add-Log -Path $strLogFile -Value 'PSSnapin [VMware.VimAutomation.Vds] is not found, which could cause backup failure' -Type Error
    Add-Log -Path $strLogFile -Value 'Installer path: [\\Server\VMware\vSphere\VMware-PowerCLI-5.5.0-1295336.exe]' -Type Info
    exit
}

# 載入snapin
if(!(Get-PSSnapin '*vmware*' -ErrorAction:SilentlyContinue))
{
    Add-PSSnapin *vmware*
    if(!$?)
    {
        Add-Log -Path $strLogFile -Value 'Failed to add vmware pssnapin' -Type Error
        Add-Log -Path $strLogFile -Value $Error[0] -Type Error
        exit
    }
}

# 連接到vCenter,假如出現錯誤,把$Alert置成$true爲最後發出告警郵件
Connect-VIServer -Server $vCenter -Force
if(!$?)
{
    Add-Log -Path $strLogFile -Value 'Failed to connect to vCenter, cause:' -Type Error
    Add-Log -Path $strLogFile -Value $Error[0] -Type Error
    $Alert = $true
}

$Tasks = @()
# 對每一項配置的VM備份做循環
foreach($e in $Entries)
{
    Add-Log -Path $strLogFile -Value "Start doing backup for: [$($e.VM)]"
    # 添加新的VM名稱爲舊名+_ScriptBackup_+當時的日期
    $VMNew = "$($e.VM)_ScriptBackup_$strDate"
    $e.NewVM = $VMNew
    $VM = $null
    $VM = @(Get-VM -Name $e.VM -ErrorAction:SilentlyContinue)
    if(!$VM)
    {
        Add-Log -Path $strLogFile -Value 'Capture none VM, does the VM exists?' -Type Warning
        continue
    }
    if($VM.Count -ge 2)
    {
        Add-Log -Path $strLogFile -Value "Capture [$($VM.Count)] VM, duplicated VMs?: [$(($VM | %{$_.Id}) -join '], [')]" -Type Warning
        continue
    }
    $VM = $VM[0]

    # 執行到此處說明VM在vCenter裏存在並唯一,腳本沒有抓到多個VM
    $VMHost = $null
    $VMHost = @(Get-VMHost -Name $e.Host -ErrorAction:SilentlyContinue)
    if(!$VMHost)
    {
        Add-Log -Path $strLogFile -Value "Capture none VMHost, does the VMHost exists?: [$($e.Host)]" -Type Warning
        continue
    }
    if($VMHost.Count -ge 2)
    {
        Add-Log -Path $strLogFile -Value "Capture [$($VMHost.Count)] VMHost, duplicated VMHosts?: [$(($VMHost | %{$_.Id}) -join '], [')]" -Type Warning
        continue
    }
    $VMHost = $VMHost[0]

    # 執行到此處說明VMHost在vCenter裏存在並唯一,腳本沒有抓到多個VMHost
    $Datastore = $null
    $Datastore = @($VMHost | Get-Datastore -Name $e.Datastore -ErrorAction:SilentlyContinue)
    if(!$Datastore)
    {
        Add-Log -Path $strLogFile -Value "Capture none Datastore, does the Datastore exists on VMHost?: [$($e.Datastore)]" -Type Warning
        continue
    }
    if($Datastore.Count -ge 2)
    {
        Add-Log -Path $strLogFile -Value "Capture [$($Datastore.Count)] Datastore, duplicated Datastores?: [$(($Datastore | %{$_.Id}) -join '], [')]" -Type Warning
        continue
    }
    $Datastore = $Datastore[0]

    # 執行到此處說明Datastore在vCenter裏存在並唯一,腳本沒有抓到多個Datastore
    Add-Log -Path $strLogFile -Value "INFO[OLDName][NewName][Host][Datastore]: [$($VM.Name)][$VMNew][$($VMHost.Name)][$($Datastore.Name)]"

    # 判斷VM是否使用了VDS
    $VDS = $null
    $VDS = $VM | Get-VDSwitch -ErrorAction:SilentlyContinue
    if(!$VDS)
    {
        # 如果沒有使用VDS,則可以直接用New-VM命令備份
        Add-Log -Path $strLogFile -Value 'VDSwitch not found on the VM, use commandlet [New-VM] to clone'
        $Task = $null
        $Task = New-VM -Name $VMNew -VM $VM -VMHost $VMHost -Datastore $Datastore -RunAsync -ErrorAction:SilentlyContinue
        if(!$?)
        {
            Add-Log -Path $strLogFile -Value 'New-VM failed, cause:' -Type Warning
            Add-Log -Path $strLogFile -Value $Error[0] -Type Warning
            continue
        }
        Add-Log -Path $strLogFile -Value "Task launched: [$($Task.Id)]"
        $Tasks += $Task.Id
    }
    else
    {
        # 如果使用VDS,則需要使用API自行備份
        Add-Log -Path $strLogFile -Value 'VDSwitch found on the VM, need to use 2nd way to clone VM'
        Add-Log -Path $strLogFile -Value "VDS [Name][KEY]: [$(($VDS | %{$_.Name}) -join ';')][$(($VDS | %{$_.Key}) -join ';')]"
        $TargetVDS = $null
        $TargetVDS = @($VMHost | Get-VDSwitch -ErrorAction:SilentlyContinue)
        # 在目標VMHost上搜索VDS,如果目標VMHost上沒有VDS,clone沒有辦法繼續
        if(!$TargetVDS)
        {
            Add-Log -Path $strLogFile -Value 'Target VMHost server has no VDSwitch, VM which uses a VDS is unable to clone to the VMHost' -Type Warning
            continue
        }
        $TargetVDS = $TargetVDS[-1]
        # 在目標VMHost上搜索到了VDS,抓取出port數量最多的一個
        $TargetVDSGroup = @($TargetVDS | Get-VDPortgroup | Sort-Object NumPorts)[-1]
        Add-Log -Path $strLogFile -Value "Target VDS randomly picked [Name][Key]: [$($TargetVDS.Name)][$($TargetVDS.Key)]"

        # API必要參數,VMHost默認的Pool即可
        $Pool = $null
        $Pool = @($VMHost | Get-ResourcePool)[0]
        if(!$Pool)
        {
            Add-Log -Path $strLogFile -Value 'No resource pool found from VMHost, please use Get-ResourcePool to find resource pool on the VMhost' -Type Warning
            continue
        }

        # 抓取VM上的網卡對象,並對其修改,修改成爲目標VMHost上的VDS
        $VMNic = $null
        $VMNic = $VM.ExtensionData.Config.Hardware.Device | ?{$_.DeviceInfo.Label -imatch 'Network adapter'}
        if($VMNic.Count -ge 2)
        {
            Add-Log -Path $strLogFile -Value 'The VM has more than 2 network adapters, not supported' -Type Warning
            continue
        }
        $VMNicBacking = $VMNic.Backing
        $VMNicBacking.Port.SwitchUuid = $TargetVDS.Key
        $VMNicBacking.Port.PortgroupKey = $TargetVDSGroup.Key
        $VMNicBacking.Port.PortKey = ''
        $VMNicBacking.Port.ConnectionCookie = ''
        
        # API必要參數
        $spec = New-Object VMware.Vim.VirtualMachineCloneSpec
        $spec.Config = New-Object VMware.Vim.VirtualMachineConfigSpec
        $nicDev = New-Object VMware.Vim.VirtualDeviceConfigSpec
        $nicDev.Operation = 'edit'
        $nicDev.Device = $VMNic
        $nicDev.Device.Backing = $VMNicBacking
        $spec.Config.DeviceChange = $nicDev
        $spec.Config.DeviceChange[0].Device.Backing.Port.PortKey = ''
        $spec.Location = New-Object VMware.Vim.VirtualMachineRelocateSpec
        $spec.Location.Host = $VMHost.ExtensionData.MoRef
        $spec.Location.Datastore = $Datastore.ExtensionData.MoRef
        $spec.Location.Pool = $Pool.ExtensionData.MoRef
        $spec.PowerOn = $false
        $spec.Template = $false

        Add-Log -Path $strLogFile -Value 'Trying to get datacenter object, this could potentially cause a dead loop!'
        # 爲了得到下面的API必要參數$Folder,需要先得到VMHost所在的Datacenter
        $Datacenter = $VMHost.Parent
        while($Datacenter.Id -notmatch 'Datacenter')
        {
            $Datacenter = $Datacenter.Parent
        }
        # API必要參數
        $Folder = $Datacenter | Get-Folder -Name 'Discovered virtual machine'
        Add-Log -Path $strLogFile -Value 'Did not fail into a dead loop!'

        # 調用API觸發VM clone task
        $Task = $null
        $Task = $VM.ExtensionData.CloneVM_Task($Folder.ExtensionData.MoRef, $VMNew, $spec)
        if(!$?)
        {
            Add-Log -Path $strLogFile -Value 'CloneVM_Task failed, cause:' -Type Warning
            Add-Log -Path $strLogFile -Value $Error[0] -Type Warning
            continue
        }
        Add-Log -Path $strLogFile -Value "Task launched: [$($Task.Type)-$($Task.Value)]"
        $Tasks += "$($Task.Type)-$($Task.Value)"
    }
}

$Tasks = @($Tasks | ?{$_})
Add-Log -Path $strLogFile -Value "Waiting for tasks to complete, count: [$($Tasks.Count)]"
while($Tasks)
{
    # 由於clone是異步進行,腳本每5分鐘進行一次跟蹤,由於vCenter默認完成的Task會在15分鐘後清除
    # 因此下面的sleep時間最好不要超過15分鐘,否則有可能抓不到Task的成功失敗的信息
    # 調試的時候可以改成10秒一檢測
    Start-Sleep -Seconds 300
    $Tasks = Get-Task -Id $Tasks -ErrorAction:SilentlyContinue
    if(!$?)
    {
        Add-Log -Path $strLogFile -Value 'Failed to refresh Task states, cause:' -Type Warning
        Add-Log -Path $strLogFile -Value $Error[0] -Type Warning
    }
    $Tasks = @(
        $Tasks | %{
            if($_.State -ne 'Running')
            {
                Add-Log -Path $strLogFile -Value "Task completed [ID][State]: [$($_.Id)][$($_.State)]"
                if($_.State -eq 'Error')
                {
                    Add-Log -Path $strLogFile -Value $_.ExtensionData.Info.Error.LocalizedMessage -Type Warning
                }
            }
            else
            {
                Add-Log -Path $strLogFile -Value "Task running [ID][% Complete]: [$($_.Id)][$($_.PercentComplete)%]"
                $_.Id
            }
        }
    )
}

# 所有Task都完成後,腳本會開始校驗VM是否成功備份
Add-Log -Path $strLogFile -Value 'Verification start'
foreach($e in $Entries)
{
    if((Get-VM -Name $e.NewVM -ErrorAction:SilentlyContinue) -and $?)
    {
        # 備份在vCenter裏找到,說明備份成功
        Add-Log -Path $strLogFile -Value "[VM][NewVM]: [$($e.VM)][$($e.NewVM)] -- New VM found in vCenter"
        if($e.Reserve)
        {
            # 如果設置了Reserve的值,腳本會根據該值找到當前vCenter裏所有的備份,然後刪掉多餘的備份VM
            $VMBackups = $null
            $VMBackupsRemoval = $null
            $VMBackups = Get-VM -Name "$($e.VM)_ScriptBackup_*" | ?{$_.Name -imatch '_ScriptBackup_\d{4}-\d{2}-\d{2}$'} -ErrorAction:SilentlyContinue
            $VMBackups = @($VMBackups | Sort-Object 'Name')
            Add-Log -Path $strLogFile -Value "Old VM backups captured: [$($VMBackups.Count)][$(($VMBackups | %{$_.Name}) -join ';')]"
            $i = $VMBackups.Count - $e.Reserve
            if($i -le 0)
            {
                $i = 0
                Add-Log -Path $strLogFile -Value 'No old backups available to be removeds'
            }
            if($i -gt 0)
            {
                Add-Log -Path $strLogFile -Value "VM old backups can be removed count: [$i]"
                $VMBackupsRemoval = $VMBackups[0..(--$i)]
                Remove-VM -VM $VMBackupsRemoval -DeletePermanently -Confirm:$false
            }
        }
    }
    else
    {
        # 備份沒有在vCenter裏找到,說明備份失敗
        Add-Log -Path $strLogFile -Value "[VM][NewVM]: [$($e.VM)][$($e.NewVM)] -- New VM not found in vCenter, backup failure?" -Type Warning
        $Alert = $true
    }
}

Add-Log -Path $strLogFile -Value 'All done!'

# 腳本中出現了需要人工檢查的錯誤,則會發出郵件
if($Alert)
{
    Send-MailMessage -To $To -From $From -SmtpServer $SmtpServer -Subject $Subject -Attachments $strLogFile
    if(!$?)
    {
        Add-Log -Path $strLogFile -Value 'Failed to send email, cause:' -Type Warning
        Add-Log -Path $strLogFile -Value $Error[0] -Type Warning
    }
}

API的參考如下,

https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/

當然,許久之前也參考過部分VMware社區的文章~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章