Virtually Sober

If there is free booze and Virtualization; I'm there!

Automating Bulk VM Protection with Zerto Virtual Replication – ZVM only

In this blog post I will show you how to automate the protection of VMs during initial configuration or re-installation of Zerto to a new datacenter. This example is only for a Zerto Virtual Manager (ZVM) that is not managed by a Zerto Cloud Manager (ZCM).

I’m going to be using a combination of PowerShell, REST APIs and CSVs to complete the task. As a best practice I wouldn’t recommend creating too many VPGs simultaneously as you don’t want to overload the vCenter, storage, WAN and Zerto resources available to you. I’ll leave you to decide your own number but I personally would stick to batches of 5-25 depending on what you have at your disposal.

2 CSV files should be created, the first containing a list of Virtual Protection Groups (VPGs) with the VPG settings and the second a list of VMs to protect with their respective VPGs. VPG names must be unique, any duplicate entries will break the script and friendly vSphere names are used for all of the VPG settings. Here you can see an example of the VPG CSV to be created (you can download a copy at the end of the blog post if its hard to see):

BulkVMProtectionVPGExample No ZCM

Once you have created the CSV containing the VPGs and their settings, now we need a CSV listing all the VMs to be protected and which VPG it should be placed in. The script supports multiple VMs per VPG, but keep in mind that in ZVR 4.5 a VM can only exist in 1 VPG at once when building out your list. Here you can see an example of this:

BulkVMProtectionVMExample No ZCM

Now we have the VMs and VPGs we want to create lets get started in PowerShell with some standard variables for login, logging and the locations of the CSVs which you need to edit for your environment. Be sure to use the source site ZVM information as VPGs are always created from source:

################################################
# Configure the variables below
################################################
$LogDataDir = "C:\ZVRBulkVMProtection\"
$VPGList = "C:\Users\JoshuaJamie\OneDrive\Zerto\Scripts\#2016Scripts\BulkVMProtection\ZVRBulkVMProtectionv1b-VPGs.csv"
$VMList = "C:\Users\JoshuaJamie\OneDrive\Zerto\Scripts\#2016Scripts\BulkVMProtection\ZVRBulkVMProtectionv1b-VMs.csv"
$ZertoServer = "192.168.0.31"
$ZertoPort = "9669"
$ZertoUser = "administrator@lab.local"
$ZertoPassword = "Password123!"

At this point this is all you have to configure (if you cheat and download the example like I would) and that’s it! Seriously, its that simple. But I want you to learn all of the elements required so you can customize it to suit your own specific needs, so I will explain them step by step so you can build this yourself too.

Before we proceed any further we will enable some logging for troubleshooting and auditing purposes, we also need a certificate policy to ignore any cert warnings from the ZVM if it is running on an untrusted cert:

################################################
# Setting log directory and starting transcript logging
################################################
$CurrentMonth = get-date -format MM.yy
$CurrentTime = get-date -format hh.mm.ss
$CurrentLogDataDir = $LogDataDir + $CurrentMonth
$CurrentLogDataFile = $LogDataDir + $CurrentMonth + "\BulkVPGCreationLog-" + $CurrentTime + ".txt"
# Testing path exists to engine logging, if not creating it
$ExportDataDirTestPath = test-path $CurrentLogDataDir
if ($ExportDataDirTestPath -eq $False)
{
New-Item -ItemType Directory -Force -Path $CurrentLogDataDir
}
start-transcript -path $CurrentLogDataFile -NoClobber
################################################
# Setting Cert Policy - required for successful auth with the Zerto API without 
################################################
connecting to vsphere using PowerCLI
add-type @"
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
        public bool CheckValidationResult(
            ServicePoint srvPoint, X509Certificate certificate,
            WebRequest request, int certificateProblem) {
            return true;
        }
    }
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

Next up is configuring the session information to interact with the Zerto API, establish a session, set the URL we are going to use for VPG creation and import the CSVs to use:

