Windows Updates übers vCenter zu installieren ist für mich eine gute Methode um kostengünstig über verschiedene Netzwerke hinweg Windows Updates auf meinen virtuellen Maschinen zu installieren. Damit benötigte ich kein System, was über das Netzwerk überall hinkommt und auch nicht X Agenten oder ähnliches.

Die Realisierung habe ich über Powershell 7 gemacht und mache mir die Funktionalität der VMWare VIX-API zu nutze. Über die VIX-API können Dateien, Befehle, usw. über die VMWare Tools in die Gast-VM „injected“ werden – somit ist keine Netzwerkkonnektivität zu der VM selbst nötig, sondern nur zum vCenter. Zusätzlich dazu braucht man nur, logischerweise, den Namen der VM und Zugangsdaten auf der Gast-VM.

Wichtig zu bemerken ist, dass die Gast-VM natürlich selbst an eine Art Updateserver gelangen muss. Entweder über das Internet, an die von Microsoft, oder an z.B. einen WSUS. Mit unserem Script triggern wir das Update nur, stellen es aber nicht bereit.

Aufbau des Scripts um Windows Updates übers vCenter zu installieren

Ordnerstruktur des Windows Update Scripts

Wie im Bild zu sehen, besteht die Scriptumgebung für das Windows Updates übers vCenter installieren aus 3 Ordnern und einem Script. Im Ordner „config“ befindet sich eine JSON-Konfigurationsdatei. Diese kann mit dem Editor oder z.B. VSCode geöffnet und angepasst werden.

{
    "patchgroups": {
        "pg-ad-01": {
            "vm": [
                "ADC0001",
                "ADC0002"
            ],
            "username": "svc-update@local.loc",
            "password": "passhash"
        },
        "pg-pki-01": {
            "vm": [
                {
                    "name": "PKI0002",
                    "username": "svc-update@local.loc",
                    "password": "passhash"
                },
                {
                    "name": "PKI0001",
                    "username": "Administrator",
                    "password": "passhash"
                }
            ],
            "username": "svc-powershell-runas@vop.loc",
            "password": "passhash"
        }
    },
    "general": {
        "notificationemail": "notifications@local.de",
        "autoreboot": "true"
    }
}

Ich organisiere VMs in sogenannten Patchgruppen. Also Gruppen von VMs, die einen Bestimmten Dienst bereitstellen oder einen Service darstellen. Diese benenne ich pg-xxx-01, also patchgroup-funktion-nummer. Diese Benennung findet sich bei mir in den Systemen überall wieder – unter anderem auch im Monitoring über PRTG als Tags an den Geräten. So stelle ich den Zusammenhang zwischen den Systemen her.

Unterhalb der jeweiligen Patchgruppe können eine oder mehrere VMs stehen. Wie im Code oben zu sehen kann man zum einen mehrere VMs angeben, die die gleichen Anmeldedaten haben, oder auch im 2. Abschnitt mehrere VMs mit individuellen Anmeldedaten. Unter „General“ sind noch die E-Mail Adresse für die Benachrichtigungen und ein Schalter, ob die VMs automatisch neustarten sollen, oder nicht.

Es findet sich noch der Ordner logs in der Scriptumgebung. Dieser wird mit den Logs der einzelnen Updateläufe gefüllt. Somit kann nachvollzogen werden, welche Updates wo installiert wurden. Diese Logs werden aber auch an die angegebene E-Mail Adresse versandt.

Im Ordner resources ist das Powershell-Modul PSWindowsUpdate zu finden. Das wird immer zu beginn des Updatelaufs auf das Ziel kopiert. Somit muss der Gast nicht unbedingt Internetzugang haben.

Aufruf und die Script-Parameter *Trommelwirbel*

Der Aufruf des Scripts erfolgt mit

./Install-WindowsUpdates.ps1 -patchgroup xyz

Es kann, wenn nur eine VM aus einer Patchgruppe aktualisiert werden soll zusätzlich der Parameter -vm mit dem VM-Namen angegeben werden:

./Install-WindowsUpdates.ps1 -patchgroup xyz -vm VMxyz

Für die Erstellung des Passhashs für die Config startet man das Script so:

./Install-WindowsUpdates.ps1 -buildCredentials

Damit wird auf der Console das Passwort abgefragt und man erhält den Passhash zurück, den man in die Config kopieren kann. Wichtig: Führe das Script mit dem User aus, mit dem du den Passhash erstellt hast – sonst klappt es nicht. Als letztes, bevor du das Script startest, musst du die globalen Parameter anpassen. Öffne dazu das Script und ändere die folgenden Zeilen:

$prtgServerExists = $true
$fromMail = "test@local.de"
$smtpServer = ""
$vCenterURL = 'vcn0001.local.loc'
$PRTGServerURL = 'prtg.local.loc'
[string]$userName = 'prtgadmin'
[string]$userPassword = 'xxxxxx'

Falls du keinen PRTG Server hast, ändere den Parameter auf $false. $fromMail ist die Absenderadresse für die Benachrichtigungen. $smtpServer dein E-Mail Server. vCenter und PRTG entsprechend die FQDNs eintragen. Username und userPassword sind die Daten für PRTG. Falls du kein PRTG hast, lass die Felder einfach leer.

Hier das Script zum lesen und rauskopieren. Weiter unten findest du noch den Download des ganzen Verzeichnisses

Windows Updates übers vCenter – Script zum Kopieren

# 1. Bei einem neuen System bzw. einer neuen Patchgruppe, erstelle zunächst die Konfiguration anhand der vorhandenen unterhalb von Patchgroups in der JSON unter config/windowsUpdate.json.
# 2. Erstelle einen Passwortstring mit Install-WindowsUpdates.ps1 -buildCredentials für die hinzuzufügenen Systeme.
# 3. Kopiere den erstellten Passwort-String bei der jeweiligen Patchgruppe unter config/windowsUpdate.json in Password.
# 4. Stelle sicher, dass die Ziel-VM eingeschaltet ist und VMWare Tools laufen.
# 5. Starte das Script mit Install-WindowsUpdates.ps1 -patchgroup <Patchgruppe>
# 6. Benachrichtigungen werden an die in der Config angegebene E-Mail Adresse gesendet. Behalte das Postfach im Auge um benachrichtigt zu werden.
# INFO: Standardmäßig werden die Dienste vor dem Updatedurchlauf ausgelesen und nach dem Updateablauf erneut.
# Beide ausgelesenen Datensätze müssen identisch sein. Dafür wird der Test am Ende des Patchdurchlaufs jede Minute für maximal 15 Minuten wiederholt. Es sei denn, die erwarteten Dienste sind vorher schon wieder gestartet.
# Erstellte Snapshots werden nicht automatisch gelöscht.
# Es wird pro Scriptausführung nur ein Updatelauf durchgeführt. Bei frisch installierten Systemen kann es daher notwendig sein, das Script erneut auszuführen um wirklich alle Updates zu installieren.
# Bei Bestandssystemen, die schon mal Updates erhalten haben, ist aufgrund der Microsoft-Regelung mit Cumulative Updates ein einzelner Updatedurchlauf ausreichend.

[CmdletBinding()]
param (
    [Parameter()]
    [string[]]
    $patchgroup,
    [string[]] $vm,
    [switch] $buildCredentials
    
)
$prtgServerExists = $true
$fromMail = "test@local.de"
$smtpServer = ""
$vCenterURL = 'vcn0001.local.loc'
$PRTGServerURL = 'prtg.local.loc'
[string]$userName = 'prtgadmin'
[string]$userPassword = 'xxxxxx'

if($prtgServerExists){
    Import-Module PRTGAPI
    }
if ($buildCredentials) {
    [string]$vm_password = Read-Host -Prompt "VM password" -AsSecureString | ConvertFrom-SecureString
    [string]$new_secrets_json = @{vm_password_encrypted = $vm_password } | ConvertTo-Json
    Write-Host ("please copy the encrypted password string to the config json at key password`n`n" + $new_secrets_json)
    exit
}
if($patchgroup -eq $null){
    Write-Host 'To Start Windows Patching run .\Install-WindowsUpdates.ps1 -patchgroup pg-xx-01. You can specify single vms from a patchgroup with parameter -vm vm1,vm2,etc.' -ForegroundColor Yellow
    Write-host ''
    Write-Host 'Example: .\Install-WindowsUpdates.ps1 -patchgroup pg-kms-01 -vm KMS0003' -foregroundcolor yellow
    Write-Host ''
    Write-Host 'Hint: Ensure that you are using the same user for running this shell, as you used to build the credentials string' -ForegroundColor Magenta
    exit(0)
}
#Functions
function resume-prtg ($Patchgruppe, $PRTGServer) {
    #Resume and Refresh Devices and Sensors
    $pauseddevices = Get-device -Tag $Patchgruppe | where { $_.Status.ToString() -like 'Paused*' -and $_.Message -like 'Infrastrukturwartung*' } 
    $pauseddevices | Resume-Object
    sleep 60
    $pauseddevices | Refresh-Object
    sleep 60
    $pausedsensors = Get-Sensor -Tag $Patchgruppe | where { $_.Status.ToString() -like 'Paused*' -and $_.Message -like 'Infrastrukturwartung*' }
    $pausedsensors | Resume-Object
    sleep 60
    $pausedsensors | Refresh-Object
    }

    function pause-prtg ($Patchgruppe) {  
    #Pause Devices and Sensors
    Get-Sensor -Tag $Patchgruppe | where { $_.type.stringvalue -eq 'businessprocess' } | Pause-Object -Duration 180 -Message "Infrastrukturwartung"
    sleep 30
    Get-device -Tag $Patchgruppe | Pause-Object -Duration 180 -Message "Infrastrukturwartung"    
    }
