Windows Updates installieren/automatisieren

Dieser Eintrag ist länger geworden als ich dachte. Zunächst wollte ich nur grob beschreiben wie man Windows Updates unter Windows 2008 und 2008 R2 installiert (funktioniert auch bei 2016, ist aber umständlicher als nötig). Jedoch führte eins zum anderen… naja und letztendlich habe ich das komplette Automatisierungs-Skript drin gehabt. Aber fangen wir mal von vorne an:

Um Windows Updates zu installieren kann folgendes Skript genutzt werden. Als Parameter übergibt man wohin die Logfiles geschrieben werden sollen.

param(
	$UpdateStatusLog,
	$ErrorLog
)

"Start" | Out-File $UpdateStatusLog

$Criteria = "IsInstalled=0 and Type='Software'"
$Searcher = New-Object -ComObject Microsoft.Update.Searcher

try {
	"Search" | Out-File $UpdateStatusLog
	$SearchResult = $Searcher.Search($Criteria).Updates
	$UpdateCount = $SearchResult.Count
	if ($UpdateCount -eq 0) {
		"NoUpdates" | Out-File $UpdateStatusLog
	}else{
		"$UpdateCount" | Out-File $UpdateStatusLog
		$Session = New-Object -ComObject Microsoft.Update.Session
		$Downloader = $Session.CreateUpdateDownloader()
		$Downloader.Updates = $SearchResult
		"Download" | Out-File $UpdateStatusLog
		$Downloader.Download()
		$Installer = New-Object -ComObject Microsoft.Update.Installer
		$Installer.Updates = $SearchResult
		"Install" | Out-File $UpdateStatusLog
		$Result = $Installer.Install()
		if($Result.rebootRequired){
			"Reboot" | Out-File $UpdateStatusLog
		}
	}
}catch{
	Get-Date | Out-File $ErrorLog -Append
	$error | Out-String | Out-File $ErrorLog -Append
	"Fehler" | Out-File $UpdateStatusLog
	"---" | Out-String | Out-File $ErrorLog -Append
}

Es ist zu beachten, dass man bis Server 2008 R2 Updates nicht per Remote-Call auf entfernten Systemen installiert werden können. Das geht erst ab Server 2012. Wer also auf einem Server 2008 oder 2008 R2 Updates remote installieren möchte, muss einen Trick anwenden:

  • Das Skripte auf dem Zielsystem verfügbar machen
  • Auf dem Zielsystem einen neuen Task erstellen
  • Den Task triggern

Das Tool um einen neuen Task (auch auf remote Maschinen) zu erstellen ist in Windows integriert und heißt „schtasks.exe“. Hier müssen einige Parameter übergeben werden:

# Session für Remote PowerShell erzeugen

     $Username = "Domain\Benutzer"
     $Password = "Passwort"
 $Computername = "server01.domain.local"

$PlainPassword = $Password
     $Password = $Password | ConvertTo-SecureString -AsPlainText -Force
  $Credentials = New-Object System.Management.Automation.PSCredential ($Username, $Password)

$SessionOption = New-PSSessionOption -OperationTimeout 7200000 -IdleTimeout 7200000 -NoMachineProfile
      $Session = New-PSSession -ComputerName $Computername -Credential $Credentials -SessionOption $SessionOption

    $TaskUser = $Username
$TaskPassword = $PlainPassword
     $RunUser = $Username
 $RunPassword = $PlainPassword

$TaskFile = "C:\scripts\WindowsUpdates\Install-WindowsUpdates.ps1"

# Der eigentlich Taskname ist etwas tricky, da viele Anführungszeichen gebraucht werden
$TaskName = "PS24_Install-WindowsUpdates.ps1"
$TaskRun = 'PowerShell.exe -ExecutionPolicy Unrestricted -command \"& '
$TaskRun += "'"
$TaskRun += $TaskFile
$TaskRun += "' '"
$TaskRun += $UpdateStatusLog
$TaskRun += "' '"
$TaskRun += $ErrorLog
$TaskRun += "'"
$TaskRun += '\"'