################################################
# Building Zerto API string and invoking API
################################################
$baseURL = "https://" + $ZertoServer + ":"+$ZertoPort+"/v1/"
# Authenticating with Zerto APIs
$xZertoSessionURI = $baseURL + "session/add"
$authInfo = ("{0}:{1}" -f $ZertoUser,$ZertoPassword)
$authInfo = [System.Text.Encoding]::UTF8.GetBytes($authInfo)
$authInfo = [System.Convert]::ToBase64String($authInfo)
$headers = @{Authorization=("Basic {0}" -f $authInfo)}
$sessionBody = '{"AuthenticationMethod": "1"}'
$contentType = "application/json"
$xZertoSessionResponse = Invoke-WebRequest -Uri $xZertoSessionURI -Headers $headers -Method POST -Body $sessionBody -ContentType $contentType
#Extracting x-zerto-session from the response, and adding it to the actual API
$xZertoSession = $xZertoSessionResponse.headers.get_item("x-zerto-session")
$zertSessionHeader = @{"x-zerto-session"=$xZertoSession}
# URL to create VPG settings
$CreateVPGURL = $BaseURL+"vpgSettings"
################################################
# Importing the CSV of Profiles to use for VM Protection
################################################
$VPGCSVImport = Import-Csv $VPGList
$VMCSVImport = Import-Csv $VMList

This is where we get to the juicy part! Unfortunately at this point the small snippets end because we now need to perform a foreach operation against the VPGs to allow for multiple VPGs to be created in the script. For this script block refer to the comments in line for the explanation of each step:

