Using PowerShell to Failover Cluster Resources for Patching
Introduction
So you are having to patch cluster nodes and would like a way to automate the failover of the resources and log what when it was done and who did the work. In this post we will cover a simple PowerShell script that will drain, pull and balance resources in a 2 or more node active / passive or active / active cluster.
About the Script
The purpose of the PowerShell script is to allow three operations to occur, they are to drain all the resources on the node you are running the script on to the other node in the two node cluster and the other is to bring all (pullall) the resources from a node to the node. Last option is for active / active clusters called balance, this will move the resource to the preferred owner. While doing this to also create a local log file and also to create a Event in the Application event log. Using this script you can automate patching of clusters with SCCM and other methods. Another note is that the script be able to work on Windows Server 2008, 2008 R2, 2012, 2012 R2 and 2016 versions.
To achieve this we will create three functions and also set a string parameter to look for drain, pullall or balance at the PowerShell command line. For the PowerShell cluster operations we will be using the FailOverClusters module and several get cmdlets such as Get-ClusterGroup, Get-ClusterResource, Move-ClusterGroup and Get-ClusterNode.
For the logging events portion we will be capturing who is doing the failover, the date and time and what operation is performed into the local logfile stored in c:\support directory using Out-File and Add-Content cmdlets. For the Event logging we will use the Write-EventLog cmdlet and create the event in Application Log. Below is the script and is fairly easy to read and can be modified to your liking. In a future post we will add the balance feature to allow this script to be used to send Cluster Groups to preferred owners in Active / Active clusters and some error checking.
The Script
Here is the script in it’s entirety. [easy_media_download url=”https://lifeofageekadmin.com/wp-content/uploads/2016/08/ClusterResourceMover2.zip“]
<# .SYNOPSIS This script will pullall, drain or balance cluster resources from the command line. .DESCRIPTION The script will pullall, drain or balance cluster resources from the command line. .NOTES File Name : ClusterResourceMover.ps1 Author : Mark Harris Requires : Windows Server 2012 R2 PowerShell Windows Server 2012 R2 Clustering Cmdlets Windows Server 2008 R2 PowerShell Windows Server 2008 R2 Clustering Cmdlets Windows Server 2016 PowerShell Windows Server 2016 Clustering Cmdlets Version : 1.0 (June 26 2017) .PARAMETER drain or pullall or balance Specifies send, retrieve or delegate cluster resources .INPUTS ClusterResourceMover accepts drain, pullall and balance .OUTPUTS Log to c:\support directory and log to Application EventLog and sends email to teams or individuals. .EXAMPLE C:\PS> .\ClusterResourceMover drain # drains all cluster resources to the other node. .EXAMPLE C:\PS> .\ClusterResourceMover pullall # Retrives all cluster resources to the current node. .EXAMPLE C:\PS> .\ClusterResourceMover balance # Moves resource to preferred owner node. #> # Get User input and execute function Must be first line in code Param([Parameter(Mandatory=$true,HelpMessage="Enter in drain, pullall, balance")][String]$ClusOp) # Create EventLog Source, will not create if exists $Result = test-path -path 'HKLM:\SYSTEM\ControlSet001\Services\EventLog\Application\ClusterHealth' if ($Result -eq "True") { Write-host "No changes made to registry, all good" } Else { New-EventLog -Source "ClusterHealth" -LogName "Application" -MessageResourceFile "%SystemRoot%\System32\EventCreate.exe" } # Create Local logfile Import-Module FailoverClusters $node = hostname $date1=Get-Date -Format "yyyy-MM-dd hh:mm tt" $clustervip=Get-Cluster $clustervipname=($clustervip.Name) $admin = (Get-Content -Path env:username).ToString() $LogFileName = "c:\Support\ClusterResMove-"+ $node +".txt" Out-File -append -FilePath $LogFileName -Encoding ASCII Add-Content -Path $LogFileName -Value "Computer: $node" -Encoding ASCII Add-Content -Path $LogFileName -Value "*****************************" -Encoding ASCII Function Balance { Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 101 -EntryType Information -Message "($clustervipname) Cluster Balance Operation Started for Windows Patching" Import-Module FailoverClusters Add-Content -Path $LogFileName -Value "Date: $date1" -Encoding ASCII Add-Content -Path $LogFileName -Value "Executed By: $admin" -Encoding ASCII Add-Content -Path $LogFileName -Value "Current Cluster Assignments" -Encoding ASCII $computer = get-content env:computername $computer = $computer.ToLower() Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName $clustergroups = Get-ClusterGroup | Where-Object {$_.IsCoreGroup -eq $false} foreach ($cg in $clustergroups) { $CGName = $cg.Name Add-Content -Path $LogFileName -Value "Getting Cluster Group Owners" -Encoding ASCII $CurrentOwner = $cg.OwnerNode.Name $POCount = (($cg | Get-ClusterOwnerNode).OwnerNodes).Count if ($POCount -eq 0) { Add-Content -Path $LogFileName -Value "Info: $CGName doesn't have a preferred owner!" -Encoding ASCII } else { $PreferredOwner = ($cg | Get-ClusterOwnerNode).Ownernodes[0].Name Add-Content -Path $LogFileName -Value "$PreferredOwner" -Encoding ASCII if ($CurrentOwner -ne $PreferredOwner) { Add-Content -Path $LogFileName -Value "Moving resource to $PreferredOwner" -Encoding ASCII $cg | Move-ClusterGroup -Node $PreferredOwner } else { Add-Content -Path $LogFileName -Value "Resource is already on preferred owner! ($PreferredOwner)" -Encoding ASCII } } } Add-Content -Path $LogFileName -Value "Cluster Balance Operation Completed" -Encoding ASCII Add-Content -Path $LogFileName -Value "New Cluster Assignments" -Encoding ASCII Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 201 -EntryType Information -Message "($clustervipname) Cluster Balance Operation completed for Windows Patching" # Uncomment FuncMail below to use email sending option uncomment for P&C clusters. # FuncMail -To "destinationemail@me.com" -From "frommail@me.com" -Subject "($clustervipname) Cluster Balance Opertaion completed by ($node)." -Body "($clustervipname) Cluster Balance Operation completed for Windows Patching" -smtpServer "smtpmailserver" } Function Drain { Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 101 -EntryType Information -Message "Cluster Drain Operation started for Windows Patching" Import-Module FailoverClusters Add-Content -Path $LogFileName -Value "Date: $date1" -Encoding ASCII Add-Content -Path $LogFileName -Value "Executed By: $admin" -Encoding ASCII Add-Content -Path $LogFileName -Value "Current Cluster Assignments" -Encoding ASCII Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName $computer = get-content env:computername $computer = $computer.ToLower() $destnode = Get-clusternode | select Name # Convert to string for use in foreach-object [string]$drainnode = ($destnode.Name -ne $computer) Get-ClusterGroup | foreach-object ` { If ($_.Name -ne $computer) { Move-ClusterGroup -Name $_.Name -Node $drainnode } } Add-Content -Path $LogFileName -Value "($clustervipname) Drain Operation Completed to $drainnode " -Encoding ASCII Add-Content -Path $LogFileName -Value "New Cluster Assignments" -Encoding ASCII Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 201 -EntryType Information -Message "($clustervipname) Cluster Drain Operation completed for Windows Patching" # Uncomment FuncMail below to use email sending option uncomment for P&C clusters. # FuncMail -To "destinationemail@me.com" -From "frommail@me.com" -Subject "($clustervipname) Cluster Drain Opertaion completed by ($node)." -Body "($clustervipname) Cluster Drain Operation completed for Windows Patching" -smtpServer "smtpmailserver" } Function PullAll { Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 101 -EntryType Information -Message "Cluster PullAll Operation Started for Windows Patching" Import-Module FailoverClusters Add-Content -Path $LogFileName -Value "Date: $date1" -Encoding ASCII Add-Content -Path $LogFileName -Value "Executed By: $admin" -Encoding ASCII Add-Content -Path $LogFileName -Value "Current Cluster Assignments" -Encoding ASCII Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName $computer = get-content env:computername $computer = $computer.ToLower() Get-clusternode | Get-ClusterGroup | foreach-object ` { If ($_.Name -ne $computer) { Move-ClusterGroup -Name $_.Name -Node $computer } } Add-Content -Path $LogFileName -Value "($clustervipname) PullAll Operation Completed to $computer " -Encoding ASCII Add-Content -Path $LogFileName -Value "New Cluster Assignments" -Encoding ASCII Get-ClusterGroup | Out-File -append -Encoding ASCII -FilePath $LogFileName Write-EventLog -LogName "Application" -Source "ClusterHealth" -EventID 201 -EntryType Information -Message "($clustervipname) Cluster PullAll Operation completed for Windows Patching" # Uncomment FuncMail below to use email sending option uncomment for P&C clusters. # FuncMail -To "destinationemail@me.com" -From "frommail@me.com" -Subject "($clustervipname) Cluster PullAll Opertaion completed by ($node)." -Body "($clustervipname) Cluster PullAll Operation completed for Windows Patching" -smtpServer "smtpmailserver" } # Take the parameter and validate the input and call the functions. If ("drain","pullall","balance" -NotContains $ClusOp) { Throw "Not a valid option! Please use drain, pullall or balance option" | Out-File -append -Encoding ASCII -FilePath $LogFileName } function FuncMail { #param($strTo, $strFrom, $strSubject, $strBody, $smtpServer) param($To, $From, $Subject, $Body, $smtpServer) $msg = new-object Net.Mail.MailMessage $smtp = new-object Net.Mail.SmtpClient($smtpServer) $msg.From = $From $msg.To.Add($To) $msg.Subject = $Subject $msg.IsBodyHtml = 1 $msg.Body = $Body $smtp.Send($msg) } # All parameters are valid call function If ($ClusOp -eq "drain") { Drain } If ($ClusOp -eq "pullall") { PullAll } If ($ClusOp -eq "balance") { Balance }
So we have the script, how do we run it and what does it look like? By running ClusterResourceMover2 drain or ClusterResourceMover2 pullall or ClusterResourceMover2.ps1 balance we can make the script do its magic.
Conclusion
So we see it is possible to use the FailoverClusters PowerShell module to automate patching of Windows Servers 2008 / 2012 / 2016 clusters and also log operations for tracking.
Great script. Have you completed the balance portion of this script yet?
I have rewritten it and some improvements all three features working. Even works on Server 2016.
Scott,
I have updated the post and the code to include the balance feature. I did cover this in another post about the fail to preferred node here. http://lifeofageekadmin.com/using-powershell-move-cluster-resources-preferred-node/. I hope this scripts helps you, I know I could not find anything like it anywhere to help automate patching clusters. I maintain over 24 3 and 4 node clusters so this was a need to fill.
Mark