# Start-Tag definieren
# Da der Task NUR manuell getriggert wird, liegt der Tag in der Vergangenheit, so startet der Task nicht ausversehen
$SD = (Get-Date).AddDays(-1).ToString("dd/MM/yyyy")

# Beliebige Uhrzeit, da eh in der Vergangenheit
$ST = "12:00"

Write-Output "Neuen Task erstellen"
# Neuen Task erstellen
Invoke-Command -Session $Session -Script {
	param($RunUser,$RunPassword,$SD,$ST,$TaskName,$TaskRun)
	C:\Windows\System32\schtasks.exe /Create /RU $RunUser /RP $RunPassword /SC Once /SD $SD /ST $ST /TN $TaskName /TR $TaskRun /RL HIGHEST
} -Argumentlist $RunUser,$RunPassword,$SD,$ST,$TaskName,$TaskRun
sleep(5)

Write-Output "Task ausführen"
# Task ausführen
Invoke-Command -Session $Session -Script {
	param($TaskName)
	C:\Windows\System32\schtasks.exe /Run /I /TN $TaskName
} -Argumentlist $TaskName

So, langsam wird es interessant. Wenn wir also schon geskriptete Remote-Tasks erstellen und dadurch Updates installieren, sollten wir vielleicht auch schauen, dass das Skript Install-WindowsUpdates immer aktuell ist. Hierfür können wir wie folgt vorgehen:

  • Text des Skripts als Variable speichern
  • Definieren wo es auf dem Zielsystem hingelegt werden soll
  • Durch einen Invoke-Command rüber pipen, damit wir nicht über CIFS o.ä. gehen müssen, sondern alles innerhalb unserer Remote PowerShell läuft.
$InstallWindowsUpdatesScript = @'
	param(
	 $UpdateStatusLog,
	 $ErrorLog	
	)

	"Start" | Out-File $UpdateStatusLog

	$Criteria = "IsInstalled=0 and Type='Software'"
	$Searcher = New-Object -ComObject Microsoft.Update.Searcher

	try{
	 "LocalSearch" | Out-File $UpdateStatusLog
	 $SearchResult = $Searcher.Search($Criteria).Updates
	 $UpdateCount = $SearchResult.Count
	 if ($UpdateCount -eq 0) {
	  "NoUpdates" | Out-File $UpdateStatusLog
	 }else{
	  "$UpdateCount" | Out-File $UpdateStatusLog
	  $Session = New-Object -ComObject Microsoft.Update.Session
	  $Downloader = $Session.CreateUpdateDownloader()
	  $Downloader.Updates = $SearchResult
	  "Download" | Out-File $UpdateStatusLog
	  $Downloader.Download()
	  $Installer = New-Object -ComObject Microsoft.Update.Installer
	  $Installer.Updates = $SearchResult
	  "Install" | Out-File $UpdateStatusLog
	  $Result = $Installer.Install()
	  if($Result.rebootRequired){
	   "Reboot" | Out-File $UpdateStatusLog
	  }else{
		"Completed" | Out-File $UpdateStatusLog
	  }
	 }
	}catch{
	 Get-Date | Out-File $ErrorLog -Append
	 $error | Out-String | Out-File $ErrorLog -Append
	 "Fehler" | Out-File $UpdateStatusLog
	 "---" | Out-String | Out-File $ErrorLog -Append
	}
'@

$ClientScriptsPathWindowsUpdates = $ClientScriptsPath + "\WindowsUpdates"
$ClientScriptsPathWindowsUpdatesScriptName = "Install-WindowsUpdates.ps1"
$ClientScriptsPathWindowsUpdatesScriptFile = $ClientScriptsPathWindowsUpdates + "\" + $ClientScriptsPathWindowsUpdatesScriptName

     $Username = "Domain\Benutzer"
     $Password = "Passwort" | ConvertTo-SecureString -AsPlainText -Force
 $Computername = "server01.domain.local"
  $Credentials = New-Object System.Management.Automation.PSCredential ($Username, $Password)

$SessionOption = New-PSSessionOption -OperationTimeout 7200000 -IdleTimeout 7200000 -NoMachineProfile
      $Session = New-PSSession -ComputerName $Computername -Credential $Credentials -SessionOption $SessionOption