################################################
# Running the creation process by VPG, as a VPG can contain multiple VMs
################################################
foreach ($VPG in $VPGCSVImport)
{
$VPGName = $VPG.VPGName
$ReplicationPriority = $VPG.ReplicationPriority
$RecoverySiteName = $VPG.RecoverySiteName
$ClusterName = $VPG.ClusterName
$FailoverNetwork = $VPG.FailoverNetwork
$TestNetwork = $VPG.TestNetwork
$DatastoreName = $VPG.DatastoreName
$JournalDatastore = $VPG.JournalDatastore
$vCenterFolder = $VPG.vCenterFolder
$JournalHistoryInHours = $VPG.JournalHistoryInHours
$RpoAlertInSeconds = $VPG.RpoAlertInSeconds
$TestIntervalInMinutes = $VPG.TestIntervalInMinutes
$JournalHardLimitInMB = $VPG.JournalHardLimitInMB
$JournalWarningThresholdInMB = $VPG.JournalWarningThresholdInMB
# Getting list of VMs for the VPG
$VPGVMs = $VMCSVImport | Where {$_.VPGName -Match "$VPGName"}
$VPGVMNames = $VPGVMs.VMName
# Logging and showing action
write-host "Creating Protection Group:$VPGName for VMs:$VPGVMNames"
################################################
# Getting Identifiers for VPG settings
################################################
# Get SiteIdentifier for getting Local Identifier later in the script
$SiteInfoURL = $BaseURL+"localsite"
$SiteInfoCMD = Invoke-RestMethod -Uri $SiteInfoURL -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$LocalSiteIdentifier = $SiteInfoCMD | Select SiteIdentifier -ExpandProperty SiteIdentifier
# Get SiteIdentifier for getting Identifiers
$TargetSiteInfoURL = $BaseURL+"virtualizationsites"
$TargetSiteInfoCMD = Invoke-RestMethod -Uri $TargetSiteInfoURL -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$TargetSiteIdentifier = $TargetSiteInfoCMD | Where-Object {$_.VirtualizationSiteName -eq $RecoverySiteName} | select SiteIdentifier -ExpandProperty SiteIdentifier 
# Get NetworkIdentifiers for API
$VISiteInfoURL1 = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/networks"
$VISiteInfoCMD1 = Invoke-RestMethod -Uri $VISiteInfoURL1 -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$FailoverNetworkIdentifier = $VISiteInfoCMD1 | Where-Object {$_.VirtualizationNetworkName -eq $FailoverNetwork}  | Select NetworkIdentifier -ExpandProperty NetworkIdentifier 
$TestNetworkIdentifier = $VISiteInfoCMD1 | Where-Object {$_.VirtualizationNetworkName -eq $TestNetwork}  | Select NetworkIdentifier -ExpandProperty NetworkIdentifier 
# Get ClusterIdentifier for API
$VISiteInfoURL2 = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/hostclusters"
$VISiteInfoCMD2 = Invoke-RestMethod -Uri $VISiteInfoURL2 -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$ClusterIdentifier = $VISiteInfoCMD2 | Where-Object {$_.VirtualizationClusterName -eq $ClusterName}  | Select ClusterIdentifier -ExpandProperty ClusterIdentifier 
# Get ServiceProfileIdenfitifer for API
$VISiteServiceProfileURL = $BaseURL+"serviceprofiles"
$VISiteServiceProfileCMD = Invoke-RestMethod -Uri $VISiteServiceProfileURL -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$ServiceProfileIdentifier = $VISiteServiceProfileCMD | Where-Object {$_.Description -eq $ServiceProfile}  | Select ServiceProfileIdentifier -ExpandProperty ServiceProfileIdentifier 
# Get DatastoreIdentifiers for API
$VISiteInfoURL3 = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/datastores"
$VISiteInfoCMD3 = Invoke-RestMethod -Uri $VISiteInfoURL3 -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$DatastoreIdentifier = $VISiteInfoCMD3 | Where-Object {$_.DatastoreName -eq $DatastoreName}  | Select DatastoreIdentifier -ExpandProperty DatastoreIdentifier 
$JournalDatastoreIdentifier = $VISiteInfoCMD3 | Where-Object {$_.DatastoreName -eq $JournalDatastore}  | Select DatastoreIdentifier -ExpandProperty DatastoreIdentifier 
# Get Folders for API
$VISiteInfoURL4 = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/folders"
$VISiteInfoCMD4 = Invoke-RestMethod -Uri $VISiteInfoURL4 -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$FolderIdentifier = $VISiteInfoCMD4 | Where-Object {$_.FolderName -eq $vCenterFolder}  | Select FolderIdentifier -ExpandProperty FolderIdentifier
# DatastoreClusters for API - not used in this example
# $VISiteInfoURL5 = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/datastoreclusters"
# $VISiteInfoCMD5 = Invoke-RestMethod -Uri $VISiteInfoURL5 -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
# $DataStoreClusterIdentifier = $VISiteInfoCMD5 | Where-Object {$_.FolderName -eq $vCenterFolder}  | Select FolderIdentifier -ExpandProperty FolderIdentifier
# Get HostIdentifier for API - not used in this example as using target cluster (which uses simple round robin)
# $VISiteHostURL = $BaseURL+"virtualizationsites/$TargetSiteIdentifier/hosts"
# $VISiteHostCMD = Invoke-RestMethod -Uri $VISiteHostURL -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
# $HostIdentifier = $VISiteHostCMD | Where-Object {$_.VirtualizationHostName -eq "Host name here"}  | Select HostIdentifier -ExpandProperty HostIdentifier 
################################################
# Getting a VM identifier for each VM to be protected and adding it to the VMIDarray
################################################
# Reseting VM identifier list, required for creating multiple protection groups
$VMIdentifierList = $null
$VMIDArray = @()
# Running for each VM operation against the VPG name
foreach ($VMLine in $VPGVMNames)
{
write-host "$VMLine"
$VMInfoURL = $BaseURL+"virtualizationsites/$LocalSiteIdentifier/vms"
$VMInfoCMD = Invoke-RestMethod -Uri $VMInfoURL -TimeoutSec 100 -Headers $zertSessionHeader -ContentType "application/JSON"
$VMIdentifier = $VMInfoCMD | Where-Object {$_.VmName -eq $VMLine} | select VmIdentifier -ExpandProperty VmIdentifier
# Adding VM ID to array
$VMIDArrayLine = new-object PSObject
$VMIDArrayLine | Add-Member -MemberType NoteProperty -Name "VMID" -Value $VMIdentifier
$VMIDArray += $VMIDArrayLine
}
################################################
# Building JSON Request for posting VPG settings to API
################################################
$JSONMain = 
"{
  ""Backup"": null,
  ""Basic"": {
    ""JournalHistoryInHours"": ""$JournalHistoryInHours"",
    ""Name"": ""$VPGName"",
    ""Priority"": ""$ReplicationPriority"",
    ""ProtectedSiteIdentifier"": ""$LocalSiteIdentifier"",
    ""RecoverySiteIdentifier"": ""$TargetSiteIdentifier"",
    ""RpoInSeconds"": ""$RpoAlertInSeconds"",
    ""ServiceProfileIdentifier"": null,
    ""TestIntervalInMinutes"": ""$TestIntervalInMinutes"",
    ""UseWanCompression"": true,
    ""ZorgIdentifier"": null
  },
  ""BootGroups"": {
    ""BootGroups"": [
      {
        ""BootDelayInSeconds"": 0,
        ""BootGroupIdentifier"": ""00000000-0000-0000-0000-000000000000"",
        ""Name"": ""Default""
      }
    ]
  },
  ""Journal"": {
    ""DatastoreClusterIdentifier"":null,
    ""DatastoreIdentifier"":""$DatastoreIdentifier"",
    ""Limitation"":{
      ""HardLimitInMB"":""$JournalHardLimitInMB"",
      ""HardLimitInPercent"":null,
      ""WarningThresholdInMB"":""$JournalWarningThresholdInMB"",
      ""WarningThresholdInPercent"":null
    }
  },
  ""Networks"": {
    ""Failover"":{
      ""Hypervisor"":{
        ""DefaultNetworkIdentifier"":""$FailoverNetworkIdentifier""
      }
    },
    ""FailoverTest"":{
      ""Hypervisor"":{
        ""DefaultNetworkIdentifier"":""$TestNetworkIdentifier""
      }
    }
  },
  ""Recovery"": {
    ""DefaultDatastoreIdentifier"":""$DatastoreIdentifier"",
    ""DefaultFolderIdentifier"":""$FolderIdentifier"",
    ""DefaultHostClusterIdentifier"":""$ClusterIdentifier"",
    ""DefaultHostIdentifier"":null,
    ""ResourcePoolIdentifier"":null
  },
  ""Scripting"": {
    ""PostBackup"": null,
    ""PostRecovery"": {
      ""Command"": null,
      ""Parameters"": null,
      ""TimeoutInSeconds"": 0
    },
    ""PreRecovery"": {
      ""Command"": null,
      ""Parameters"": null,
      ""TimeoutInSeconds"": 0
    }
  },
  ""Vms"": ["
# Resetting VMs if a previous VPG was created in this run of the script
$JSONVMs = $null
# Creating JSON request per VM using the VM array for all the VMs in the VPG
foreach ($VM in $VMIDArray)
{
$VMID = $VM.VMID
$JSONVMsLine = "{""VmIdentifier"":""$VMID""}"
# Running if statement to check if this is the first VM in the array, if not then a comma is added to string
if ($JSONVMs -ne $null)
{
$JSONVMsLine = "," + $JSONVMsLine
}
$JSONVMs = $JSONVMs + $JSONVMsLine
}
# Creating the end of the JSON request  
$JSONEnd = "]
}"
# Putting the JSON request elements together and outputting the request
$JSON = $JSONMain + $JSONVMs + $JSONEnd
write-host "Running JSON request below:
$JSON"
################################################
# Posting the VPG JSON Request to the API
################################################
Try 
{
$VPGSettingsIdentifier = Invoke-RestMethod -Method Post -Uri $CreateVPGURL -Body $JSON -ContentType $ContentType -Headers $zertSessionHeader 
write-host "VPGSettingsIdentifier: $VPGSettingsIdentifier"
}
Catch {
Write-Host $_.Exception.ToString()
$error[0] | Format-List -Force
}
################################################
# Confirming VPG settings from API
################################################
$ConfirmVPGSettingURL = $BaseURL+"vpgSettings/"+"$VPGSettingsIdentifier"
$ConfirmVPGSettingCMD = Invoke-RestMethod -Uri $ConfirmVPGSettingURL -Headers $zertSessionHeader -ContentType "application/json"
################################################
# Commiting the VPG settings to be created
################################################
$CommitVPGSettingURL = $BaseURL+"vpgSettings/"+"$VPGSettingsIdentifier"+"/commit"
write-host "Commiting VPG creation for VPG:$VPGName with URL:
$CommitVPGSettingURL"
Try 
{
Invoke-RestMethod -Method Post -Uri $CommitVPGSettingURL -ContentType "application/json" -Headers $zertSessionHeader -TimeoutSec 100
}
Catch {
Write-Host $_.Exception.ToString()
$error[0] | Format-List -Force
}
################################################
# Waiting xx minute/s before creating the next VPG
################################################
write-host "Waiting 1 minute before creating the next VPG or stopping script if on the last VPG"
sleep 60
#
# End of per VPG actions below
}
# End of per VPG actions above
#
################################################
# Stopping logging
################################################
Stop-Transcript

And there you go! Feel free to copy and paste all the examples above or simply click the link below to download a ZIP file containing the example PS1 and CSV files. Be sure to use the ZVRBulkVMProtectionv1b.ps1 example and not the A version as this is for ZCM managed environments only:

Download ZVRBulkVMProtectionv1.zip

I hope you found this useful, any issues or feedback let me know and happy scripting!

Joshua

Leave a Reply

%d bloggers like this: