PowerShell: Automating Microsoft Failover Cluster Maintenance – FileServer, SQL AlwaysOn, Hyper-V Guest VMs, Disks Operations

#####################################################################################################
# MsClusterMaintenance_v0.0.2.ps1
# Author: KimConnect.com
# License: GPLv3
# Description: this program automates the maintenance routines affecting Microsoft Failover Clusters
#
# Requires: PowerShell version 4.0+
# 
# Current features:
# 1. Display all cluster nodes and their statuses
# 2. Stop/Pause and Start/Resume Clusters and nodes
# 3. Fix clustered disks to resolve "Chkdsk spotfix needed on volume \\?\Volume" errors
# 4. Systematic Windows updates for all nodes of target cluster(s)
#
# Features in development:
# (I have written codes for these already - just need to locate or rewrite them)
# 1. Windows Patching function - Done
# 2. Reset quorum and node voting assignments
# 3. Reset (evict and rejoin) problematic nodes
# 4. SQL: Change SQL AlwaysOn Primary node role as precursor to maintenance (scheduled Windows Updates)
# 5. SQL: Add/remove replicas of an existing Availability Group
# 6. Hyper-V: Moving Virtual Machines & Expanding Disk Volumes
# 7. Fileserver: create/remove disks, client-access-points, SMB shares
# 8. Perform cluster diagnostics and make recommendations
# 9. Sios Datakeeper: if I can find my script, I'll add in to this program
#        (import-module "C:\Program Files (x86)\SIOS\DataKeeper\DKPwrShell")
# 
# Limitations:
# 1. Windows 2008 clusters: 50/50 chance of working. I can spend time making it compatible, but why?
# 2. Linux: not compatible even with dotNet & PowerShell Core added
# 3. Non-domain joined Windows Shervers: nope
# 4. Datrium DVX: it's pretty good. All point-and-clicks and no fun.
# 5. Fat fingers: you can be fat, but your fingers need to have muscles to work with the menus.
#####################################################################################################

# User input variables
$clusterNames="CLUSTER1","CLUSTER2" # Optional: specify clusternames or leave it empty for autoscans
$nodes="" # Optional

# Static program variables
$functions=@();
$functions+=[PSCustomObject]@{Function='GetNodes';Command="getNodes `$clusterNames"};
$functions+=[PSCustomObject]@{Function='SuspendOneNode';Command="suspendOneNode `$clusterNames"};
$functions+=[PSCustomObject]@{Function='SuspendAllNodes';Command="suspendAllNodes `$clusterNames"};
$functions+=[PSCustomObject]@{Function='ResumeOneNode';Command="resumeOneNode `$clusterNames"};
$functions+=[PSCustomObject]@{Function='ResumeAllNodes';Command="resumeAllNodes `$clusterNames"};
$functions+=[PSCustomObject]@{Function='StopOneCluster';Command="StopOneCluster"};
$functions+=[PSCustomObject]@{Function='StopAllClusters';Command="StopAllClusters `$clusterNames"};
$functions+=[PSCustomObject]@{Function='StartOneCluster';Command="StartOneCluster"};
$functions+=[PSCustomObject]@{Function='StartAllClusters';Command="StartAllClusters `$clusterNames"};
$functions+=[PSCustomObject]@{Function='FixDisks';Command="FixDisks `$clusterNames"};
$functions+=[PSCustomObject]@{Function='patchNodes';Command="patchNodes `$clusterNames"};
$functions+=[PSCustomObject]@{Function='startClusterManually';Command="startClusterManually"};

################################## Excuting Program as an Administrator ####################################
# Get the ID and security principal of the current user account
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
 
# Get the security principal for the Administrator role
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
 
# Check to see if we are currently running "as Administrator"
if ($myWindowsPrincipal.IsInRole($adminRole))
   {
   # We are running "as Administrator" - so change the title and background color to indicate this
   $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
   #$Host.UI.RawUI.BackgroundColor = "Black"
   clear-host
   }else{
       # We are not running "as Administrator" - so relaunch as administrator
   
       # Create a new process object that starts PowerShell
       $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
   
       # Specify the current script path and name as a parameter
       $newProcess.Arguments = $myInvocation.MyCommand.Definition;
   
       # Indicate that the process should be elevated
       $newProcess.Verb = "runas";
   
       # Start the new process
       [System.Diagnostics.Process]::Start($newProcess);
   
       # Exit from the current, unelevated, process
       exit
       }
 
Write-Host -NoNewLine "Running as Administrator..."
################################## Excuting Program as an Administrator ####################################

# Adding Prerequisite Microsoft Cluster
Function includeFailoverClustersModule{
    if (!(get-module -Name "FailoverClusters") ){
    try{
        Install-WindowsFeature RSAT-Clustering-MGMT | out-null;
        Install-WindowsFeature RSAT-Clustering-PowerShell | out-null;
        #Install-WindowsFeature Failover-Clustering | out-null;
        }catch{
                $rsatFailoverClusterInstalled=(Get-WindowsCapability -name Rsat.FailoverCluster* -online).State;                                                                                                                                                                                                                                                                                                                                          if(!($rsatFailoverClusterInstalled)){
                                                                                                <# Pre-emptively resolve this error
            Import-Module : The specified module 'failoverclusters' was not loaded because no valid module file was found in any
            module directory.
            At line:1 char:1
            + Import-Module failoverclusters
            + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                + CategoryInfo          : ResourceUnavailable: (failoverclusters:String) [Import-Module], FileNotFoundException
                + FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand

            And this error:

            Install-WindowsFeature : The term 'Install-WindowsFeature' is not recognized as the name of a cmdlet, function, script
            file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct
            and try again.
            At line:4 char:5
            +     Install-WindowsFeature RSAT-Clustering-MGMT | out-null;
            +     ~~~~~~~~~~~~~~~~~~~~~~
                + CategoryInfo          : ObjectNotFound: (Install-WindowsFeature:String) [], CommandNotFoundException
                + FullyQualifiedErrorId : CommandNotFoundException

            # Validate whether ServerManager is available on this system. Hint: Windows 2003 Server & Non MS Server OS's will return $null results
            Get-Module -ListAvailable | ? { $_.Name -eq 'ServerManager' }

            # Solution: Set Windows to Use Online updates, instead of WSUS
            #>
                $wuRegistryHive="REGISTRY::HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
                $wuKey="UseWUServer"
                $currentWu = Get-ItemProperty -Path $wuRegistryHive -Name $wuKey -ErrorAction SilentlyContinue | select -ExpandProperty UseWUServer
                if($currentWu){
                    Set-ItemProperty -Path $wuRegistryHive -Name $wuKey -Value 0;
                    Restart-Service wuauserv;
                    }    
                Get-WindowsCapability -Name Rsat.FailoverCluster* -Online | Add-WindowsCapability –Online
                <# This error means that there's a pending RSAT install operation on this system
                Get-WindowsCapability : The data area passed to a system call is too small.
                At line:1 char:1
                + Get-WindowsCapability -Name RSAT* -Online
                + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    + CategoryInfo          : NotSpecified: (:) [Get-WindowsCapability], COMException
                    + FullyQualifiedErrorId : Microsoft.Dism.Commands.GetWindowsCapabilityCommand
                #>
                if($currentWu){
                    Set-ItemProperty -Path $wuRegistryHive -Name $wuKey -Value $currentWu;
                    Restart-Service wuauserv;
                    }        
                }

            }
    Import-Module FailoverClusters | out-null;
    }
}
includeFailoverClustersModule;

# Populate Clusternames variable
function getClusterNames{
    param($domainName=$env:USERDNSDOMAIN)
    write-host "Now scanning $domainName for all available cluster names. This may take a while...`r`n";
    $clusterNames=(get-cluster -Domain $domainName).Name
    return $clusterNames;
}

function getNodes($clusterNames){
    $clusterNodes=foreach ($clustername in $clusterNames){
            try{
                get-clusternode -cluster $clustername -ea Stop|select-object @{Name='ClusterName';e={$clustername}},Name,State
                }catch{
                    $clusterIsStarted=Start-Cluster -Name $cluster
                    if (!($clusterIsStarted)){
                        write-host "Cluster $clustername status is OFFLINE. Please run 'startCluster -clusterName `$clusterName' manually get set it online."
                        }
                    [PSCustomObject]@{ClusterName=$clustername;Name="UnableToScanNodes";State="Unknown"};
                    continue;
                    }
            }
    return $clusterNodes
}

function startClusterManually([string]$clusterName){

    if (!($clusterName)){
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            return $false;
            }
        $userInput = Read-Host -Prompt "Please type in ONE cluster name";
        $name=if($userinput -notmatch '\.'){$userInput+"."+$env:USERDNSDOMAIN}else{$userInput}
        $ip=[System.Net.Dns]::GetHostEntry($name).AddressList.IPAddressToString.Trim();              
        if(!($ip)){
            write-host "Attempt $attempts of $maxAttempts`: $name doesn't resolve to any IP on this network.`r`n";
            }else{
                write-host "$userInput has resolved to IP $ip.";
                $clusterName=$userInput;
                break;
                }        
        }
    }

    function onlineClusterResources{
        try{
            Start-Cluster -Name $clusterName;
            Start-ClusterResource -cluster $clusterName -Name "Cluster Name";
            start-clusterresource -cluster $clusterName -Name "Storage Qos Resource";
            $nodes=Get-ClusterNode -Cluster $clusterName
            $nodes|%{
                if ($_.State -eq 'Paused'){
                    try{
                    Resume-ClusterNode -cluster $clusterName -Name $_.Name;
                    write-host "$($_.Name) is now UP.";
                        }catch{
                            write-host "Unable to start node $($_.Name).";
                            write-host $Error;
                            }

                    }else{
                        write-host "Node $($_.Name) current status is UP.";
                        }    
                }
            write-host "$clusterName is been online successfully.";
            return $true;
            }catch{
                write-host "There are some errors while attempting to set cluster $clusterName online.";
                write-host $Error
                return $false;
                } 
        }
    
    if($clusterName -notmatch '\.'){$clusterName+"."+$env:USERDNSDOMAIN}
    $ip=[System.Net.Dns]::GetHostEntry($clustername).AddressList.IPAddressToString.Trim();
    #$ipToName=[System.Net.Dns]::GetHostByAddress($ip).HostName;
    if (!($ip)){
        write-host "$clusterName doesn't resolve to any IP on this network.";
        return $false
        }else{
            $nameIsOnline=Test-Connection -ComputerName $clusterName -Count 1 -Quiet;
            $clusterIsStarted=try{
                Start-ClusterResource -cluster $clusterName -Name "Cluster Name" -ErrorAction Stop;
                Start-Cluster -Name $clusterName;
                start-clusterresource -cluster $clusterName -Name "Storage Qos Resource";
                $true;
                }catch{
                write-host "Unable to start cluster $clustername properly.";
                $false;
                }
            if (!($nameIsOnline -and $clusterIsStarted)){
                write-host "$clustername is currently not pingable by $ip."
                
                $maxAttempts=3
                $attempts=0;
                while ($attempts -le $maxAttempts){
                    if($attempts++ -ge $maxAttempts){
                        write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
                        return $false;
                        }
                    $userInput = Read-Host -Prompt "Please type in ONE name of a node to belong to cluster $clusterName";                
                    $isOnline=Test-Connection -ComputerName $userInput -Count 1 -Quiet;
                    if ($isOnline){
                        write-host "$userInput is pingable. Program will now attempt to set $clusterName online...";
                        $onlineClustername=invoke-command -ComputerName $userInput -ScriptBlock {
                            param($clustername)
                            try{                        
                                Start-ClusterResource -Name "Cluster Name" -ErrorAction Stop;
                                $result=$true;
                                }catch{
                                    write-host "Unable to $clustername resource 'Cluster Name'.";
                                    #write-host $Error;
                                    $result=$false;
                                    }                            
                            return $result;
                            } -ArgumentList $clustername
                        if ($onlineClustername){
                            $success=onlineClusterResources;
                            if($success){return $true;}                          
                            }else{
                                write-host "$clusterName couldn't be set online via $userInput. Please try a different node.";
                                }
                        }else{
                            write-host "Attempt number $attempts of $maxAttempts`: $userInput is not pingable. Try again..`r`n";
                            }
                    }

                
                }else{
                    onlineClusterResources;
                    }
    
        }
    }

function displayNodes($nodes){
    $display=for ($i=0;$i -lt $nodes.count;$i++){
        "$($i+1)`: $($nodes[$i].ClusterName) | $($nodes[$i].Name) | Status=$($nodes[$i].State)";
        }
    return $display|out-string
    }

function suspendAllNodes($clusterNames){
    # Put cluster nodes into maintenance
    $randomString=generateRandomString -anyCharacters -charactersCount 40
    $confirmed=confirmation -testValue $randomString -maxAttempts 3;
    if ($confirmed){
        $nodes=getNodes $clusterNames
        $nodes|%{Suspend-ClusterNode -Name $_.Name}
        return $True
        }else{
            write-host "No actions were taken.";
            return $False;
            }
}

function resumeAllNodes($clusterNames){
    # Resume all nodes
    try{
        $clusterNames|%{Start-Cluster -Name $_} | Out-Null;
        $nodes=getNodes $clusterNames
        $nodes|%{
            if ($_.State -ne 'Up'){
                Resume-ClusterNode -Name $_.Name;
                }else{
                    write-host "$($_.Name) is already UP."
                    }
            }
        return $true;
        }catch{
            return $false;
            }
}

function confirmation($testValue="I confirm",$maxAttempts=3){
    $confirmed=$false;
    $attempts=0;

    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
            break;
            }
        $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm";
        if ($userInput.ToLower() -ne $testValue.ToLower()){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again..`r`n"
            }else{
                $confirmed=$true;
                write-host "Confirmed!`r`n";
                break;
                }
        }
    return $confirmed;
}

function generateRandomString{
    param(
        [switch]$anyCharacters,
        [switch]$numbersAndLetters=$true,
        [int]$charactersCount=10
        )
    if ($anyCharacters){
        $randomValue=-join ((33..126) | Get-Random -Count $charactersCount | % {[char]$_})
        }else{
        $randomValue=-join ((48..57) + (97..122) | Get-Random -Count $charactersCount | % {[char]$_}); 
        }
    return $randomValue;        
}

function stopOneCluster{
    $display=for ($i=0;$i -lt $clusterNames.count;$i++){
        "$($i+1)`: $($clusterNames[$i])`r`n";
        }
    $lines=($display | Measure-Object –Line).Lines
    write-host $display
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $cluster=$clusterNames[$value-1];
                write-host "$userInput corresponds to $cluster`r`n";
                break;
                }
        }
    
    # Safeguard
    $confirmed=$false;
    $randomString=generateRandomString
    $confirmed=confirmation -testValue $randomString -maxAttempts 3;

    # Initiating cluster shutdown-for-maintenance sequence
    if ($confirmed){
        $nodes=getNodes $cluster
        $nodes|%{
            if ($_.State -eq 'Up'){
                Suspend-ClusterNode -Name $_.Name;
                write-host "$($_.Name) is now Paused."
                }else{
                    write-host "$($_.Name) is already Paused."
                    }    
            }
        Stop-Cluster -Name $cluster -Force
        write-host "$cluster is now in maintenance mode (shutdown).";
        return $true;
        }else{
            return $false;
            }
}

function stopAllClusters($clusterNames){
    # Safeguard
    $confirmed=$false;
    $randomString=generateRandomString
    $confirmed=confirmation -testValue $randomString -maxAttempts 3;
    
    # Shutdown clusters
    if ($confirmed){
        try{
            $nodes=getNodes $clusterNames
            $nodes|%{
                if ($_.State -eq 'Up'){
                    Suspend-ClusterNode -Name $_.Name;
                    write-host "$($_.Name) is now Paused."
                    }else{
                        write-host "$($_.Name) is already Paused."
                        }    
                }
            $clusterNames|%{
                Stop-Cluster -Name $_ -Force;
                write-host "$_ is now in maintenance mode (shutdown).";
                }
            return $true;
            }catch{
                return $false;
                }
    }else{
        return $false;
        }
}

function startOneCluster{
    $display=for ($i=0;$i -lt $clusterNames.count;$i++){
        "$($i+1)`: $($clusterNames[$i])`r`n";
        }
    $lines=($display | Measure-Object –Line).Lines
    write-host $display
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $cluster=$clusterNames[$value-1];
                write-host "$userInput corresponds to $cluster`r`n";
                break;
                }
        }    
    
    startClusterManually -clusterName $cluster;
}

function startAllClusters($clusterNames){
    # Turn clusters back on
    try{
    $clusterNames|%{
        Start-Cluster -Name $_ | Out-Null;
        $nodes|%{
            if ($_.State -eq 'Paused'){
                Resume-ClusterNode -Name $_.Name;
                write-host "$($_.Name) is now UP."
                }else{
                    write-host "$($_.Name) is already UP.";
                    }    
            }
        write-host "$cluster is now in normal mode.";       
        }
        return $true;
    }catch{
        return $false;
        }
}

function suspendOneNode($clusterNames){
    $display=displayNodes $nodes;
    $lines=($display | Measure-Object –Line).Lines
    write-host $display
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $nodes=getNodes $clusterNames;
                $node=$nodes[$value-1];
                write-host "$userInput corresponds to $($node.Name)`r`n";
                break;
                }
        }

    $confirmed = confirmation -testValue "I confirm"
    if ($confirmed){    
        if ($node.State -eq "Up"){
            Suspend-ClusterNode -Name $node.Name
            }else{
                write-host "$($node.Name) is already PAUSED."
                }
        return $true;
        }else{
            return $false;
            }
}