# Das Skript zum Installieren rüber kopieren...
Write-Output "Skript rüber kopieren"
Invoke-Command -Session $Session -Script {
	param($ClientScriptsPathWindowsUpdatesScriptFile,$InstallWindowsUpdatesScript) `
	Set-Content -Path $ClientScriptsPathWindowsUpdatesScriptFile -Value $InstallWindowsUpdatesScript
} -Argumentlist $ClientScriptsPathWindowsUpdatesScriptFile,$InstallWindowsUpdatesScript


Zum Schluss kann man sich dann noch beliebig austoben… was ich auch getan habe. Es folgt das fertige Skript, welches bei euch garantiert noch Anpassungen braucht. Es beinhaltet u.a. folgende Parameter:

  • Computernamen, Username, Passwort
  • Ziel-Pfad für das Installations-Skript
  • Telefon- und SMS-Nummer
  • Pfad zu 3rd-Party Tool welches anrufen und SMS schicken kann

Zudem beachtet es u.a. folgende Themen:

  • Max. Reboot-Cycles die durchlaufen werden dürfen
  • Max. Zeit die das Installieren dauern darf
  • Was passiert wenn der PC/Server beim Reboot hängen bleibt
  • Alarmierung per 3rd Party Tool

Es setzt zudem voraus, dass pskill.exe von Systeninternals PSTools im Pfad „C:\Tools\PSTools\pskill.exe“ parat liegt (Pfad kann angepasst werden). Das Skript selbst ist sauber kommentiert, sodass ihr euch zurecht finden solltet 🙂 Viel Spaß!

param(
	[Parameter(Mandatory=$true)][String][ValidateNotNullOrEmpty()]$Computername,
	[Parameter(Mandatory=$true)][String][ValidateNotNullOrEmpty()]$Username,
	[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()]$Password,
	[String]$ClientScriptsPath = "C:\scripts\ps24_automation",
	[String]$PhoneNumber,
	[String]$SMSNumber,
	[String]$PathToMakePhoneCall,
	[String]$PathToSendSMS
)

$InstallWindowsUpdatesScript = @'
	param(
	 $UpdateStatusLog,
	 $ErrorLog	
	)

	"Start" | Out-File $UpdateStatusLog

	$Criteria = "IsInstalled=0 and Type='Software'"
	$Searcher = New-Object -ComObject Microsoft.Update.Searcher

	try{
	 "LocalSearch" | Out-File $UpdateStatusLog
	 $SearchResult = $Searcher.Search($Criteria).Updates
	 $UpdateCount = $SearchResult.Count
	 if ($UpdateCount -eq 0) {
	  "NoUpdates" | Out-File $UpdateStatusLog
	 }else{
	  "$UpdateCount" | Out-File $UpdateStatusLog
	  $Session = New-Object -ComObject Microsoft.Update.Session
	  $Downloader = $Session.CreateUpdateDownloader()
	  $Downloader.Updates = $SearchResult
	  "Download" | Out-File $UpdateStatusLog
	  $Downloader.Download()
	  $Installer = New-Object -ComObject Microsoft.Update.Installer
	  $Installer.Updates = $SearchResult
	  "Install" | Out-File $UpdateStatusLog
	  $Result = $Installer.Install()
	  if($Result.rebootRequired){
	   "Reboot" | Out-File $UpdateStatusLog
	  }else{
		"Completed" | Out-File $UpdateStatusLog
	  }
	 }
	}catch{
	 Get-Date | Out-File $ErrorLog -Append
	 $error | Out-String | Out-File $ErrorLog -Append
	 "Fehler" | Out-File $UpdateStatusLog
	 "---" | Out-String | Out-File $ErrorLog -Append
	}
'@

$ClientScriptsPathWindowsUpdates = $ClientScriptsPath + "\WindowsUpdates"
$ClientScriptsPathWindowsUpdatesScriptName = "Install-WindowsUpdates.ps1"
$ClientScriptsPathWindowsUpdatesScriptFile = $ClientScriptsPathWindowsUpdates + "\" + $ClientScriptsPathWindowsUpdatesScriptName
$UpdateStatusLog = $ClientScriptsPathWindowsUpdates + "\UpdateStatus.Log"
$ErrorLog = $ClientScriptsPathWindowsUpdates + "\Error.log"

$PlainPassword = $Password
$Password = $Password | ConvertTo-SecureString -AsPlainText -Force
$Credentials = New-Object System.Management.Automation.PSCredential ($Username, $Password)

$SessionOption = New-PSSessionOption -OperationTimeout 7200000 -IdleTimeout 7200000 -NoMachineProfile
$Session = New-PSSession -ComputerName $Computername -Credential $Credentials -SessionOption $SessionOption

$CountUpdateCycles = 0
$MaxUpdateCycles = 5

$InitialTime = Get-Date
$MaxOverallTime = 3 # Hours

# Variablen leeren
$problem = $Null
$success = $Null
$UpdateCount = $Null # Darf nich die Zahl Null "0" sein, weil in der While danach geschaut wird.
$TotalUpdateCount = 0

# Ordner erstellen bzw. leeren
Write-Output "Zunächst den Ordner erstellen bzw. leeren"
Invoke-Command -Session $Session -Script {
	param($ClientScriptsPathWindowsUpdates)
	if(!(Test-Path $ClientScriptsPathWindowsUpdates)){
		New-Item -Type Directory $ClientScriptsPathWindowsUpdates
	}else{
		Get-ChildItem $ClientScriptsPathWindowsUpdates -Recurse | Remove-Item
	}
} -Argumentlist $ClientScriptsPathWindowsUpdates

# Solange die Cycles noch nicht durch sind, und es nicht "Keine Updates" gibt, und es bisher kein Problem gab.
while(($CountUpdateCycles -lt $MaxUpdateCycles) -and ($UpdateCount -ne 0) -and !$problem){
	Write-Output "Durchgang: $CountUpdateCycles"
	Write-Output "MaxUpdateCycles: $MaxUpdateCycles"
	Write-Output "UpdateCount: $UpdateCount"
	
	# Auf Systen schalten und schauen ob Updates verfügbar sind
	Write-Output "Remote schauen ob es Updates gibt"
	$UpdateCount = Invoke-Command -Session $Session -Script {
		param($UpdateStatusLog)
		$Criteria = "IsInstalled=0 and Type='Software'"
		"RemoteSearch" | Out-File $UpdateStatusLog
		$Searcher = New-Object -ComObject Microsoft.Update.Searcher
		$ComputerObject = Get-WMIObject Win32_ComputerSystem
		try{
			$SearchResult = $Searcher.Search($Criteria).Updates
			$UpdateCount = $SearchResult.Count
			if ($UpdateCount -eq 0) {
				"NoUpdates" | Out-File $UpdateStatusLog
			}		
			return $UpdateCount
		}catch{
			Write-Output "FEHLER auf $FQDN"
			Write-Output $error | Out-String
		}
	} -Argumentlist $UpdateStatusLog

	$TotalUpdateCount += $UpdateCount
	
	if($CountUpdateCycles -eq 0){
		$InitialUpdateCount = $UpdateCount
	}
	
	Write-Output "Es gibt $UpdateCount Update(s)"
	
	# Credentials für den geplanten Task definieren
	$TaskUser = $Username
	$TaskPassword = $PlainPassword
	$RunUser = $Username
	$RunPassword = $PlainPassword

	# Wenn es Updates gibt
	if($UpdateCount -gt 0){
		Write-Output "Anzahl der Updates ist größer 0"

		# Zunächst das Skript zum Installieren rüber kopieren...
		Write-Output "Skript rüber kopieren"
		Invoke-Command -Session $Session -Script {
			param($ClientScriptsPathWindowsUpdatesScriptFile,$InstallWindowsUpdatesScript) `
			Set-Content -Path $ClientScriptsPathWindowsUpdatesScriptFile -Value $InstallWindowsUpdatesScript
		} -Argumentlist $ClientScriptsPathWindowsUpdatesScriptFile,$InstallWindowsUpdatesScript
		
		# Windows Updates dürfen per PowerShell nicht remote installiert werden.
		# Darum braucht man einen Workaround über den Task-Scheduler der ein lokales Skript aufruft, das wiederum die Updates installiert.
		# Geplanten Task auf Ziel-System erstellen		
		$TaskFile = $ClientScriptsPathWindowsUpdatesScriptFile

		$TaskName = "PS24_" + $ClientScriptsPathWindowsUpdatesScriptName
		$TaskRun = 'PowerShell.exe -ExecutionPolicy Unrestricted -command \"& '
		$TaskRun += "'"
		$TaskRun += $TaskFile
		$TaskRun += "' '"
		$TaskRun += $UpdateStatusLog
		$TaskRun += "' '"
		$TaskRun += $ErrorLog
		$TaskRun += "'"
		$TaskRun += '\"'
	
		# Start-Tag definieren
		# Da es getriggert wird, liegt der Tag in der Vergangenheit, so startet der Task nicht ausversehen
		$SD = (Get-Date).AddDays(-1).ToString("dd/MM/yyyy")
		# Beliebige Uhrzeit, da eh in der Vergangenheit
		$ST = "12:00"
		
		Write-Output "Etwaigen alten Task löschen"
		# Etwaigen alten Task löschen
		
		Invoke-Command -Session $Session -Script {
			param($TaskName)
			C:\Windows\System32\schtasks.exe /Delete /TN $TaskName /F
		} -Argumentlist $TaskName
		sleep(1)
		
		
		Write-Output "Neuen Task erstellen"
		# Neuen Task erstellen
		Invoke-Command -Session $Session -Script {
			param($RunUser,$RunPassword,$SD,$ST,$TaskName,$TaskRun)
			C:\Windows\System32\schtasks.exe /Create /RU $RunUser /RP $RunPassword /SC Once /SD $SD /ST $ST /TN $TaskName /TR $TaskRun /RL HIGHEST
		} -Argumentlist $RunUser,$RunPassword,$SD,$ST,$TaskName,$TaskRun
		sleep(5)
		
		Write-Output "Task ausführen"
		# Task ausführen
		Invoke-Command -Session $Session -Script {
			param($TaskName)
			C:\Windows\System32\schtasks.exe /Run /I /TN $TaskName
		} -Argumentlist $TaskName
		sleep(1)

		# Die Zeit definieren die der Server zum installieren der Updates brauchen darf
		$MaxTimeForUpdatesInstallation = 120 # Minutes
		$CounterForUpdatesInstallation = 0
		$SleepForUpdatesInstallation = 30 # Seconds
		$MaxLoopsForUpdatesInstallation = $MaxTimeForUpdatesInstallation * 60 / $SleepForUpdatesInstallation

		# Während der definierten Zeit schauen wie der Status des Log-Files ist
		while($CounterForUpdatesInstallation -lt $MaxLoopsForUpdatesInstallation){
			
			if((Get-Date) -gt $InitialTime.AddHours($MaxOverallTime)){
				# Die ganze Routine dauert zu lange
				Write-Output "Der gesamte Workflow dauert schon mind. $MaxOverallTime Stunden. Abbruch."
				$problem = "PROBLEM: Die gesamte Update-Installation dauert schon mind. $MaxOverallTime Stunden"
				break:
			}
			
			Write-Output "While-Schleife für Update-Installation"
			Write-Output "Counter $CounterForUpdatesInstallation von $MaxLoopsForUpdatesInstallation"
			$Status = Invoke-Command -Session $Session -Script {
						param($UpdateStatusLog)
						Get-Content $UpdateStatusLog
					} -Argumentlist $UpdateStatusLog
			Write-Output "Status der Text-Datei ist: $Status"
			# Solange er noch keinen Reboot will, weiter warten
			if($Status -ne "Reboot"){
				Write-Output "Status ist NICHT Reboot"
				sleep($SleepForUpdatesInstallation)
				$CounterForUpdatesInstallation++
			}elseif($Status -eq "Reboot"){
			# Wenn er dann doch endlich einen Reboot will, aus der Schleife raus
				Write-Output "Status ist Reboot"
				break;
			}elseif($Status -eq "Completed"){
				Write-Output "Status ist Completed"
				break;
			}elseif($Status -eq "Fehler"){
				Write-Output "Die Windows Updates Installer wirft einen Fehler"
				$problem = "PROBLEM: Die Windows Updates Installer wirft einen Fehler"
				break;
			}
		}
		
		# Nochmal schauen ob das Log-File "Reboot" sagt, nicht, dass einfach die Schleife zu Ende war...
		if($Status -eq "Reboot"){
			Write-Output "Ja, Status ist wirklich Reboot"
			# Schauen wann der letzte Reboot war, damit man es später vergleichen kann
			$PreviousBootUpTime = Invoke-Command -Session $Session -Script {
				Get-WmiObject win32_operatingsystem | select csname, @{LABEL=’LastBootUpTime’
					;EXPRESSION={
						$_.ConverttoDateTime($_.lastbootuptime)
					}
				}
			}	
			$PreviousBootUpTime = $PreviousBootUpTime.LastBootUpTime
			Write-Output "PreviousBootUpTime: $PreviousBootUpTime"
			
			# Den Server neu starten:
			Write-Output "Server wird neu gestartet"
			Invoke-Command -Session $Session -Script {Restart-Computer -Force}
			
			# Zeiten definieren, wie lange der Reboot dauern darf
			$MaxTimeForReboot = 30 # Minutes
			$CounterForReboot = 0
			$SleepForReboot = 30 # Seconds
			$MaxLoopsForReboot = $MaxTimeForReboot * 60 / $SleepForReboot

			while($CounterForReboot -lt $MaxLoopsForReboot){
				Write-Output "While-Schleife für Reboot"
				Write-Output "Counter $CounterForReboot von $MaxLoopsForReboot"
		
				# Schauen wann der letzte Reboot war
				# Da der Server neu gestartet wurde, erstellen wir eine neue Session
				$Session = New-PSSession -ComputerName $Computername -Credential $Credentials -SessionOption $SessionOption
				$LastBootUpTime = Invoke-Command -Session $Session -Script {
					Get-WmiObject win32_operatingsystem | select csname, @{LABEL=’LastBootUpTime’
						;EXPRESSION={
							$_.ConverttoDateTime($_.lastbootuptime)
						}
					}
				}
				$LastBootUpTime = $LastBootUpTime.LastBootUpTime
				Write-Output "LastBootUpTime: $LastBootUpTime"
				
				# Wenn der Reboot erfolgreich war (also die aktuelle letzte BootTime größer ist als die vorherige BootTime)
				if($LastBootUpTime -gt $PreviousBootUpTime){
					Write-Output "LastBootUpTime größer als PreviousBootUpTime"
					Write-Output "60 Sekunden warten"
					# Geben wir dem System nochmal etwas Zeit um richtig online zu kommen
					Sleep(60)
					
					Write-Output "Break -> System ist jetzt wieder up, also muss man nicht mehr ihn der Reboot-Schleife warten"
					# Raus aus der While
					break;
				# Wenn nicht
				}else{
					if($LastBootUpTime -eq $PreviousBootUpTime){
						Write-Output "Hängt wohl noch beim Herunterfahren"
						Write-Output "LastBootUpTime GLEICH wie PreviousBootUpTime"
						Write-Output "LastBootUpTime: $LastBootUpTime"
						Write-Output "PreviousBootUpTime: $PreviousBootUpTime"
						if($CounterForReboot -eq ($MaxLoopsForReboot/2)){
							Write-Output "Halbzeit! --> PSKill wäre angebracht."
							C:\Tools\PSTools\pskill.exe -t \\$ComputerName -u $Username -p $PlainPassword trustedinstaller.exe
							# Oft hängt der Windows Installer Dienst beim Herunterfahren
							# Da hilt nur die Exe zu killen
							# PsKill.exe \\$ComputerName trustedinstaller.exe
						}
					}else{
						Write-Output "Ist zur Zeit nicht erreichbar. Fährt wohl gerade hoch."
						Write-Output "LastBootUpTime NICHT größer als PreviousBootUpTime"
						Write-Output "LastBootUpTime: $LastBootUpTime"
						Write-Output "PreviousBootUpTime: $PreviousBootUpTime"
					}
					Sleep($SleepForReboot)
					$CounterForReboot++
				}
			}
		}elseif($Status -eq "Completed"){
			# Updats erfolgreich installiert und brauchen keinen Reboot
			Write-Output "Updats auf $ComputerName erfolgreich installiert und brauchen keinen Reboot"
			$success = "ERFOLG: Updats erfolgreich installiert und brauchen keinen Reboot"
		}else{
		# Wenn der Status doch nicht auf Reboot ist, ist die Schleife zu Ende.
		# Entweder dauert es zu lange bis die Updates installiert sind oder er hat nie begonnen.
			# FEHLER #
			Write-Output "ES GAB EINEN FEHLER:"
			Write-Output "Der Status lautet: $Status"
			Write-Output $error | Out-String
			if(!$problem){
				$problem = "PROBLEM: Nach $MaxTimeForUpdatesInstallation wurde noch immer kein Reboot durchgeführt" # ca. 52 Zeichen von 160 SMS Zeichen
			}
		}		
	}else{
	# Wenn es keine Updates gibt...
		Write-Output "Es gibt keine Updates"
		$success = "ERFOLG: Es gibt keine neuen Updates"
	}
	
	# Durchgang zu Ende
	Write-Output "Durchgang zu Ende"
	
	# Werte sind wie folgt:
	Write-Output "Werte sind wie folgt:"
	Write-Output "Durchgang: $CountUpdateCycles"
	Write-Output "MaxUpdateCycles: $MaxUpdateCycles"
	Write-Output "UpdateCount: $UpdateCount"
	Write-Output "---------------------------------"
	
	$CountUpdateCycles++
}

