When vSphere 7 introduced vLCM to replace VUM (VMware Update Manager) it was announced that the Baseline update approach would be deprecated in a later version, in favour of Image-based updates.
We extensively use automation around Baselines, to show a measurable compliance against our quarterly patching cycle. This went something like this:
- On the first day of each calendar quarter (eg 1st Jan, 1st April etc) a script is run against each vCenter to create a new baseline which includes all the latest VMware patches. This is attached to all datacenters, and any baselines older than a year are removed
- Scripts are then run against each vCenter, to apply the new baseline – starting with the Lab environment, and working through in order of criticality, finishing on the production one. For example, the Lab would be done as soon as the baseline is available, then a week later, non-production environments, then DR a week later, and then finally Production a week after DR.
- Reports are run weekly, showing the status of each host against the baselines, enabling us to track the compliance, and the progress of the rollout.
Image-based updates work in quite a different way, so this approach needed some rethinking. The only feasible way seems to be to just measure compliance against the defined image, so that is what I’ve gone with.
The main purpose of this post, is to cover the PowerCLI cmdlets and structures that were used to achieve this, both as an aide-memoir for me, and to share my findings for anyone doing a similar migration.
Checking if a cluster is using Image-based updates
This is a simple one
(Get-Cluster $cluster).CollectiveHostManagementEnabled
returns $true
Updating the Image
I had to use a Try…Catch here to detect whether there was an update to apply
try {
$rec = Get-LcmClusterDesiredStateRecommendation -Current -Cluster $cluster -ErrorAction:Stop
} catch {
Write-Output "Cluster $cluster has no recommended updates"
continue
# move on to the next vCenter
}
$update=Set-Cluster -Cluster $cluster -BaseImage $rec.Image -VendorAddOn $rec.VendorAddOn -Confirm:$false
This only updates the Base Image and Vendor AddOn, I’ve not touched Components, or the Firmware and Drivers Addon at this time.
Testing compliance
Test-LcmClusterCompliance returns an object containing the status of the compliance, and arrays of the hosts matching certain states.
$comp=Get-Cluster -Name $cluster |Test-LcmClusterCompliance
$comp.Status
$comp.CompliantHosts
$comp.NonCompliantHosts
I’ve then used this output to produce a report (and sorry, this is quite verbose!)
$statuses=@()
$statuses+=$comp.NonCompliantHosts | %{ $_ | select VMhost,
@{N="CurrentImage";E={[string]$_.BaseImageCompliance.Status + " " +
$_.BaseImageCompliance.Current.Name + " - " + $_.BaseImageCompliance.Current.Version}},
@{N="CurrentAddOn";E={[string]$_.AddOnCompliance.Status + " " +
($_.AddOnCompliance.Current.Name).Replace("PowerEdge Servers running ","") + " - " +
$_.AddOnCompliance.Current.Version}},
@{N="TargetImage";E={$_.BaseImageCompliance.Target.Name + " - " +
$_.BaseImageCompliance.Target.Version}},
@{N="TargetAddOn";E={($_.AddOnCompliance.Target.Name).Replace("PowerEdge Servers running ","")+
" - " + $_.AddOnCompliance.Target.Version}} }
$statuses+=$comp.CompliantHosts | %{ $_ | select VMhost,
@{N="CurrentImage";E={[string]$_.BaseImageCompliance.Status + " " +
$_.BaseImageCompliance.Current.Name + " - " + $_.BaseImageCompliance.Current.Version}},
@{N="CurrentAddOn";E={[string]$_.AddOnCompliance.Status + " " +
($_.AddOnCompliance.Current.Name).Replace("PowerEdge Servers running ","") + " - " +
$_.AddOnCompliance.Current.Version}},
@{N="TargetImage";E={$_.BaseImageCompliance.Target.Name + " - " +
$_.BaseImageCompliance.Target.Version}},
@{N="TargetAddOn";E={($_.AddOnCompliance.Target.Name).Replace("PowerEdge Servers running ","")+
" - " + $_.AddOnCompliance.Target.Version}} }
This is then formatted into a report like:
Checking when the Image was updated
This is necessary to allow a staged approach through the environments, while keeping the quarterly updates across the estate. It’s not something that I found possible to do via normal LCM cmdlets, and had to dig around in the SDK cmdlets
Anything using the SDK cmdlets has to use the bare moref IDs, which means cropping the object type off the front of the ID
$comp=Invoke-GetClusterSoftwareCompliance -Cluster `
(Get-Cluster $cluster).Id.Replace("ClusterComputeResource-","")
$comp
incompatible_hosts : {}
hosts : @{host-7829=; host-7830=}
non_compliant_hosts : {}
impact : NO_IMPACT
commit : 10
compliant_hosts : {host-7829, host-7830}
scan_time : 13/10/2022 12:18:26
unavailable_hosts : {}
notifications :
host_info : @{host-7829=; host-7830=}
status : COMPLIANT
$commit=Invoke-GetClusterCommitSoftware -Cluster `
(Get-Cluster $cluster).Id.Replace("ClusterComputeResource-","") -commit $comp.commit
author apply_status description commit_time
------ ------------ ----------- -----------
xxxxxx@xxx.xxx APPLIED 13/10/2022 12:30:25
That commit_time is the time the Image was updated
Applying the image – Whole Cluster
This can be done with a one-liner
Get-Cluster -Name $cluster | Set-Cluster -Remediate -AcceptEULA
Applying the image – Individual Host
This is significantly more complicated, but our VMware TAM pointed me in the direction of the SDK cmdlets again for this.
# The SDK cmdlets need the moref ID's trimming
$clusterid=(Get-Cluster $cluster ).Id.Replace("ClusterComputeResource-","")
$vmhostid=(Get-VMHost $vmhost).Id.Replace("HostSystem-","")
# Get the time just before we start the task, so we can filter the Get-Task output
$start = Get-Date
# Create a specification object - you can supply more than one $vmhostid, comma separated
$SettingsClustersSoftwareApplySpec = Initialize-SettingsClustersSoftwareApplySpec -Hosts `
$vmhostid -AcceptEula $true
# Apply the specification object to the cluster
Invoke-ApplyClusterSoftwareAsync -Cluster $clusterid -SettingsClustersSoftwareApplySpec `
$SettingsClustersSoftwareApplySpec
# The apply task runs async, and the output doesn't seem to match to a task id, so now we find the task
$task=Get-Task |?{$_.ObjectId -eq $cluster.Id -and $_.StartTime -gt $start -and $_.Name -eq "apply`$task"}
# Loop until the task finishes
While ($task.State -eq "Running") {
Sleep -Seconds 60
$task=Get-Task |?{$_.ObjectId -eq $cluster.Id -and $_.StartTime -gt $start -and `
$_.Name -eq "apply`$task"}
}
We do things this way so that we can silence alerting for each host while it patches. If that’s not something you bother with, then it’s far simpler to do the one-liner above!