function resumeOneNode($clusterNames){
    $display=displayNodes $nodes;
    $lines=($display | Measure-Object –Line).Lines
    write-host $display
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $nodes=getNodes $clusterNames;
                $node=$nodes[$value-1];
                write-host "$userInput corresponds to $($node.Name)`r`n";
                break;
                }
        }
    try{
        if ($node.State -eq "Paused"){
            Resume-ClusterNode -Name $node.Name
            }else{
                write-host "$($node.Name) is already UP."
                }
            return $true;
            }catch{
                return $false;
                }
}

# Fix quorum, Force a cluster to start by seizing the quorum
function fixQuorum($clusterNames){
    $nodeName="SQL01"
    Start-ClusterNode –Name $nodeName -FixQuorum
    (Get-ClusterNode $nodeName).NodeWeight = 1 #set its weigh to 1
    }

# fixClusterDiskErrors.ps1
function selectClusterName{
    param($clusterNames,$domainName=$env:USERDNSDOMAIN)
    if(!$clusterNames){
        write-host "Now scanning $domainName for all available cluster names. This may take a while...`r`n";
        $clusterNames=(get-cluster -Domain $domainName).Name
        }
    $clusterList=@();
    $index=0;
    foreach ($clustername in $clusterNames){
        $clusterList+=[PSCustomObject]@{Index=$index++;ClusterName=$clustername};
        }

    function makeSelection{
        param($list)     
        $input=Read-Host "Please pick an index number from the above list";
        #write-host "Index value $input received.";
        $indexMax=$list.count-1
        if ($input -ge 0 -and $input -le $indexMax){
            #write-host "Index value $input is valid.";
            return $input;
            }else{
                write-host "Index value $input is invalid.";
                return $false;
                }
    }
    $maxAttempts=3;
    $attempts=1;
    do {
        cls |out-null;    
        write-host "Attempt number: $attempts of $maxAttempts`r`n";
        $clusterList| Format-Table | Out-String | Write-Host;
        $attempts++;    
        $selectedNumber=makeSelection -list $clusterList
        if ($selectedNumber){
            $selectedIndex=[convert]::ToInt16($selectedNumber)
            <# This error would occur if: $selectedIndex=[int]$selectedNumber;        
            Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32".
            At line:6 char:9
            +         $selectedIndex=[int]$selectedItem;
            +         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
                + FullyQualifiedErrorId : ConvertToFinalInvalidCastException

            Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32".
            At line:7 char:9
            +         $clusterName=$clusterList[[int]$selectedIndex];
            +         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
                + FullyQualifiedErrorId : ConvertToFinalInvalidCastException
        
            #>
            $clusterName=$clusterList[[int]$selectedIndex].ClusterName;
            write-host "$clusterName is selected.";
            return $clusterName;
            }else{
                if($attempts -gt $maxAttempts){break;}
                sleep 2;            
                }
    } while($selectedNumber -eq $false)
    return $false;
}

# Take role offline > set disk online > run fixdisk > set role online
function fixClusterDiskErrors{
    param([string]$clusterName=(get-cluster).name)
    [int]$maxAttempts=3;
    
    function getAllClusterDisks($clusterName){
        $allClusterDisks=@();
        $index=0;
        $cluster=Get-ClusterResource -Cluster $clusterName
        $nodes=$cluster.OwnerNode.Name|select-object -Unique
        if ($nodes.gettype() -eq [string]){
            $viableNode=$nodes;
            }else{
                $viableNode=$nodes[0];
                }
        $allDisks=$cluster| ? { $_.ResourceType.Name -eq "Physical Disk" } |sort -Property Name
        
        foreach ($disk in $allDisks) {
          $diskName = $disk.Name;
          $assignedTo=$disk.OwnerGroup.Name;
          $ownerNode=$disk.OwnerNode.Name;
          $status=$disk.ResourceSpecificStatus;
          $diskObject  = gwmi MSCluster_Resource -ComputerName $viableNode -Namespace root/mscluster | ? { $_.Name -eq $diskName };
          $disk = gwmi -Namespace root/mscluster -ComputerName $viableNode -Query "ASSOCIATORS OF {$diskObject} WHERE ResultClass=MSCluster_Disk";
          $partition = gwmi -Namespace root/mscluster -ComputerName $viableNode -Query "ASSOCIATORS OF {$disk} WHERE ResultClass=MSCluster_DiskPartition";
          $driveLetter = ($partition.Path)[0];
          $allClusterDisks+=[PSCustomObject]@{Index=$index++;DiskName=$diskName;DriveLetter=$driveLetter;AssignedTo=$assignedTo;OwnerNode=$ownerNode;Status=$status};
        }
        return $allClusterDisks
    }
    
    function fixClusterDisk{
        param(
            [string]$clusterName,
            [string]$diskName
            )
        $clusterDiskObject=Get-ClusterResource -Cluster $clusterName|?{$_.Name -eq $diskName}
        $resource  = gwmi MSCluster_Resource -Namespace root/mscluster |? { $_.Name -eq $diskName }
        $disk      = gwmi -Namespace root/mscluster -Query "ASSOCIATORS OF {$resource} WHERE ResultClass=MSCluster_Disk"
        $diskLetter = (gwmi -Namespace root/mscluster -Query "ASSOCIATORS OF {$disk} WHERE ResultClass=MSCluster_DiskPartition").Path[0]
        $ownerNode=$clusterDiskObject.OwnerNode

        $session=new-pssession $ownerNode
        invoke-command -Session $session -ScriptBlock{
            param($includeFailoverClustersModule,$diskLetter,$diskName)
            [scriptblock]::Create($includeFailoverClustersModule).invoke();
            $groupName=(Get-ClusterResource -Name $diskName).OwnerGroup
            Stop-ClusterGroup $groupName|out-null;
            # Error: if step 3 is executed before step 2
            #start-clusterresource : An error occurred while attempting to bring the resource 'prdechome19-n' online.
            #    The remote server has been paused or is in the process of being started
            #At line:1 char:1
            #+ start-clusterresource -name
            #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            #    + CategoryInfo          : ResourceBusy: (:) [Start-ClusterResource], ClusterCmdletException
            #    + FullyQualifiedErrorId : SharingPaused,Microsoft.FailoverClusters.PowerShell.StartClusterResourceCommand
            try{
                Resume-ClusterResource $diskName|out-null;
                Start-ClusterResource $diskName|out-null;
                Repair-Volume -DriveLetter $diskLetter -OfflineScanAndFix;
                Start-ClusterGroup $groupName|out-null;
                }catch{
                    write-host "Unable to fix volume name $diskName";
                    write-host $Error;
                    }
            } -ArgumentList ${function:includeFailoverClustersModule},$diskLetter,$diskName
        remove-pssession -Session $session
    }

    function makeSelection{
        $input=Read-Host "Please pick an index number from the above list";
        write-host "Index value $input received.";
        if ($input -ne ""){
            $selection=$clusterDisks[$input];
            $status=$selection.Status
            $diskName=$selection.DiskName
            }else{
                write-host "Null input is invalid.";
                return $false;
                }
        if ($selection){
            if ($status){
                return $diskName;
                }else{
                    write-host "$diskName currently does not have corruption errors.";
                    return $false;
                    }
            }else{
                write-host "Index value $input is invalid.";
                return $false;
                }
    }

    write-host "Scanning $clusterName for all available disks. This may take a while...`r`n";
    $clusterDisks=getAllClusterDisks -clusterName $clustername;
    $attempts=1;
    do {
        cls;
        write-host "Attempt number: $attempts of $maxAttempts`r`n";
        $attempts++;
        $clusterDisks|ft -AutoSize;
        $diskLabel=makeSelection
        if ($diskLabel){
            $confirmed=confirmation;
            if($confirmed){
                write-host "Now fixing $diskLabel as selected.";
                fixClusterDisk -clusterName $clustername -diskName $diskLabel;
                }
            }
        if($attempts -gt $maxAttempts){break;}
        sleep 2;
    } while($diskLabel -eq $false)

}

function fixDisks($clusterNames){
    $clusterName=selectClusterName -clusterNames $clusterNames
    if ($clusterName){
        fixClusterDiskErrors -clusterName $clustername;
        }else{
                write-host "no valid cluster names have been selected.";
                
            }
}

####################### ↓ Windoze Update ↓ ##########################
function updateRemoteWindows{ 
    [CmdletBinding()] 
    param ( 
        [parameter(Mandatory=$true,Position=0)] 
        [string[]]$computer=$ENV:computername
        )
    
    <#
    .SYNOPSIS
    This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed, after reboot, windows updates will continue to run until no more updates are available.
    .PARAMETER URL
    User the Computer parameter to specify the Computer to remotely install windows updates on.
    # Around 20% of this snippet was forked from https://www.altaro.com/msp-dojo/powershell-windows-updates/
    # Partial credit: Luke Orellana
    # Note: I've added more features:
    # - Check WSUS settings to account for that scenario
    # - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
    # - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
    # - Check if server needs a reboot before issuing the reboot command instead of just inadvertently trigger reboots
    # - Fixed the blank lines in output log causing bug in status query
    # - More thorough cleanup routine
    # - Detect and handle proxies (in development)
    #>

    function installPrerequisites{
        $psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -ErrorAction SilentlyContinue;
        if (!($psWindowsUpdateAvailable)){
            try {                
                try{[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12}catch{}       
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
                Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
                Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
                Import-Module PSWindowsUpdate -force | Out-Null;                    
                }
            catch{
                "Prerequisites not met on $ENV:COMPUTERNAME.";
                }
            }         
        }

    function checkPendingReboot{
            param([string]$computer=$ENV:computername)

            function checkRegistry{
                 if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
                 if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
                 if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
                 try { 
                   $util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
                   $status = $util.DetermineIfRebootPending()
                   if(($status -ne $null) -and $status.RebootPending){
                     return $true
                   }
                 }catch{}
                 return $false             
            }

            $localhost=$ENV:computername
            if ($localHost -eq $computer){
                $result=checkRegistry;
             }else{
                $result=Invoke-Command -ComputerName $computer -ScriptBlock{
                                                                    param($importedFunc);
                                                                    [ScriptBlock]::Create($importedFunc).Invoke();
                                                                    } -ArgumentList ${function:checkRegistry}
                }
            return $result;
        }
    
    # Function requires 2 parameters: -computerName and -processName
    function killProcess{
        [CmdletBinding()]
        param(
          [string[]]$computerName=$ENV:COMPUTERNAME,
          [parameter(Mandatory=$true)][string]$executableName="powershell.exe"
        )
        
        # WMI querying method
        $processes = Get-WmiObject -Class Win32_Process -ComputerName $ComputerName -Filter "name='$executableName'" 
        
        if ($processes){
            foreach ($process in $processes) {
              $terminationResult = $process.terminate()
              $processid = $process.handle
 
            if($terminationResult.returnvalue -eq 0) {
              write-host "The process $executableName `($processid`) terminated successfully"
            } else {
                  write-host "The process $executableName `($processid`) termination has some problems"
                }
            }
        }else{
            "$executableName is currently not running on $computerName."
            }
    }

    function checkWsus{
        # Check if this machine has WSUS settings configured
	    $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
	    $wuKey="UseWUServer";
	    $wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
	    if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}    
    }

    # This function is incomplete, more updated snippet is available on KimConnect.com
    function turnoffWsus{
		# Turn WSUS settings OFF temporarily...
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
		setRegKey -path $wuPath -name $wuKey -value 0;
		restart-service wuauserv;        
        }

    function turnonWsus{
        # Turning WSUS settings back to ON status
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
		setRegKey -path $wuPath -name $wuKey -value 1;
		restart-service wuauserv;
        }

    Do{ 
        #starts up a remote powershell session to the computer
        do{
            $session = New-PSSession -ComputerName $computer
            write-host "Connecting to remote computer $computer..."
            sleep -seconds 1
            if ($session){write-host "Connected."}
            } until ($session.state -match "Opened")

        # Install prerequisites
        invoke-command -session $session -scriptblock {
            param($importedFunc);
            [ScriptBlock]::Create($importedFunc).Invoke();
            } -Args ${function:installPrerequisites}; 

        #retrieves a list of available updates 
        write-host "Checking for new updates on $computer";
        $updates = invoke-command -session $session -scriptblock {
            Import-Module PSWindowsUpdate -force -ErrorAction SilentlyContinue;
            # Determine whether Wsus is being used
            $wsusServiceId='3da21691-e39d-4da6-8a4b-b43877bcb1b7';
            $winUpdateServiceId='9482f4b4-e343-43b6-b170-9a65bc822c77';
            $registeredWindowsServices=Get-WUServiceManager;
            $defaultWsus=$registeredWindowsServices|?{$_.ServiceID -eq $wsusServiceId -and $_.IsDefaultAuService};
            $defaultWindowsUpdate=$registeredWindowsServices|?{$_.ServiceID -eq $winUpdateServiceId -and $_.IsDefaultAuService};  
            if ($defaultWindowsUpdate){
                write-host "Now checking Microsoft for patches...";
                $patches=Get-wulist -verbose -NotCategory 'Drivers' -NotTitle 'OneDrive' -ServiceID $winUpdateServiceId
                }else{
                    write-host "Now checking WSUS server for patches...";
                    $patches=Get-wulist -verbose -NotCategory 'Drivers' -NotTitle 'OneDrive' -ServiceID $wsusServiceId
                    }                      
            return $patches;
            } 

        # Count how many updates are available 
        $updatesCount = ($updates.kb).count                

        # If there are available updates proceed with installing the updates and then reboot the remote machine if required
        if ($updates -ne $null){ 
            #checkWsus;
            #if($wsus){turnoffWsus;}
                
            #Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
            $invokeScript = {
                Import-Module PSWindowsUpdate -force -ErrorAction SilentlyContinue;
                # Determine whether Wsus is being used
                $wsusServiceId='3da21691-e39d-4da6-8a4b-b43877bcb1b7';
                $winUpdateServiceId='9482f4b4-e343-43b6-b170-9a65bc822c77';
                $registeredWindowsServices=Get-WUServiceManager;
                $defaultWsus=$registeredWindowsServices|?{$_.ServiceID -eq $wsusServiceId -and $_.IsDefaultAuService};
                $defaultWindowsUpdate=$registeredWindowsServices|?{$_.ServiceID -eq $winUpdateServiceId -and $_.IsDefaultAuService};
                   
                # Perform Updates
                if ($defaultWindowsUpdate){
                    write-host "Windows Updates are set to download directly from Microsoft. Now checking for patches...";
                    Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot -NotCategory 'Drivers' -NotTitle 'OneDrive' | Out-File C:\PSWindowsUpdate.log;
                    }else{
                        write-host "Windows Updates are controlled by Wsus. Now checking for patches...";
                        Get-WindowsUpdate -AcceptAll -Install -IgnoreReboot -NotCategory 'Drivers' -NotTitle 'OneDrive' | Out-File C:\PSWindowsUpdate.log;
                        }
                }
            Invoke-WUjob -ComputerName $computer -Script $invokeScript -Confirm:$false -RunNow -ErrorAction SilentlyContinue|out-null
            
            #Show update status until the amount of installed updates equals the same as the count of updates being reported 
            sleep -Seconds 30 # Wait for the log file to generate
            $dots=80
            $dotsCount=0
            $lastActivity="";
            $installedCount=0;
            Write-Host "There is/are $updatesCount pending update(s)`n";
                        
            do {                
                $updatestatus = Get-Content "\\$computer\c$\PSWindowsUpdate.log"            
                $currentActivity=$updatestatus | select-object -last 1
                
                if (($currentActivity -ne $lastActivity) -AND ($currentActivity -ne $Null)){
                    Write-Host "Procesing $($installedCount+1) of $updatesCount updates."
                    Write-Host "`n$currentActivity";
                    $lastActivity=$currentActivity;
                    }else{
                        if ($dotCount++ -le $dots){
                            Write-Host -NoNewline ".";
                            if($installedCount -eq $updatesCount){Write-Host "Processing last update: $installedCount of $updatesCount."}
                        }else{
                            Write-Host ".";
                            $dotCount=0;
                            }                                                   
                        }                
                sleep -Seconds 10 
                $ErrorActionPreference = 'SilentlyContinue'                 
                $ErrorActionPreference = 'Continue'
                $installedCount = ([regex]::Matches($updatestatus, "Installed")).count
                }until ($installedCount -eq $updatesCount)
 
                #restarts the remote computer and waits till it starts up again
                if (checkPendingReboot $computer){                    
                    "`nReboots required.`nRestarting remote computer $computer to clear pending reboot flags." 
                    Restart-Computer -Wait -ComputerName $computer -Force;
                    "$computer has been restarted."
                }else{"No reboots required."}                        
            }    
    }until(($updates -eq $null) -OR ($installedCount -eq $updatesCount))

    function cleanup{
        <# Error when providing an Array variable in place of an expected String data type
        Test-NetConnection : Cannot process argument transformation on parameter 'ComputerName'. Cannot convert value to type System.String.
        At line:209 char:45
        +         if(test-netconnection -ComputerName $computer -port 445 -Erro ...
        +                                             ~~~~~~~~~
            + CategoryInfo          : InvalidData: (:) [Test-NetConnection], ParameterBindingArgumentTransformationException
            + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-NetConnection
        #>
        <# Unnecessary
        if(test-netconnection -ComputerName "$computer" -port 445 -ErrorAction SilentlyContinue -InformationLevel Quiet){
            if(Get-Process -ComputerName $computer powershell -ErrorAction SilentlyContinue){
                Write-Host "Terminating any powershell.exe processes."
                killProcess -ComputerName $computer -ExecutableName powershell.exe
                }
            } 
        #>
        <#       
        Test-Path : Cannot bind argument to parameter 'Path' because it is null.
        At line:216 char:23
        +         if (Test-Path $logFile -ErrorAction SilentlyContinue){
        +                       ~~~~~~~~
            + CategoryInfo          : InvalidData: (:) [Test-Path], ParameterBindingValidationException
            + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.TestPathCommand

        Test-Path : Cannot bind argument to parameter 'Path' because it is an empty string.
        At line:231 char:23
        +         if (Test-Path "$logFile" -ErrorAction SilentlyContinue){
        +                       ~~~~~~~~~~
            + CategoryInfo          : InvalidData: (:) [Test-Path], ParameterBindingValidationException
            + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.TestPathCommand
        #>
        $logFile="\\$computer\c$\PSWindowsUpdate.log"
        if (Test-Path "$logFile" -ErrorAction SilentlyContinue){
            Write-Host "Deleting log to prevent collisions with subsequent runs.";                       
            Write-Host "Removing $logFile."
            Remove-item $logFile | Out-Null
            }
        Write-Host "Removing the Invoke-WuJob's remnant, PSWindowsUpdate scheduled task, from $computer."
        invoke-command -computername $computer -ScriptBlock {
                            if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
                                Unregister-ScheduledTask -TaskName 'PSWindowsUpdate' -Confirm:$false};
                            }
        }
    cleanup;

    # Revert WSUS registry edits, if any
    #if($wsus){turnonWsus;}

    Write-Host "Windows is now up to date on $computer";
    <# Check scheduled task of remote computer in case of simultaneous updating
    $task=Invoke-Command -ComputerName $computer -ScriptBlock{(Get-ScheduledTask -TaskName "PSWindowsUpdate").State}
    if ($task.State -eq "Running"){"Still Running..."}
    #>
}

function updateLocalWindows{
    # Prerequisites
    function installPrerequisites{
        $psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -ErrorAction SilentlyContinue;
        if (!($psWindowsUpdateAvailable)){
            try {                
                try{[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12}catch{}         
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
                Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
                Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;                   
                }
            catch{
                write-host "Prerequisites not met on $computer.";
                }
            }
    Import-Module PSWindowsUpdate -force -ErrorAction SilentlyContinue | Out-Null;        
    }

    function checkPendingReboot{
            param([string]$computer=$ENV:computername)

            function checkRegistry{
                 if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
                 if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
                 if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
                 try { 
                   $util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
                   $status = $util.DetermineIfRebootPending()
                   if(($status -ne $null) -and $status.RebootPending){
                     return $true
                   }
                 }catch{}
                 return $false             
            }

            $localhost=$ENV:computername
            if ($localHost -eq $computer){
                $result=checkRegistry;
             }else{
                $result=Invoke-Command -ComputerName $computer -ScriptBlock{
                                                                    param($importedFunc);
                                                                    [ScriptBlock]::Create($importedFunc).Invoke();
                                                                    } -ArgumentList ${function:checkRegistry}
                }
            return $result;
        }
    installPrerequisites;

    # Register the user of Windows Update Service if it has not been registered
    $MicrosoftUpdateID="7971f918-a847-4430-9279-4a52d1efe18d"
    $registered=$MicrosoftUpdateID -in (Get-WUServiceManager).ServiceID
    if (!($registered)){
        Add-WUServiceManager -ServiceID 7971f918-a847-4430-9279-4a52d1efe18d -Confirm:$false # -AddServiceFlag 7 
        }

    $wsusServiceId='3da21691-e39d-4da6-8a4b-b43877bcb1b7';
    $winUpdateServiceId='9482f4b4-e343-43b6-b170-9a65bc822c77';
    $registeredWindowsServices=Get-WUServiceManager
    $defaultWsus=$registeredWindowsServices|?{$_.ServiceID -eq $wsusServiceId -and $_.IsDefaultAuService}
    $defaultWindowsUpdate=$registeredWindowsServices|?{$_.ServiceID -eq $winUpdateServiceId -and $_.IsDefaultAuService}
                   
    # Perform Updates
    if ($defaultWindowsUpdate){
        write-host "Windows Updates can be downloaded directly from Microsoft. Now checking for patches...";
        Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot -NotCategory 'Drivers' -NotTitle 'OneDrive';
        }else{
            write-host "Windows Updatea are bound to WSUS. Now checking for patches...";
            Get-WindowsUpdate -AcceptAll -Install -IgnoreReboot -NotCategory 'Drivers' -NotTitle 'OneDrive';
            }

    if (checkPendingReboot){
        $warning="There is a pending reboot flag on this host.`n";
        write-host $warning;
        return $false;
    }else{
        Write-host "$env:computername is patched.";
        return $true;
        }
}

function patchMsClusterNode($node,$originalClusterResources){
    write-host "Capturing original roles, move them out, suspend node...";
    $originalRoles=$originalClusterResources|?{$_.OwnerNode -eq $node}
    $clusterName=$originalRoles.ClusterName|select -Unique
    Get-ClusterNode -name $node -cluster $clusterName | Get-ClusterGroup | Move-ClusterGroup
    Suspend-ClusterNode -Name $node -cluster $clusterName

    write-host "Calling Windows Update function..."
    updateRemoteWindows -computer $node|out-null;

    write-host "Restoring roles to original owner node $node..."
    Resume-ClusterNode -Name $node -cluster $clusterName
    $originalRoles|%{Move-ClusterGroup -Name $_.Name -Node $node -Cluster $clusterName}
}

function patchMsClusterLocalNode($node,$originalClusterResources){
    write-host "Now patching the local node $env:computername..."
    write-host "Capturing original roles, move them out, suspend node..."
    $originalRoles=$originalClusterResources|?{$_.OwnerNode -eq $node}
    $clusterName=$originalRoles.ClusterName|select -Unique
    Get-ClusterNode -name $node -cluster $clusterName | Get-ClusterGroup | Move-ClusterGroup
    Suspend-ClusterNode -Name $node -cluster $clusterName

    write-host "Calling Windows Update function...";
    $rebootRequired=updateLocalWindows;

    write-host "Restoring roles to their original owner node $env:computername";
    if(!($rebootRequired)){
        Resume-ClusterNode -Name $node -cluster $clusterName|out-null;
        $originalRoles|%{Move-ClusterGroup -Name $_.Name -Node $node -Cluster $clusterName}
        write-host "Done.";
        }else{
            write-host "This computer has not finished patching. Please write these roles down > manually reboot > continue patching > return roles back:`r`n$originalRoles";
            }
}

function patchNodes($clusterNames){
    $timer=[System.Diagnostics.Stopwatch]::StartNew(); 
    # Get All Clusternodes and Roles
    $originalClusterResources=$clusterNames|%{
        $clusterName=$_;
        $roles = get-cluster -Name $clusterName|get-clustergroup  
        $roles|Select-Object @{Name='ClusterName';Expression={$clusterName}},OwnerNode,Name,State
        }|sort -Property ClusterName,OwnerNode
    
    # Determine whether current computer is on the list of Failover Cluster nodes
    $thisComputer=$env:computername
    $patchLocal=$False
    $nodes=getNodes $clusterNames;
    $remoteNodes=$nodes|%{
        $match=$_.Name -eq $thisComputer;
        if($match){
            $patchLocal=$True;
            }else{
                $_;
                }
        }

    $attempts=0;
    $maxAttempts=5;
    $testValue="Automatic";
    $automatic=$false;
    $cancel=$false;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "A maximum number of attempts have reached. No confirmations received!`r`n";
            $cancel=$true;
            break;
            }
        $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to enable Windows Update to run Automatically on ALL NODES!";
        if ($userInput.ToLower() -ne $testValue.ToLower()){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again..`r`n";
            }else{
                $automatic=$true;
                write-host "Automatic Mode Confirmed!`r`n";
                break;
                }
        }
    if(!($cancel)){
        # Perform Windows update on each remote node
        foreach ($node in $remoteNodes){
            if($automatic){
                write-host "Now patching $($node.Name)...`r`n";
                patchMsClusterNode $node.Name $originalClusterResources;
                }else{
                    $confirmed=confirmation;
                    if($confirmed){
                        write-host "Now patching $($node.Name)...`r`n";
                        patchMsClusterNode $node.Name $originalClusterResources;
                        }else{
                            write-host "No confirmations to proceed received. Program now exits.";
                            break;
                            }
                    }
            }

        # Lastly, perform windows update on this node
        if($patchLocal){
        patchMsClusterLocalNode $thisComputer $originalClusterResources;
        }  
    }
    $timeElapsedHours=[math]::round($timer.Elapsed.TotalHours,2);
    write-host "Total patch time: $timeElapsedHours hours."
}
####################### ↑ Windoze Update ↑ ##########################

function pickList($list,$maxAttempts=3){
    $display="";
    write-host $drawLines;
    for ($i=0;$i -lt $list.count;$i++){
        $display+="$($i+1)`: $($list[$i])`r`n";
        }
    $lines=($display | Measure-Object –Line).Lines
    $menu="`r`n============================================`r`nCommand Numbers:`r`n============================================`r`n"
    $menu+="$display============================================`r`n"
    write-host $menu

    $attempts=0;
    $proceed=$false;
    $pickedIndex=-1;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            $proceed=$false;
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        
        # Sanitize input, converting String to Integer
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $pickedIndex=$value-1
                $pickedItem=$list[$pickedIndex];
                write-host "$userInput corresponds to $pickedItem`r`n";
                $proceed=$true;
                break;
                }
        } 
    return @($proceed,$pickedIndex)
}