if($UpdateCount -ne 0){
	# Irgendetwas stimmt nicht.
	# Nach 5 Durchläufen sind noch immer Updates verfügbar
	Write-Output "$CountUpdateCycles von $MaxUpdateCycles Durchgänge sind durch Trotzdem gibt es noch $UpdateCount Update(s)..."
	if(!$problem){
		$problem = "PROBLEM: Nach $MaxUpdateCycles sind noch immer $UpdateCount Updates verfügbar" # ca. 44 Zeichen von 160 SMS Zeichen
	}
}else{
	Write-Output "Geplanten Task löschen"
	# Den geplanten Task löschen
	Invoke-Command -Session $Session -Script {
		param($TaskName)
		C:\Windows\System32\schtasks.exe /Delete /TN $TaskName /F
	} -Argumentlist $TaskName
	sleep(1)

	if ($InitialUpdateCount -eq 0){
		$success = "ERFOLG: Es gab keine neuen Updates"
		Write-Output $success
	}else{
		$success = "ERFOLG: Das System $ComputerName wurde erfolgreich gepatcht. Es wurden $TotalUpdateCount Patches installiert"
		Write-Output $success
	}
}

if($success){
	# Alles ist gut
	Write-Output $success
	if($SMSNumber){			
		$Message = "$success - $ComputerName"
		# SMS schreiben und anschl. Call ausführen
		& $PathToSendSMS -Message $Message -PhoneNumber $SMSNumber
	}
}else{
	# Es gibt ein Problem.
	if($problem){
		$Message = "$problem - $ComputerName"
	}else{
		$Message = "FEHLER: Unbekannter Fehler - $ComputerName"
	}
	Write-Output $Message

	if($SMSNumber){
		# SMS schreiben und anschl. Call ausführen
		& $PathToSendSMS -Message $Message -PhoneNumber $SMSNumber
	}
	
	if($PhoneNumber){	
		# 30 Sekunden danach nochmal anrufen um drauf aufmerksam zu machen
		sleep(30)
		& $PathToMakePhoneCall -PhoneNumber $PhoneNumber
	}
}

Write-Output "30 Sekunden Zeit zum Lesen"
Sleep(30) 

In meinem Fall wird dieses Skript selbst nur von einem anderen Skript aufgerufen. Dieses erstellt wiederum vorher einen Snapshot sofern es sich um eine VM handelt, liest die Login-Daten, etc. aus der CMDB aus, bietet eine kleine GUI in welcher man die Zielsysteme auswählen sowie sein Kürzel sowie die Telefonnummern hinterlegen kann, etc.

Da dies jedoch eine zu spezifische Anwendung ist, habe ich mich entschlossen sie nicht zu posten.