🚀 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.

💡
Key Security Design All credentials are injected via Jenkins environment variables using the Credentials Binding plugin. vCenter passwords never appear in script files, build logs, or email notifications. The script reads them from $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.

⚠️
Zerto VRA Identifiers are environment-specific. Before running the script, authenticate to your ZVM and call 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

🔵 Phase 1 — Pre-Patching & VM Evacuation
1

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.

PowerShellvCenter connection validation
# 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
2

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.

PowerShellSet DRS to Manual
$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
}
3

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.

PowerShellDisable non-Zerto DRS rules
# 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
4

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.

💡
Projected memory check — not current state. The script adds the VM's configured memory ($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.
PowerShellThreshold-aware vMotion loop
# 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
    }
}
🟠 Phase 2 — Maintenance Mode Entry & ESXCLI Patching
5

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.

6

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.

PowerShellESXCLI profile update + reboot + ping-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
🟢 Phase 3 — Post-Patch Verification & Cluster Restoration
7

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.

PowerShellPost-patch verification + DRS restore + exit maintenance
# 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.

PowerShellZerto session authentication function
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:

PowerShellList VRA identifiers from your ZVM — run once to populate $ZertoVraIds
$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

PowerShell$ZertoVraIds hashtable — replace values with output from above
$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.

PowerShellEvacuation POST + dual-poll task status check
# 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.

JSONC:\Patching\Evacuate.json — automatic Zerto placement (recommended)
{
  "vmsAllocations": [
    {
      "hostIdentifier": null,
      "vmIdentifier"  : null
    }
  ]
}
💡
What null means here. Setting both 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.

JSONEvacuate.json — directed evacuation to a specific host (optional)
{
  "vmsAllocations": [
    {
      "hostIdentifier": "host-identifier-from-zerto-api",
      "vmIdentifier"  : null
    }
  ]
}
⚠️
File must exist before the build runs. The script loads this file with 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

PowerShellLoading Evacuate.json in the script
# 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.

1

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.

2

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.

3

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".

4

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.

5

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.

Jenkins Build StepWindows PowerShell
Set-ExecutionPolicy Bypass -Scope Process -Force
& "C:\Patching\VMware-ESXi-PrePatching-Automation.ps1"
6

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.

7

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.

8

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.

un. Go to Build with Parameters, type the FQDN of the ESXi host to patch, and click Build. Console output streams live. The transcript is archived as a build artefact. Email notifications arrive at vMotion completion, Zerto evacuation, maintenance mode entry, patching, and cluster restoration.

📋 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

ParameterTypeDescriptionRequired?
FromVMHostNameStringFQDN 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
UsernameSecretvCenter service account name — injected via Credentials Binding plugin, never stored in plain textSECRET
PasswordSecretvCenter account password — injected via Credentials Binding plugin, masked in all build logs and console outputSECRET

Script Configuration Block — Update Once Per Environment

VariableDescriptionExample Value
$vcservervCenter Server FQDN or IP addressvcenter.yourdomain.local
$ClusterNameDRS cluster name containing the ESXi hosts to patchCluster01
$MaxMemAllowedMemory utilisation % ceiling for destination host selection during vMotion85
$strZVMIPIP address of the Zerto Virtual Manager172.16.x.x
$strZVMPortZerto REST API port — default is 96699669
$ZertoVraIdsHashtable mapping ESXi host FQDN to its Zerto VraIdentifier. Run GET /v1/vras on your ZVM to discover values.See Section 5
$SmtpServerInternal SMTP relay IP or hostname reachable from the Jenkins Windows agent172.0.x.x
$MailFromSender address for all milestone notification emailsvmware-automation@automatewithravi.com
$MailToRecipient address for all milestone notification emailsinfra-alerts@automatewithravi.com
$PatchDepotPathVMFS 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:

PowerShellAdd at start of Phase 1
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.

🚫
Never skip the DRS-to-Manual step. With DRS in Fully Automated mode the cluster can move a VM back onto the host you are draining — creating an evacuation loop. The Phase 1 DRS-to-Manual step must always run before any vMotion begins. The script enforces this with an Exit 1 gate if the cluster name does not match or DRS is not enabled.

📦 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 →
← Back to Tech Blogs More VMware Posts →