function getCommand{
    $pickItem=picklist $functions.Function
    if ($pickItem[0]){
        $command=$functions.Command[$pickItem[1]];
        }else{
            $command="write-host 'Good Bye!'";
            }
    return $command;
}

function loopProgram{

    do {
        $command=getCommand
        write-host "Please verify this command: '$command'`r`n"
        $userInput = Read-Host -Prompt "Type 'yes' or 'y' to confirm. Type 'no', 'n', 'exit', 'quit', or 'cancel' to terminate";
        
        if ($userInput.ToLower() -match 'yes|y|exit|quit|cancel|no|n'){
            if ($userInput.ToLower() -match 'yes|y'){
                write-host "Confirmation received. Now executing '$command'`r`n";                    
                }else{
                    write-host "'$userInput' is received as a command to exit this program.`r`n";
                    $command=$false;
                    }
            $exitLoop = $true;
            }else{
                cls;
                write-host "'$userInput' is an invalid value. Try again..`r`n";
                $exitLoop = $false;
                }
        } while($exitLoop -eq $false) 
    return $command;      
}

function runProgram{
    #write-host "Initiating scan of all Failover Cluster Nodes in $env:USERDNSDOMAIN.. This may take awhile.`r`n";
    $header="`r`n============================================`r`n"
    $header+="Here is the current status of all the nodes:`r`n============================================`r`n"
    $header+="$(displayNodes $nodes)============================================`r`n";
    write-host $header
    
    $command=loopProgram;
    if($command.gettype() -eq [string]){
        Invoke-Expression $command;
        }else{
            write-host "Good bye!"
            } 
}

write-host "Prepopulating some global variables. Please wait..."
if(!($clusterNames)){$clusterNames=getClusterNames;}
if(!($nodes)){$nodes=getNodes $clusterNames;}
cls;

runProgram;

Leave a Reply

Your email address will not be published. Required fields are marked *