if($prtgServerExists){
#PRTG Credentials

# Convert to SecureString
[securestring]$secStringPassword = ConvertTo-SecureString $userPassword -AsPlainText -Force
[pscredential]$prtgcreds = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword)
#Connections
Write-Host "Connect to PRTG Server" -ForegroundColor Yellow
$PRTGServer = Connect-PrtgServer -server $PRTGServerURL -Credential $prtgcreds -force
}
#Global Variables
Write-Host "Login to vCenter-Server $vCenterURL" -ForegroundColor Yellow
$vcenter = Connect-VIServer -Server $vCenterURL
#Read from Config
Write-Host "Read Configs from JSON" -ForegroundColor Yellow
$jsondata = Get-Content "config\windowsUpdate.json" | Out-String | ConvertFrom-Json
$notificationemail = $jsondata.general.notificationemail
$autoReboot = $jsondata.general.autoreboot
$testcommand = $jsondata.test.command
$testresult = $jsondata.test.result
$username = ($jsondata.patchgroups.$patchgroup).username
$password = ($jsondata.patchgroups.$patchgroup).password | ConvertTo-SecureString
if ($vm -ne $null) {
    $vms = $vm
}
else {
    $vms = ($jsondata.patchgroups.$patchgroup).vm
}
 ##PRTG Pause
 if($prtgServerExists){
 Write-Host "Pause PRTG Sensors and Devices" -ForegroundColor Yellow
 pause-prtg $patchgroup
}
$Job = $vms | ForEach-Object -Parallel { 
    "Update-Run for $_ :"
    #�bergabe Parameter
    $vcenter = $using:vcenter
    $PRTGServer = $using:PRTGServer
    $notificationemail = $using:notificationemail
    $autoReboot = $using:autoReboot
    $testcommand = $using:testcommand
    $testresult = $using:testresult
    $username = $using:username
    $password = $using:password
    $vmname = $_
    "Get VM with name $vmname"
    $vm = Get-VM -name $vmname -Server $vcenter
    if($vm -eq $null){
        $vmname +"could not be found in vcenter"
        exit(0)
    }
    #Remove old files
    Remove-Item -Path .\logs\$($vm.name)*
    #Prepare VM
    "Enabling TLS 1.2"
    $tls12 = Invoke-VMScript -VM $vm -ScriptText "[System.Net.ServicePointManager]::SecurityProtocol = 'TLS12'" -GuestUser $username -GuestPassword $password
    "Copying PSWindowsUpdate Module"
    Copy-VMGuestFile -Source .\resources\PSWindowsUpdate -Destination "C:\Program Files\WindowsPowerShell\Modules\PSWindowsUpdate" -VM $vm -LocalToGuest -GuestUser $username -GuestPassword $password -force
    #Searching for Updates
    "Searching for Updates..."
    $SearchForUpdates = Invoke-VMScript -VM $vm -ScriptText "Get-WUList | Convertto-JSON" -GuestUser $username -GuestPassword $password -ToolsWaitSecs 600
    # If Updates were found
    if (($SearchForUpdates.ScriptOutput | ConvertFrom-JSON).Length -gt 0) {
        "Found " + ($SearchForUpdates.ScriptOutput | ConvertFrom-JSON).Length + "Updates"
    
        ##Setting up Log File
        $currentDate = Get-Date -Format FileDateTime
        $searchLogfile = ".\logs\$vmname-foundUpdates-$currentDate.log"
        $SearchForUpdates.ScriptOutput | ConvertFrom-JSON | Out-File $searchLogfile -force
        ##Creating Scnapshot
        "Creating Snapshot..."
        $snapshot = New-Snapshot -VM $vm -Name "$vmname - Infrastrukturwartung" -Description "Created by patch-script from Script"
    
        #Setting up Logfile
        $installDate = Get-Date -Format FileDateTime
        $installLogfile = "logs\$vmname-installedUpdates-$installDate.log"
        #Start installation
        "Start installing Updates..."
        $InstallUpdates = Invoke-VMScript -VM $vm -ScriptText "Get-WUInstall -Download -AcceptAll -Install -IgnoreReboot" -GuestUser $username -GuestPassword $password -ToolsWaitSecs 1800 -ErrorVariable installFailure | Out-File $installLogfile -force
        
        #Set Autoreboot
        if ($autoreboot -eq "true") {
            $rebooted = $true
        }
        else { $rebooted = $false }
    

        ##Retry on Failure
        if ($installFailure) {
            "Update installation failed. Retrying"
            Start-Sleep -Seconds 30
            if ($autoreboot -eq "true") {
                #Setting up Logfile
                $installDate = Get-Date -Format FileDateTime
                $installLogfile = "logs\$vm-installedUpdates-$installDate.log"
                #Start installation
                "Start installing Updates..."
                $InstallUpdates = Invoke-VMScript -VM $vm -ScriptText "Get-WUInstall -Download -AcceptAll -Install -IgnoreReboot" -GuestUser $username -GuestPassword $password -ToolsWaitSecs 1800 -ErrorVariable stillinstallFailure | Out-File $installLogfile -force
        
                #Set Autoreboot
                if ($autoreboot -eq "true") {
                    $rebooted = $true
                }
                else { $rebooted = $false }
           
                if ($stillinstallFailure) {
                    #If installation still fails, notification mail will be send.
                    Send-MailMessage -Attachments $searchLogfile, $installLogfile -Body "Updates auf der VM $($vm.name) wurden NICHT durchgeführt. Während der Installation sind Fehler aufgetreten." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update Failure Notification"
                    exit(1)
                }
            }
        }
        if ($rebooted -eq $false) {
            #If Reboot is false, end script here.
            "Update installation finished. Reboot is pending"
            Send-MailMessage -Attachments $searchLogfile, $installLogfile -Body "Updates auf der VM $($vm.name) wurden durchgeführt. Gefundene und Installierte Updates siehe Anhänge. Der Reboot kann jetzt manuell durchgeführt werden - Die PRTG Sensoren sind weiterhin pausiert." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update Notification"
        }
        if ($rebooted) {
            #If VM should reboot it shuts of and powers back on
            "Update Installation Finished. Waiting for GuestOS to Reboot."
            Shutdown-VMGuest -VM $vm -Confirm:$false
            while ($vm.PowerState -eq 'PoweredOn') {
                Start-Sleep -Seconds 10
                $vm = Get-VM -Name $vmname -Server $vcenter
            }
            Start-Sleep -Seconds 25
            Start-VM -VM $vm -Server $vcenter
            Start-Sleep -Seconds 15
            #Waiting for Tools to come back as a first indicator the OS is booted up
            "Wait for VMWare Tools... "
            $toolsStatus = Wait-Tools -VM $vm -TimeoutSeconds 1800 -Server $vcenter -ErrorVariable notOnline
            Start-Sleep -Seconds 30
            if ($notOnline) {
                #if Tools aren´t back online after 1800 Seconds, Notification will be send.
                "VMWare tools are not responding... Sending Mail notification" 
                Send-MailMessage -Attachments $searchLogfile, $installLogfile -Body "Updates auf der VM $($vm.name) wurden durchgeführt. Die VM ist jedoch nach 30 Minuten noch nicht wieder Online. Bitte nachprüfen.." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update Failure Notification"    
            }
            else {
                $success = $false
                #Execute given Testcommand 15 Times (once every minute) to check if Guest OS is completly up and running
                for ($i = 1; $i -lt 15; $i++) {
                    try {
                        $runningServices = Invoke-VMScript -VM $vm -ScriptText "get-service | Where-Object Status -eq 'Running' | ConvertTo-Json" -GuestUser $username -GuestPassword $password -ToolsWaitSecs 600
                        $runningServices = $runningServices.ScriptOutput | ConvertFrom-Json
						$result = Compare-Object -ReferenceObject $runningServicesPreUpdate -DifferenceObject $runningServices
                        if (!$result) {
                            $success = $true
                            Write "Testcommand successful - All pre Reboot running Services are running again`n" 
                            break;
                        }
                        Start-Sleep -Seconds 60
                    }
                    catch {
                        Start-Sleep -Seconds 60
                    }
                    finally {
                        Write "Try Test-command Retry $i/15`n"
                    }
                }
            
                if ($success) {
                    #If all is done send notification mail
                    Write "VMWare Tools are back online & Test Command executed successfully - sending notification message`n"
                    Send-MailMessage -Attachments $searchLogfile, $installLogfile -Body "Updates auf der VM $($vm.name) wurden durchgef�hrt. Gefundene und Installierte Updates siehe Anh�nge. Der Reboot wurde automatisch durchgef�hrt." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update Notification"
                }
                else {
                    #if Test is unsuccessful send information to notification email
                    Write "Test unsuccessful - rollback to snapshot if needed`n"
                    Send-MailMessage -Attachments $searchLogfile, $installLogfile -Body "Der Success-Test auf der VM $($vm.name) konnte nach 15 Minuten nicht erfolgreich durchgef�hrt werden. Folgende Dienste sind noch nicht gestartet: $($result.Inputobject.ServiceName). Bitte manuell pr�fen und ggf. Snapshot zur�ckspielen." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update Failure Notification"
                }
            
            }
        }
    }
    else {
        #if no updates were found, send notification mail
        "No Updates found on $($vm.name)"
        Send-MailMessage -Body "Es wurden keine Updates auf der VM $($vm.name) gefunden." -From $fromMail -to $notificationemail -SmtpServer $smtpServer -Subject "Windows Update No Updates Found Notification"    
    }
    sleep 1 
} -ThrottleLimit 5 -AsJob
sleep 5
#Wait until Running jobs aren´t running anymore
while (((Get-Job -id $Job.Id).State) -eq 'Running') {
    
    #Output current output of childjobs for debugging
    foreach ($child in $job.ChildJobs) {
        Write-Host $child.Output -ForegroundColor Green
        if($child.error){
            Write-Host $child.Error -ForegroundColor Red
        }
    }
    Write-Host "Job still running..." -ForegroundColor Yellow
    Start-Sleep -Seconds 60
}
#Resume PRTG
if($prtgServerExists){
Write-Host "All Done - Resuming PRTG Sensors & Devices" -ForegroundColor Green
resume-prtg $patchgroup
}
foreach ($child in $job.ChildJobs) {
        Write-Host "Errors in Jobs: " $child.error -ForegroundColor Red
    }
#Remove Jobs
Get-Job -id $job.id | Remove-Job
Disconnect-VIServer -Confirm:$false
if($prtgServerExists){
Disconnect-PrtgServer
}

Windows Updates Script zum Download (Entpacken mit 7Zip)

Erklärung zum Script

Nach dem Start wird der Connect zum vCenter und ggf. zum PRTG hergestellt. Im PRTG wird nach dem Tag gesucht, was der Patchgruppe entspricht und alle verbundenen Sensoren pausiert.

Nun wird für jede VM, die für den Scriptlauf in Frage kommt ein Job gestartet. Der Updatelauf geschieht also Parallel. Für jede VM wird zunächst das PS Modul kopiert, danach TLS 1.2 aktiviert (benötigt für das Modul) und anschließend werden Updates gesucht. Werden Updates gefunden, die installiert werden können, wird für die VM ein Snapshot erstellt. Danach werden die Updates installiert und je nach Konfiguration entweder automatisch neugestartet, oder es gibt nur eine Benachrichtigung, dass nun neugestartet werden könnte.

Wenn man autoreboot auf false stehen hat endet das Script hier schon fast – es wird nur das PRTG, falls vorhanden fortgesetzt und die entsprechende Mail versendet.

Wenn autoreboot auf true steht starten die VMs nach der Updateinstallation neu. Hierbei ist wichtig zu wissen, dass das Script sich zuvor alle Dienste, die liefen, gemerkt hat – denn nun, nach dem Neustart, wird das Script 15 Minuten lang, alle 60 Sekunden die Dienste der VM abfragen und wartet darauf, dass die „Laufende Dienste nach Reboot“ = „Laufende Dienste vor Reboot“ entspricht.

Damit kann sichergestellt werden, dass die Updates fertig sind und das System wieder hochgefahren und einsatzbereit ist. Sollte das nach 15 Minuten nicht klappen, wird eine Mail ausgelöst, die auf diesen Zustand hinweist und auffordert manuell zu kontrollieren.

Nun wird ggf. PRTG fortgesetzt und die finale Mail rausgesendet.

Wem die 15 Minuten zu knapp sind kann diese im Script auch erhöhen in Zeile 241

for ($i = 1; $i -lt 15; $i++) {

Einfach statt 15 auf eine andere Zahl ändern.