🚀 Overview & Problem Statement
Patching ESXi hosts in a production vSphere DRS cluster looks simple on paper but involves a precise sequence of steps that must all succeed before the host can go into maintenance mode. You need to switch DRS to Manual so the cluster does not fight your controlled migrations, disable all DRS VM-Host affinity rules that would block vMotions, verify that every destination host has enough free memory and CPU headroom to absorb the incoming workloads, and — if you are running Zerto for replication — gracefully evacuate the Virtual Replication Appliance (VRA) on that host so that replication continues uninterrupted on another host.
That is before the ESXCLI patching command even runs. After the reboot you need to verify the correct software profile is installed, take the host out of maintenance mode, re-enable all DRS rules, and return DRS to Fully Automated. Done manually, one host takes 45–60 minutes of active babysitting. With a 10-node cluster and a monthly patch cycle that is a full working day per site, every month.
This post walks through a fully parameterised PowerShell and PowerCLI script that handles the complete end-to-end lifecycle, driven by a Jenkins Parameterised Build. Engineers supply the target ESXi hostname, trigger the build, and walk away. Email notifications arrive at every key milestone. The Jenkins console streams live progress throughout.
$Env:Username, $Env:Password, and
$Env:FromVMHostName at runtime only.
🏗️ Automation Flow — All Three Phases
The script is structured into three gated phases. If any step fails, an alert email is sent and the script exits with code 1 so Jenkins marks the build FAILED immediately rather than continuing into a broken state.
✅ Prerequisites
Jenkins LTS + Windows Agent
A Windows node running PowerShell 5.1+. Label it windows-powercli.
The script uses Windows paths and PowerCLI which requires a Windows host.
VMware PowerCLI
On the Windows agent:
Install-Module VMware.PowerCLI -Scope AllUsers -Force
Then: Set-PowerCLIConfiguration -ParticipateInCEIP $false
Jenkins Credentials Binding
Install the Credentials Binding plugin. Store vCenter credentials as
Secret Text with IDs vcenter-username and vcenter-password.
Patch Depot on Shared Datastore
The ESXi offline bundle ZIP must be on a shared datastore accessible by all hosts.
Update $PatchDepotPath in the script to the VMFS path.
Zerto Virtual Manager
Required if Zerto replication is running. The script uses ZVM REST API v1. Port 9669 must be reachable from the Jenkins Windows agent.
Evacuate.json on Agent
Place Evacuate.json at C:\Patching\Evacuate.json on the
Windows agent before the first run. See Section 6 for the
exact payload structure.
SMTP Relay
An internal SMTP relay reachable from the Jenkins agent. Update $SmtpServer,
$MailFrom, and $MailTo in the script configuration block.
Log Directory
Create C:\Patching\ on the Windows agent and grant the Jenkins service
account write permissions. Transcripts land here as PS-logs.txt.
GET https://<ZVM_IP>:9669/v1/vras to retrieve the
VraIdentifier for each ESXi host. Update the $ZertoVraIds
hashtable in the script with your values. Incorrect identifiers will cause the evacuation
POST to target the wrong VRA silently.
📜 Script Phases — Deep Dive
vCenter Connectivity and Authentication
Before touching the cluster, the script tests TCP port 443 reachability to vCenter using
Test-NetConnection. Only on success does it attempt Connect-VIServer.
Both checks gate on Exit 1, making Jenkins mark the build FAILED
immediately rather than continuing against a disconnected vCenter.
# Gate 1 — TCP reachability before attempting login
$VcenterNetTest = Test-NetConnection -ComputerName $vcserver -Port 443
$VcenterReachable = $VcenterNetTest.TcpTestSucceeded
if (-not $VcenterReachable) {
Write-Host "ERROR: Cannot reach $vcserver on port 443. Exiting." -ForegroundColor Red
Exit 1
}
# Gate 2 — Validate authenticated session
$VcenterConnection = Connect-VIServer -Server $vcserver -User $vcusername -Password $vcpassword
if (-not $VcenterConnection.IsConnected) {
Write-Host "ERROR: Failed to connect to $vcserver. Check credentials." -ForegroundColor Red
Exit 1
}
Write-Host "Connected to vCenter: $vcserver" -ForegroundColor Green
Set DRS to Manual
With DRS in Fully Automated mode the cluster can move VMs back onto the source host while
you are evacuating it. Switching to Manual means DRS still generates
recommendations but takes no automatic action. The cluster name and DRS-enabled state are
verified — the script exits with Exit 1 if either check fails.
$Cluster = Get-Cluster -Name $ClusterName
$Cluster_DRS = $Cluster.DrsEnabled
if ($Cluster_DRS -eq $true -and $Cluster.Name -like $ClusterName) {
Write-Host "Setting DRS to Manual on cluster: $ClusterName" -ForegroundColor Yellow
Set-Cluster -Cluster $ClusterName -DRSEnabled:$true `
-DrsAutomationLevel "Manual" -Confirm:$false
}
else {
Write-Host "DRS not enabled on $ClusterName or name mismatch. Exiting." -ForegroundColor Red
Exit 1
}
Disable Non-Zerto DRS Rules
DRS VM Rules (keep-apart or keep-together) and VM-Host Rules (pin VMs to specific hosts) block vMotion off the source host. The script disables all of them except rules containing Zerto in their name — Zerto manages VRA placement rules independently, and disabling them would disrupt replication continuity.
# Disable VM-to-VM DRS rules (keep-apart / keep-together) — skip Zerto
Get-Cluster -Name $ClusterName | Get-DrsRule |
Where-Object { $_.Name -notlike "*Zerto*" } |
Set-DrsRule -Enabled:$false
# Disable VM-to-Host affinity/anti-affinity rules — skip Zerto
Get-Cluster -Name $ClusterName | Get-DrsVMHostRule |
Where-Object { $_.Name -notlike "Zerto*" } |
Set-DrsVMHostRule -Enabled:$false
Write-Host "Non-Zerto DRS rules disabled successfully." -ForegroundColor Green
Intelligent VM vMotion with Memory & CPU Threshold Checks
For every powered-on VM on the source host (Zerto VRA appliances excluded), the script
iterates candidate destination hosts sorted by lowest memory usage. Before migrating,
it calculates the projected memory percentage after placing the VM —
not just current utilisation — to prevent over-placement onto an already-pressured host.
Both the memory ceiling (85%) and CPU ceiling (95%) must pass before Move-VM
is called.
$SingleVM.MemoryGB) to the
destination host's current usage ($DestHost.MemoryUsageGB) and calculates
the resulting percentage of total host memory. This prevents over-placement even when
an individual VM's footprint is large.
# All powered-on VMs on the source host, excluding Zerto VRA appliances
$VMsInHost = Get-VMHost -Name $FromVMHostName | Get-VM |
Where-Object { $_.PowerState -like "*On" -and $_.Name -notlike "Z-VRA*" }
foreach ($SingleVM in $VMsInHost) {
# Candidate hosts: not source, not in maintenance, sorted least-loaded first
$WorkingNodes = Get-Cluster -Name $ClusterName | Get-VMHost |
Where-Object {
$_.Name -notlike $FromVMHostName -and
$_.ConnectionState -notlike "*ain*" -and # excludes Maintenance
$_.ConnectionState -notlike "NotResponding" -and
$_.ConnectionState -notlike "Unknown"
} | Sort-Object -Property MemoryUsageGB
$Migrated = $false
foreach ($DestHost in $WorkingNodes) {
# Projected memory % after placing this VM on the destination host
[int]$ProjectedMemGB = $DestHost.MemoryUsageGB + $SingleVM.MemoryGB
[int]$MemPercent = $ProjectedMemGB /
(Get-VMHost -Name $DestHost.Name -ErrorAction Stop).MemoryTotalGB * 100
[int]$CPUPercent = $DestHost.CpuUsageMhz / $DestHost.CpuTotalMhz * 100
if ($MemPercent -lt 85 -and $CPUPercent -lt 95) {
Move-VM -VM $SingleVM.Name -Destination $DestHost.Name -ErrorAction Stop
Write-Host "[$($SingleVM.Name)] moved to [$($DestHost.Name)]" -ForegroundColor Green
$Migrated = $true
break # found a suitable host — move to the next VM
}
}
if (-not $Migrated) {
Write-Host "WARNING: No suitable host found for [$($SingleVM.Name)]" -ForegroundColor Red
}
}
Power Off VRA VM and Enter Maintenance Mode
After the Zerto VRA evacuation completes, the VRA VM is still powered on but idle.
The script sends a graceful OS shutdown via Stop-VMGuest and waits 30 seconds.
It then counts remaining powered-on VMs: if exactly one remains (VRA did not respond to guest
tools), Stop-VM force-powers it off. Only when zero VMs remain powered on does
Set-VMHost -State Maintenance execute, and the resulting state is verified
before patching proceeds.
ESXCLI Software Profile Update & Reboot Wait
The script uses the PowerCLI ESXCLI v2 interface via Get-EsxCli -V2 — no SSH
or direct host credentials needed. It enumerates profiles in the depot ZIP, builds the
expected post-patch name ((Updated) <ProfileName>), compares it to the
currently installed profile, then applies the update. After the forced reboot, a 60-second
sleep gives the host time to begin shutting down, then ICMP polling loops until the host
responds, followed by a 120-second service-initialisation wait.
$ESXiCLI = Get-EsxCli -VMHost $FromVMHostName -V2
$ESXiInMaintenance = $ESXiCLI.system.maintenanceMode.get.Invoke()
# Enumerate profiles available in the offline depot ZIP
$TargetProfileName = $ESXiCLI.software.sources.profile.list.Invoke(
@{ 'depot' = $PatchDepotPath }).Name
$CurrentProfile = ($ESXiCLI.software.profile.get.Invoke()).Name
$ExpectedPostPatch = "(Updated) $TargetProfileName"
if ($ESXiInMaintenance -eq "Enabled" -and $ExpectedPostPatch -ne $CurrentProfile) {
# 'update' preserves VIB configuration — preferred over 'install' for incremental patches
$UpdateResult = $ESXiCLI.software.profile.update.invoke(@{
'depot' = $PatchDepotPath
'profile' = $TargetProfileName
})
Write-Host "Result: $($UpdateResult.Message)"
Write-Host "Reboot required: $($UpdateResult.RebootRequired)"
if ($UpdateResult.RebootRequired -eq $true) {
Restart-VMhost -VMHost $FromVMHostName -Force:$true -Confirm:$false
}
}
# Brief sleep to allow shutdown to begin before polling
Start-Sleep -Seconds 60
# Poll ICMP until the host responds after reboot
do {
$HostPingable = Test-Connection -ComputerName $FromVMHostName -Quiet
} until ($HostPingable -eq $true)
Write-Host "$FromVMHostName is pingable. Waiting for ESXi services..."
Start-Sleep -Seconds 120 # Allow all ESXi services to fully initialise
Post-Patch Profile Verification & Cluster Restoration
After the host comes back online, ESXCLI is re-initialised and the installed profile name
is read. If it matches $ExpectedPostPatch, patching is confirmed successful.
DRS rules are re-enabled (excluding Zerto), DRS is returned to Fully Automated,
and the host exits maintenance mode via Set-VMHost -State Connected. A final
success email is sent and the vCenter session is cleanly disconnected.
# Re-verify installed profile after reboot
$ESXiCLI_Post = Get-EsxCli -VMHost $FromVMHostName -V2
$PostPatchProfile = ($ESXiCLI_Post.software.profile.get.Invoke()).Name
if ($PostPatchProfile -eq $ExpectedPostPatch) {
Write-Host "Verified: $FromVMHostName running $PostPatchProfile" -ForegroundColor Green
} else {
Write-Host "WARNING: Expected [$ExpectedPostPatch] — Found [$PostPatchProfile]" -ForegroundColor Red
}
# Restore DRS — re-enable all non-Zerto rules, return to Fully Automated
Set-Cluster -Cluster $ClusterName -DRSEnabled:$true -DrsAutomationLevel "FullyAutomated" -Confirm:$false
Get-Cluster -Name $ClusterName | Get-DrsRule | Where-Object { $_.Name -notlike "*Zerto*" } | Set-DrsRule -Enabled:$true
Get-Cluster -Name $ClusterName | Get-DrsVMHostRule | Where-Object { $_.Name -notlike "Zerto*" } | Set-DrsVMHostRule -Enabled:$true
# Exit maintenance mode and reconnect host to cluster
Set-VMHost -VMHost $FromVMHostName -State Connected
Disconnect-VIServer -Server $vcserver -Confirm:$false
🔁 Zerto VRA Evacuation via REST API
If a host running a Zerto VRA goes into maintenance mode without first evacuating that VRA,
all Virtual Protection Groups (VPGs) anchored to it enter an error state and replication
is suspended. The Zerto REST API provides a
changerecoveryvra/execute endpoint that migrates a VRA's protected VPGs to
another VRA before the host goes offline.
Authentication — Get-ZertoSession
The script authenticates to the ZVM using HTTP Basic authentication over HTTPS on port 9669.
The response header contains an x-zerto-session token that must be included
in the header of every subsequent API call.
function Get-ZertoSession {
param([string]$ZvmUser, [string]$ZvmPassword)
$sessionURI = "https://$strZVMIP`:$strZVMPort/v1/session/add"
# Encode credentials as Base64 for Basic auth header
$authBytes = [System.Text.Encoding]::UTF8.GetBytes("$ZvmUser`:$ZvmPassword")
$authBase64 = [System.Convert]::ToBase64String($authBytes)
$authHeader = @{ Authorization = "Basic $authBase64" }
$response = Invoke-WebRequest -Uri $sessionURI -Headers $authHeader `
-Method POST -Body '{"AuthenticationMethod": "1"}' `
-ContentType "application/json" -SkipCertificateCheck
# The session token is returned in the response header, not the body
return $response.Headers.get_item("x-zerto-session")
}
Discover Your VRA Identifiers
Each VRA has a unique VraIdentifier string in Zerto. The script stores these
in a hashtable keyed by ESXi host FQDN — adding a new host is a single line. Run the
snippet below once against your ZVM to discover the identifiers for your environment:
$xZertoSession = Get-ZertoSession -ZvmUser $strZVMUser -ZvmPassword $strZVMPwd
$ZertoHeader = @{ "x-zerto-session" = $xZertoSession }
$vraListURI = "https://$strZVMIP`:$strZVMPort/v1/vras"
$vrasList = Invoke-RestMethod -Uri $vraListURI -Headers $ZertoHeader `
-ContentType "application/json" -SkipCertificateCheck
# Outputs: HostDisplayName (matches ESXi FQDN) and VraIdentifier
$vrasList | Select-Object HostDisplayName, VraIdentifier | Format-Table -AutoSize
Hashtable Configuration — One Line Per Host
$ZertoVraIds = @{
"esx101.yourdomain.local" = "2724388199262172525"
"esx102.yourdomain.local" = "2724388199262170715"
"esx103.yourdomain.local" = "2724388199262168037"
"esx104.yourdomain.local" = "2724388199262150817"
# Add one line per host — no other code changes needed
}
Evacuation Execution and Progress Polling
The evacuation endpoint returns a task ID. The script polls the task progress endpoint twice: first after 90 seconds, then again after a further 120 seconds if the task is not yet at 100%. If it still has not completed, the script exits with Exit 1 and sends an alert email rather than proceeding to maintenance mode. This prevents the host going dark while VPGs are mid-migration between VRAs.
# POST evacuation task — response is the task ID string
$executeURI = "https://$strZVMIP`:$strZVMPort/v1/vras/$VraId/changerecoveryvra/execute"
$taskId = Invoke-RestMethod -Uri $executeURI -Headers $ZertoHeader `
-Body ($EvacuatePayload | ConvertTo-Json) `
-ContentType "application/json" -Method Post -SkipCertificateCheck
$taskURI = "https://$strZVMIP`:$strZVMPort/v1/tasks/$taskId"
# Poll 1 — wait 90 seconds and check progress
Start-Sleep -Seconds 90
$p1 = (Invoke-RestMethod -Uri $taskURI -Headers $ZertoHeader `
-ContentType "application/json" -SkipCertificateCheck).Status.Progress
if ($p1 -eq 100) { return $true }
# Poll 2 — wait an additional 120 seconds before giving up
Start-Sleep -Seconds 120
$p2 = (Invoke-RestMethod -Uri $taskURI -Headers $ZertoHeader `
-ContentType "application/json" -SkipCertificateCheck).Status.Progress
if ($p2 -ne 100) {
Write-Host "ERROR: VRA evacuation did not complete. Check ZVM." -ForegroundColor Red
return $false # caller sends alert email + Exit 1
}
📄 Evacuate.json — Payload Explained
The evacuation POST request body is loaded from a JSON file at
C:\Patching\Evacuate.json on the Jenkins Windows agent. This file tells the
Zerto API how to re-assign the VMs protected by the source VRA to other VRAs/hosts in the
same Zerto site.
Correct Payload Structure
The Zerto API expects a vmsAllocations array. Each entry in the array maps a
protected VM identifier (vmIdentifier) to a target host identifier
(hostIdentifier). When both are set to null, Zerto automatically
selects the best available VRA on another host — this is the recommended approach for
routine host maintenance because it lets Zerto's placement logic decide.
{
"vmsAllocations": [
{
"hostIdentifier": null,
"vmIdentifier" : null
}
]
}
hostIdentifier and vmIdentifier to
null is an intentional instruction to the Zerto API —
it signals "evacuate all protected VMs from this VRA and let Zerto decide the best
target VRA automatically." This is not a placeholder waiting to be filled in; it is
the correct value for a blanket host-maintenance evacuation where you want Zerto's built-in
load balancing to handle placement.
Optional: Target a Specific Host
If you need to direct all VPGs to a specific target host rather than letting Zerto choose,
replace null with the Zerto host identifier for the target ESXi host. Retrieve
host identifiers from GET /v1/virtualizationsites/{siteId}/hosts on your ZVM.
{
"vmsAllocations": [
{
"hostIdentifier": "host-identifier-from-zerto-api",
"vmIdentifier" : null
}
]
}
Get-Content -Raw at runtime. If the file is
missing or the JSON is malformed, the evacuation POST will fail. Place
Evacuate.json at C:\Patching\Evacuate.json on the Jenkins
Windows agent and verify it parses correctly with
Get-Content -Raw C:\Patching\Evacuate.json | ConvertFrom-Json.
How the Script Loads It
# Load the evacuation payload from the JSON file on the Jenkins agent
$EvacuatePayload = Get-Content -Raw "C:\Patching\Evacuate.json" | ConvertFrom-Json
# The payload is then passed as the request body to the Zerto evacuation endpoint
# ConvertTo-Json serialises it back to a JSON string for Invoke-RestMethod
$taskId = Invoke-RestMethod -Uri $executeURI -Headers $ZertoHeader `
-Body ($EvacuatePayload | ConvertTo-Json) `
-ContentType "application/json" -Method Post -SkipCertificateCheck
🔧 Jenkins Setup — Parameterised Build
Running this from Jenkins gives you encrypted credentials, a complete audit trail of every patch run, real-time console output, archived transcript logs as build artefacts, and the ability to trigger or schedule without ever touching the script. Here is the exact configuration.
Create a New Freestyle Job
Go to Jenkins → New Item → Freestyle Project. Name it vSphere-ESXi-Patching. Tick "This project is parameterised" in the General section before adding any parameters below.
Store Credentials as Secret Text
Go to Manage Jenkins → Credentials → Global → Add Credentials.
Add two Secret Text entries: ID vcenter-username (your vCenter
service account name) and ID vcenter-password (the password). These are
AES-256 encrypted at rest and masked in all console output and build logs.
Add String Parameter — FromVMHostName
Click Add Parameter → String Parameter. Name:
FromVMHostName. Leave the default value blank — forcing
the operator to type the FQDN each run prevents accidentally reusing the previous host.
Description: "FQDN of the ESXi host to patch — e.g. esx101.yourdomain.local".
Bind Credentials to Environment Variables
Under Build Environment, tick "Use secret text(s) or file(s)".
Add two bindings: map vcenter-username → variable name Username,
and vcenter-password → variable name Password. Jenkins injects
these as $Env:Username and $Env:Password — exactly what the
script reads at the top of its configuration block.
Add Windows PowerShell Build Step
Under Build → Add Build Step → Windows PowerShell, enter the two
lines below. Jenkins automatically passes all String Parameters and bound secrets as
$Env: variables before this step runs.
Set-ExecutionPolicy Bypass -Scope Process -Force
& "C:\Patching\VMware-ESXi-PrePatching-Automation.ps1"
Restrict to the Windows PowerCLI Agent
Under General → Restrict where this project can be run, enter
windows-powercli. This prevents Jenkins from dispatching the job to a
Linux agent where PowerCLI is unavailable and the script would fail immediately or,
worse, partially.
Archive Transcript Log as Build Artefact
Under Post-build Actions → Archive the artefacts, enter
C:/Patching/PS-logs.txt. Every build retains the full
Start-Transcript output — a downloadable audit record of every vCenter
command, vMotion result, Zerto API response, and ESXCLI output for that run.
Optional: Email Extension Plugin for Build-Level Alerts
Install the Email Extension Plugin. Under Post-build Actions, add Editable Email Notification, triggering on both Success and Failure. The script already sends milestone emails internally, but this Jenkins-level plugin fires a build summary even if the script crashes before its own notification logic executes.
📋 Build Parameters & Script Configuration Reference
The Jenkins build form exposes one user-typed parameter. All other values are configured in the script configuration block at the top of the file — update them once for your environment and they apply to every build run.
Jenkins Parameters
| Parameter | Type | Description | Required? |
|---|---|---|---|
FromVMHostName | String | FQDN of the ESXi host to patch — e.g. esx101.yourdomain.local. Left blank by default to force the operator to type it each run. | REQUIRED |
Username | Secret | vCenter service account name — injected via Credentials Binding plugin, never stored in plain text | SECRET |
Password | Secret | vCenter account password — injected via Credentials Binding plugin, masked in all build logs and console output | SECRET |
Script Configuration Block — Update Once Per Environment
| Variable | Description | Example Value |
|---|---|---|
$vcserver | vCenter Server FQDN or IP address | vcenter.yourdomain.local |
$ClusterName | DRS cluster name containing the ESXi hosts to patch | Cluster01 |
$MaxMemAllowed | Memory utilisation % ceiling for destination host selection during vMotion | 85 |
$strZVMIP | IP address of the Zerto Virtual Manager | 172.16.x.x |
$strZVMPort | Zerto REST API port — default is 9669 | 9669 |
$ZertoVraIds | Hashtable mapping ESXi host FQDN to its Zerto VraIdentifier. Run GET /v1/vras on your ZVM to discover values. | See Section 5 |
$SmtpServer | Internal SMTP relay IP or hostname reachable from the Jenkins Windows agent | 172.0.x.x |
$MailFrom | Sender address for all milestone notification emails | vmware-automation@automatewithravi.com |
$MailTo | Recipient address for all milestone notification emails | infra-alerts@automatewithravi.com |
$PatchDepotPath | VMFS path to the ESXi offline bundle ZIP on the shared datastore | /vmfs/volumes/Datastore/Patches/VMware-ESXi-7.0.3-XXXX.zip |
🌟 Benefits
Time Savings
45–60 minutes of manual work per host reduced to a Jenkins trigger. At 10 hosts per month that is a full working day returned to the team.
No Credential Exposure
vCenter passwords are AES-256 encrypted in Jenkins Credentials. They never appear in script files, build logs, console output, or email notifications.
Zerto Replication Safe
VRA evacuation completes and is verified before maintenance mode is set. VPG replication continues on the target VRA with no RPO impact during the patching window.
Intelligent Placement
VMs only migrate to hosts where projected memory stays below 85% and CPU below 95%. No blind vMotion that triggers memory ballooning on a saturated host.
Full Audit Trail
Jenkins records who triggered the build, when, and against which host. The PowerShell transcript is archived as a build artefact for post-incident review.
Milestone Notifications
Email alerts fire at vMotion completion, Zerto evacuation, maintenance mode entry, patch completion, and cluster restoration — full visibility without watching the console.
💡 Pro Tips & Best Practices
🔑 Use a Dedicated vCenter Service Account
Create a dedicated service account such as svc-vsphere@automatewithravi.com with the minimum vCenter roles: Virtual Machine → Migrate, Host → Maintenance, and Cluster → Modify. Never use a personal or domain admin account. When credentials rotate, update one Jenkins Secret Text entry — not hunt through scripts.
📅 Check VPG SLA Status Before Evacuating
Before triggering VRA evacuation, call GET /v1/vpgs and confirm all VPGs on the target host are in MeetingSLA status. Evacuating a VRA when a VPG is already degraded risks losing a recovery point. A quick pre-flight check saves a much longer incident conversation.
🔔 Add a Pre-Run Start Notification
The script sends notifications at completion milestones. Add one at the very start — before any DRS changes — so on-call engineers know a patching run has begun:
Send-Notification `
-Subject ("ESXi Patching STARTED: $FromVMHostName — " + (Get-Date -Format dd-MM-yyyy)) `
-Body "Hi Team,`n`nAutomated ESXi patching has started on $FromVMHostName.`nDRS is being set to Manual and VM migrations will begin shortly.`n`nThis is an automated notification."
⏱️ Adjust Zerto Polling for Larger Environments
The script polls Zerto at 90 seconds then 120 seconds. If your VRA protects many VPGs, evacuation can take longer. Increase the sleep durations or add a third polling attempt before Exit 1 to avoid failing a successful-but-slow evacuation.
📦 GitHub Repository
The complete sanitised script — VMware-ESXi-PrePatching-Automation.ps1 — along with the example Evacuate.json payload and notes on discovering your Zerto VRA identifiers, is published in the VMware automation repository on GitHub.
Clone it, update $vcserver, $ClusterName, $ZertoVraIds, $PatchDepotPath, and the SMTP settings, store your vCenter credentials in Jenkins, place Evacuate.json at C:\Patching\ on the Windows agent, and trigger your first parameterised patching build.
automatewithravi / VMware
VMware-ESXi-PrePatching-Automation.ps1 · Evacuate.json · Jenkins configuration notes
View Script on GitHub →