dsccommunity / SharePointDsc

The SharePointDsc PowerShell module provides DSC resources that can be used to deploy and manage a SharePoint farm
MIT License
247 stars 107 forks source link

SPProductUpdate: Add possibility to use Zero-Downtime Patching with SPDsc #1096

Open ykuijs opened 5 years ago

ykuijs commented 5 years ago

@andikrueger and I have been discussing the possibility to patch SharePoint using Zero-Downtime Patching with SharePointDsc. Since this requires directing traffic to other servers in the SharePoint farm, this will require implementing a solution to make sure the server is not serving traffic.

We both see that a lot of companies don't configure their load balancing the proper way. Their appliances only ping the SharePoint WFE IP addresses. If the IIS services are stopped or not responding (intentionally or because of some sort of issue), the server doesn't serve any requests, but the load balancer still gets a ping response and considers the server as operational.

Therefore load balancer solutions should at least check for HTTP status codes or better yet, retrieve a page and check for specific content every 10 to 15 seconds.

If this is the case, there is a possibility to do ZDP with SPDsc:

  1. Prepare WFE by fooling the LB by either: a. Create a IIS Rewrite rule, to send out 503 status codes b. Changing the IP of the SharePoint Web application binding - changing the HTTP listening port c. Setting a firewall rule, that blocks all traffic coming from the load balancer IP (which is the easiest to create) d. Create a new IIS Web Application that catches all the traffic to that server and does a redirect to the IP address of any other WFE (with the host-header attached) e. Have a PowerShell script check the local web applications and create an HTML file in an anonymous web site. If the checks are good, the file contains the text "Ok". If the checks fail, the file contains the text "Failed". The LB checks this HTML file and depending on the result, considers the server as operational or not. f. Any other ways to temporally disable this WFE from taking request issued by the LB.
  2. Now the LB should route the traffic to the remaining WFEs.
  3. Enable Side-by-side on WFE 1
  4. Install the patches
  5. Reboot the server
  6. Disable the LB fooling solution on this WFE
  7. Prepare WFE 2 by using either method mentioned in 1.
  8. Proceed with the patching on this server, etc

We are curious what other think of this solution? If there are any other ideas that might be better as the ones suggested above.

andikrueger commented 5 years ago

Assuming, we do have a proper LB solution in place, there would still be the need to create a new SPZeroDownTimePatching resource. This resource would be needed two times per WFE per configuration to enable side-by-side and disable side-by-side properly.

ykuijs commented 5 years ago

I do not agree with creating a new resource. The reason for this is that one resource would have the enabled state and the other the disabled state, so these will always be in conflict with each other and the server will never be in a compliant state. Each time a configuration is applied, the first resource would enabled SbS and the second would disable it again. If "Apply and Autocorrect" is configured, this would constantly enabled/disabled during each consistency run.

I rather would extend the SPProductUpdate resource to enable side-by-side at the beginning of the set method and to disable it at the end of the set method.

andikrueger commented 5 years ago

Wouldn’t extending the Set-Method lead to one WFE being more up-to-date than another (not yet patched) WFE and in this way impact the user experience?

andikrueger commented 5 years ago

I did some drawing this morning. The following picture shows the several steps needed to patch a 2 WFE with several App Servers Farm with DSC and ZDP.

Zero Downtime Patching

Following color codes for the drawing:

jimbrayDel commented 5 years ago

I have thought about this and the best thought I had was a script wrapper around the DSC resources to achieve the goal. I thought the enabling of SideBySide was best handled with a Function as well as setting the proper version. Then for dealing with the farm to do true ZDP, you can only execute on half the farm resources. I worked with Nik and this seems feasible within my DSC script to identify the Nodes AllNodes = @( @{ NodeName = "Servername-CA" CertificateFile = "\FAAMECK003\E$\DSCConfig\1Certificate\DSCAuthor.cer" Thumbprint = "?97815e8e2c4906e5b22bae09eaf3aec0e801f432" PSDscAllowDomainUser = $true Role = "WebSet1" }

The Role allows me to have a set of WFE, App, and DC servers. Then when calling the upgrade installation and such you just have

Node $AllNodes.Where{$_.Role -eq $NodeType}.NodeName The $NodeType is passed or changed in the wrapper for which "SET" of WFE's to do, Set1 or Set2, so that the DSC execution is controlled and operating on exactly the half of the farm controlled by the wrapper. All of this to be run from an external server to the farm so that each server could be rebooted without losing context of where the upgrade process was.

Apologies if this is considered insane, I just was trying to figure out a method to do it on my own, I should have checked here sooner ..

ykuijs commented 5 years ago

Hi @jimbrayDel Not entirely sure what you mean here. Can you elaborate a little more?

andikrueger commented 5 years ago

As I see it @jimbrayDel would introduce a new key-value pair Role="WebSet1" or Role="WebSet2" for every node. With having this information in the configuration file, we would be able to handle the patch process more efficiently, as we would be able to identify the farm servers more easily, that needs to be patched simultaneously.

Using Role is ambiguous, as it it conflicts with the MinRoles. I would use ServerGroup as Key.

jimbrayDel commented 5 years ago

@andikrueger Exactly what I was trying to say, I agree with your thought that Role could be confusing, and ServerGroup would work just fine. The Goal I am working on is to have the ZDP be completely automated without any manual intervention required.

kayodebristol commented 5 years ago

@ykuijs @andikrueger Trying to make this as simple as possible.

Steps:

  1. Server #1 of the role gets placed into ServerGroup1, Server #2 of the role gets placed into ServerGroup2.
  2. Remove ServerGroup1 from load balancer
  3. SPProductUpdate ServerGroup1
  4. Return ServerGroup1 to load balancer
  5. Remove ServerGroup2 from load balancer
  6. SPProductUpdate ServerGroup2
  7. Return ServerGroup2 to load balancer
  8. PSConfigWizard ServerGroup1
  9. PSConfigWizard ServerGroup2
  10. Set SideBySideToken = highest # folder name in hive on all servers (foreach WebApplication)
Configuration SPFarmUpdateZDT
{
    $CredsSPFarm = Get-Credential -Message "Farm Account Service Account"
    Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 3.6.0.0
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    #ServerGroup 1
    Node $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
    {
        Script EnableSideBySideFarmWide
        {
            SetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                Write-Verbose "Enabling Side By Side Farm Wide"   
                foreach($webApp in $webapps){
                    $Webapp.WebService.EnableSideBySide = $true
                    $WebApp.Update()
                }
            }
            TestScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.EnableSideBySide -eq $false){
                        return $false
                    }
                }
                return $true
            }
            GetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.EnableSideBySide -eq $false){
                        return $false
                    }
                }
                return $true
            }
        }
        Script BlockLoadBalancer{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $true
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $false            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }            
        }
        SPProductUpdate ServerGroup1
        {
            SetupFile = "C:\Patch\CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
        WaitForAll SPProuductUpdateAllNodes
        {
            ResourceName = "[SPProductUpdate]ServerGroup2"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 2}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
            DependsOn = "[SPProductUpdate]ServerGroup1"
        }
        SPConfigWizard ServerGroup1
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            IsSingleInstance = "Yes"
            DependsOn = "[WaitForAll]SPProuductUpdateAllNodes"

        }
        Script UnBlockLoadBalancerServerGroup1{

            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $false
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $true            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
    }
    #ServerGroup 2
    Node $AllNodes.Where{$_.ServerGroup -eq 2}.NodeName
    {
        WaitForAll UnBlockServerGroup1
        {
            ResourceName = "[Script]UnBlockLoadBalancerServerGroup1"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
        }
        Script BlockLoadBalancerServerGroup2{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            # Must return a boolean: $true or $false            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $true
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $false            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }            
            DependsOn = "[WaitForAll]UnblockServerGroup1"
        }
        SPProductUpdate ServerGroup2
        {
            SetupFile = "C:\Patch\CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn = "[Script]BlockLoadBalancerServerGroup2"
        }
        WaitForAll SPConfigWizardServerGroup1
        {
            ResourceName = "[SPConfigWizard]ServerGroup1"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
        SPConfigWizard ServerGroup2
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            IsSingleInstance = "Yes"
            DependsOn = "[WaitForAll]SPConfigWizardServerGroup1"
        }
        Script SetSideBySideTokenFarmWide
        {
            SetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $path = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\Layouts")
                Folders = Get-ChildItem $path -Directory | Where-Object name -match '\d+.\d+.\d+.\d+'
                $latest = ''
                switch($folders.count){
                    0: {break;}
                    1: {$latest = $folders.name}
                    2: {
                        if( ($folders[0].name).replace('.','') -gt ($folders[1].name).replace('.','') ){
                            $latest = $folders[0].name
                        }
                        else{$latest = $folders[1].name}
                    }
                }
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($WebaApp.WebService.EnableSideBySide -eq $true){
                        $Webapp.WebService.SideBySideToken = $latest
                        $WebApp.Update()
                    }
                }
            }
            TestScript = {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $path = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\Layouts")
                Folders = Get-ChildItem $path -Directory | where name -match '\d+.\d+.\d+.\d+'
                $latest = ''
                switch($folders.count){
                    0: {break;}
                    1: {$latest = $folders.name}
                    2: {
                        if( ($folders[0].name).replace('.','') -gt ($folders[1].name).replace('.','') ){
                            $latest = $folders[0].name
                        }
                        else{$latest = $folders[1].name}
                    }
                }
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.SideBySideToken -ne $latest -and $WebApp.WebService.EnableSideBySide){
                        return $false
                    }
                }
                return $true
            }
            GetScript = {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                return @{Result = $WebApps | ForEach-Object{
                    @{DisplayName = $_.DisplayName; Url = $_.Url; SideBySideToken = $_.WebService.SideBySiteToken}
                    }
                }
            }
            DependsOn = "[SPConfigWizard]ServerGroup2"
        }
        Script UnBlockLoadBalancer{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $false
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $true            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
    }
}
$ConfigData = @{
    AllNodes = [array] ((Get-SPFarm).Servers | Where-Object Role -ne "Invalid" | ForEach-Object{
        @{
            NodeName = $_.Address;
            ServerGroup = $_.Address -replace '\D', '';
            LoadBalancerIPAddress = "10.10.10.1";
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        }
    })
}
SPFarmUpdateZDT -ConfigurationData $ConfigData

New to DSC, would appreciate feedback, comments.

ykuijs commented 5 years ago

Hi @kayodebristol

Thanks for sharing your idea. I see one major issue with this idea: DSC isn't meant for deploying separate configurations for each change. You basically update a configuration with the change you want to implement and DSC goes out and does that change for you. So you specify the state you want the environment to be in.

In your example, you specified two script resource per server group that will cause issues: For ServerGroup1, these are "BlockLoadBalancer" and "UnBlockLoadBalancerServerGroup1" For ServerGroup2, these are "BlockLoadBalancerServerGroup2" and "UnBlockLoadBalancer"

The reason these two resource will cause issues is that the desired state of these resources are the exact opposite of each other. So it is impossible that the server will ever be in a compliant state, since it will always be incompliant with one or the other.

So basically you created a configuration that specifies a value to be 0 and 1 at the same time. If you configure the Local Configuration Manager as "Apply and Autocorrect", this means every 15 minutes (by default) traffic is blocked by the BlockLoadBalancer resource (since the firewall rule doesn't exist and therefore the server is incompliant) and shortly after that the traffic is allowed again by the UnBlockLoadBalancer resource (since the firewall rule does exist and therefore the server is incompliant).

If we want to enable this scenario, we need to implement this routine in the SPProductUpdate resource.

kayodebristol commented 5 years ago

@ykuijs Great explanation, thank you. Totally get it. There might be another way, if one could detect when SPProductUpdate was running... maybe with a mutex? Nix the UnBlockLoadBalancer script resources and change the BlockLoadBalancer Script resource to something like this:

`Script BlockLoadBalancerWhilePatching{ SetScript { Try { $mutex = [Theading.Mutex]::OpenExisting('Global[SPProductUpdate Mutex Name]'); #not sure if there is such a thing, but if there is, Loadbalancer will be blocked New-NetFirewallRule -Direction Inbound -DisplayName "SP Block Load Balancer While Patching - DSC" -Name "SPBLBWPDSC" -RemoteAddress $remoteAddress -Action Block -ErrorAction SilentlyContinue; $mutex = $null; } Catch {

OpenExitisting throws an exemption if the mutex name is not found

             if($null -eq $mutex){ $Remove-NetFireWallRule -Name SPBLBWPDSC }
             else{$mutex = $null}
        }
}

} `

Initially, I thought I could use 'Global_MSIExecute' to detect it, but that didn't work in testing. Did I get lost in the rabbit hole, again, maybe?

andikrueger commented 5 years ago

Did you have a look at this idea: #1097?

kayodebristol commented 5 years ago

@andikrueger Yes. #1097 Deals with EnableSideBySide & SideBySideToken. I handle that with EnableSideBySideFarmWide and SetSideBySideTokenFarmWide Script resources in SPFarmUpdateZDT Configruation, above.

@ykuijs pointed out that my handling of the loadbalancer (UnBlockLoadBalancer & BlockLoadBalancer script resources) won't work. So, my previous post was specific to addressing the loadbalancer part of the problem.

I read somewhere that writing script resources was a good first step for creating custom resources... I think I'm too green to write a custom resource, but I want to contribute. Thought submitting script resources that could accomplish ZDTP would be beneficial. No?

andikrueger commented 5 years ago

@kayodebristol your input is highly appreciated!

Yes, the networking part is challenging. As @ykuijs pointed out we can’t have to resources within one configuration that do say “do it” and “don’t do it” at the same time. The same applies to the SideBySidePart.

The easiest solution I can think of (and @ykuijs mentioned above) is to extend the SPProductUpdate Resource with a switch parameter ZDP. This switch would work like ShutdownServices and would disable incoming network traffic.

We won’t need it for

Doing it this way won’t conflict in the configuration.

I’m happy to create some draft resources for ZDP with DSC.

What’s your opinion on the extension described above?

kayodebristol commented 5 years ago

@andikrueger

Looks like I missed something. Node should be pulled out of loadbalancer during SPProductUpdate and SPConfigWizard steps. It doesn't matter that SPConfigWizard shuts down services. Dumb loadbalancer will still direct traffic to node until TCP response is blocked.

Think I'm in favor of a separate resource instead of extending existing... I would ideally like the node removed from the loadbalancer during all "patch" activities (Windows Update, SPProductUpdate, & SPConfigWizard). Perhaps a property of this resource would be a Trigger = All (default) | Windows | SharePoint. Just a thought.

Still not tracking on the SideBySide piece, however. Assumptions:

  1. Once EnableSideBySide is set to $true for a SPWebApplication, it can stay enabled for the life of the SPWebApplication. zero downtime patching steps never mention setting EnableSideBySide back to false.
  2. SideBySideToken value needs to change, but the configuration stays the same, latest (highest version number folder name). The zero downtime patching steps, above, set the SideBySideToken to the latest at the end of the patching process, that's it. The SetSideBySideTokenFarmWide script resource accomplishes that and DependsOn = "[SPConfigWizard]ServerGroup2" (all servers have the latest code). The SideBySideToken value will change, but always to the latest folder version. The configuration, apply the latest, remains the same throughout.

Am i still missing something?

andikrueger commented 5 years ago

I think, there is a challenge with your approach of a seperate resource. Within the resource that removes the node from load-balancing we would need to know, if this node would need to install any updates. Otherwise this node would block traffic and would need to allow traffic seconds later during a configuration run.

This block and do not block is my main concern. This is similar to https://github.com/PowerShell/SharePointDsc/issues/1096#issuecomment-549985611

AFAIK running config wizard will stop the IIS service. This is equal to set blocking firewall rules in place. The server wont answer to your request.

This leaves us with the SPProductUpdate resource. Within the Set-Method of this resource, we could easily block the network traffic and unblock the traffic too. Doing so will not conflict.

The part about the SideBySide Token:

  1. SharePoint allows to set and unset the property. We should have this ability in DSC too. That's why there should be option available to disable side by side.
  2. Yes. We would need to set to the highest version number at the end of the process.
kayodebristol commented 5 years ago

@andikrueger I disagree. User gets 404 if server doesn't reply. That's not zero down time. The blocking firewall rule is used because even the most basic load balancer does a ping (except perhaps DNS load balancing, but you can't help everyone). If the node doesn't ping, it is removed.

SideBySide settings have the specific goal of having users get the same code. It doesn't facilitate zero down time at all. Just consistency.

Zero down time to me means that the user never gets a 404. So, the load balancer never sends a request to a server that's not ready to handle that request. So, node should be removed from load balancer during "patch" activities. Placed back in load balancer patch activity completes.

The only requirement, as far as timing goes, is that the node must be placed back in the load balancer before it's partner node or ServerGroup is removed. Seconds not required. WaitForAll can do that.

SideBySideToken and EnableSideBySide are 2 separate SPWebApplication Settings.

$WebApp.WebService.EnableSideBySide = $false (default) | $true #This directs SharePoint to copy code to the version named folder

$WebApp.WebService.SideBySideToken = "" (Default) | x.x.x.x (folder name) #Tells SharePoint which folder to serve code from.

If you turn on ZDT patching with it's own resource, i.e. Enabled = $true, SideBySideToken = [Latest folder] & EnableSideBySide = $true. Enabled = $false, SideBySideToken = '' & EnableSideBySide = $false. But, once you go ZDT why would you go back? Lost a node, so you can't do high availability. OK, still doesn't make sense to turn it off. Imagine a single server farm, if you patch it you have down time. No matter your SideBySide setting your down time will be the same. Actually, thinking about, it should be on by default. But that's a different team.

Yes, figuring out how to implement the block during certain times is a challenge, but you have to extend both SPProductUpdate and PSConfigWizard with a redundant setting. And it doesn't help during Windows patching. A separate resource is harder, I get it, but it's the more elegant solution, if we can figure it out.

The only problem I see is being able to detect when SPConfigWizard, PSProductUpdate, or Windows Updates are in progress... If can't figure that out, then extending both resources is the only option.

andikrueger commented 5 years ago

You are right about the 404 status code. I'm totally with you in case of ZDP, the user should not be confronted with any 404 or any other error messages. From a users perspektiv: SharePoint should be up an running.

@ykuijs and I had a discussion about this technical debt before and we came to the conclusion, that we would need a basic LB-guidance:

Therefore load balancer solutions should at least check for HTTP status codes or better yet, retrieve a page and check for specific content every 10 to 15 seconds. see: https://github.com/PowerShell/SharePointDsc/issues/1096#issue-469922845

This would eliminate every ping based solution, which is very error prone and basically not recommended for production.

jimbrayDel commented 5 years ago

@kayodebristol I think what @andikrueger is referring to for the Load balancer is most intelligent devices today especially BigIP can be configured to check for a static page on IIS and based on the text can remove or add a server to the working pool. In our case we have a html page with a simple up/down that the BigIp reads if the text is "Down" then it removes the server from the pool, and vice versa. We had already automated this ability for some of our scripting.

Function F5Status ($status,$WebServ) # used to set Status.HTML for servers to remove from loadbalancing and to add back to load balancing {
$line1 = "" $line3 = "" $nwline = "rn" $content = $line1 + $nwline + $status + $nwline + $line3 $uncPath = "F$\inetpub\wwwroot" Remove-Item "\$webserv\$uncPath\status.html" -ErrorAction SilentlyContinue New-Item "\$webserv\$uncPath\status.html" -type File Add-Content -Path "\$webserv\$uncPath\status.html" $content if (Select-String -Path "\$webserver\$uncPath\status.html" -Pattern $status) {Write-Output "\$webserver\$uncPath\status.html done."}

}

jimbrayDel commented 5 years ago

Interesting the pasting action removed some text from the function. here it is with the characters escaped Function F5Status ($status,$WebServ) # used to set Status.HTML for servers to remove from loadbalancing and to add back to load balancing {
$line1 = "''" $line3 = "''" $nwline = "rn" $content = $line1 + $nwline + $status + $nwline + $line3 $uncPath = "E$\inetpub\wwwroot" Remove-Item "\$webserv\$uncPath\status.html" -ErrorAction SilentlyContinue New-Item "\$webserv\$uncPath\status.html" -type File Add-Content -Path "\$webserv\$uncPath\status.html" $content if (Select-String -Path "\$webserver\$uncPath\status.html" -Pattern $status) {Write-Output "\$webserver\$uncPath\status.html done."}

}

jimbrayDel commented 5 years ago

$line1 = "\ H.T.M.L" $line3 = "\</H.T.M.L>'"

anyway if this doesn't work, line 1 and line 3 are the open and closed anchors for H-T-M-L

andikrueger commented 5 years ago

@jimbrayDel thank you for the example. This is what I was referring to. There are so many appliances that offer these options to check for status pages or static content.

kayodebristol commented 5 years ago

@jimbrayDel In theory, Yes. In practice, not so much. ADFS authentication sends back a redirect, which even the most advanced load balancers have issues with. A static, anonymous auth site, sure. But now I've got my security guys yelling at me. Maybe Windows auth on the site with a service account on my load balancer. Now one service account can take down my whole farm (deleted, password changed, expired, etc...).

The firewall rule is the most elegant, and handles most cases, almost all loadbalancers/configs. I thought the only question was if both SPProductUpdate and PSConfigWizard would have to be extended. I think so.

I'm not saying something else wouldn't work... just that it's not as elegant and doesn't handle as many use cases.

Would love to be able to say, "Patching SharePoint? Just run this DSC, fogetaboutit!" Disclaimer: Must have high availability, minimum to TCP health monitoring, avoid contact with eyes and skin and avoid inhaling fumes. Don't try this in your living room; these are trained professionals.

jimbrayDel commented 5 years ago

@kayodebristol I am not sure I understand about the ADFS, what I was refeering to is removing the server from the load balancer pool. This would eliminate any traffic to the server which would be before any ADFS authentication would even take place. Also on pre-existing users that are already on a server the load balancer should relocate them once you take the server out of the pool. That is unless you are using "Persistence" or "Sticky" configurations that locks the Load balancer on a particular server, but that practice is not recommended. We have Distributed Cache service so we should not need sticky or persistence any more, we can allow users to bounce around. What am I missing or overlooked?

kayodebristol commented 5 years ago

@jimbrayDel I get it. Some security Nazis in certain environments will call that unauthenticated content. Crazy, I agree. But some folks even block icmp between servers. Even using authenticated content can be problematic. Blocking all inbound traffic with a firewall rules doesn't work in every case, but it works in most cases, and requires less effort than altering a status.html file, is all.

kayodebristol commented 5 years ago

Was reminded that Windows Firewall is also disabled in some environments in favor of other 3rd party tools like McAfee. Perhaps we leave load balancing out for the time being and just focus on EnableSideBySide & SideBySideToken? Folks with intelligent load balancers will benefit and folks without will at least have one less manual step.

andikrueger commented 5 years ago

Good point with 3rd Party tools. That’s why we would need to focus on one scenario. I would still go with Windows Firewall for the time being and make this option optional.

Side by side: YES!

Let me try to provide an experimental PR for ZDP. I have something in mind.

kayodebristol commented 5 years ago

@andikrueger Awesome! I revised my example configuration (SPZDPDSCConfig). Removed load balancing stuff, and cleaned up a bit to only do the SideBySide stuff on localhost. Maybe it will be useful in some way.

ThomasLie commented 4 years ago

Well, you should consider that some companies will use GPOs to configure Firewall settings, In this case you won't be able to activate or deactivate any firewall rules...PS will not return any errors, however the setting will not be effective.

Why not simply stopping IIS, this should cause any LB to switch to any other WFE...

Regards, Thomas

andikrueger commented 4 years ago

Good feedback on Firewall Settings beeing controlled by GPO.

@ykuijs and I discussed "stopping IIS" before. By stopping IIS we will not be able to do some manual testing during the update process.

JvanderMeer-GitHub commented 3 years ago

Hi All,

I have create an example for patching the Smallest HA MinRole farm topology (4 servers).

The idea is that you would add you own logic for any preparations needed, such as removing a server from the load balancer. After PSCONFIG I randomly encountered situations where PSCONFIG just stopped, I could trace back the cause of this. Just to ensure that the upgrade was completed I run PSCONFIG again.

Below the example, any feedback is appreciated that optimize the code.

Configuration SPFarmUpdate
{
    $CredsSPFarm = Get-Credential -UserName $ConfigurationData.NonNodeData.SetupAccount -Message "Farm Account"
    Import-DscResource -ModuleName SharePointDSC
    Import-DscResource -ModuleName PSDesiredStateConfiguration

    $firstWFE = ($AllNodes | Where-Object { $_.Role -eq "WFEDC" } | Select-Object -First 1).NodeName
    $secondWFE = ($AllNodes | Where-Object { $_.Role -eq "WFEDC" } | Select-Object -Skip 1).NodeName
    $firstAPP = ($AllNodes | Where-Object { $_.Role -eq "APP" } | Select-Object -First 1).NodeName
    $secondAPP = ($AllNodes | Where-Object { $_.Role -eq "APP" } | Select-Object -Skip 1).NodeName

    $PatchLogFile = $ConfigurationData.NonNodeData.PatchLog
    $stsPatchFileName = $ConfigurationData.NonNodeData.stsPatchFileName
    $wsslocPatchFileName = $ConfigurationData.NonNodeData.wsslocPatchFileName

    $Patch1Location = (Join-Path -Path $ConfigurationData.NonNodeData.PatchLocation -ChildPath $ConfigurationData.NonNodeData.stsPatchFileName)
    $Patch2Location = (Join-Path -Path $ConfigurationData.NonNodeData.PatchLocation -ChildPath $ConfigurationData.NonNodeData.wsslocPatchFileName)

    $PreparationSourceLocation = $ConfigurationData.NonNodeData.PreparationSourceLocation
    $PreparationSourceDestination = $ConfigurationData.NonNodeData.PreparationSourceDestination

    Node $firstWFE
    {
        #Copy scripts needed for patching process.
        File ScriptSources
        {
            Ensure          = "Present"
            Type            = "Directory"
            Recurse         = $true
            SourcePath      = $PreparationSourceLocation
            DestinationPath = $PreparationSourceDestination
            Checksum        = "modifiedDate"
        }

        #Preparations needed for starting the patching phase - e.g. removing server from the loadbalancer through a script.
        #This will only execute if there is no log file or the log file doesnt contain the expected content.
        Script PrepareServerStage1
        {
            SetScript            = { #Preparation logic - create your own logic needed for preparations
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[File]ScriptSources"
        }

        #Installation of the first patch
        SPProductUpdate InstallPatch1
        {
            SetupFile            = $Patch1Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]PrepareServerStage1"
        }

        #Creation log and entry if the first patch has been installed
        Script CreateLogPatch1
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value $using:stsPatchFileName
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value $using:stsPatchFileName
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch1"
        }

        #Installation of the second patch
        SPProductUpdate InstallPatch2
        {
            SetupFile            = $Patch2Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]CreateLogPatch1"
        }

        #Run Get-SPProduct
        Script SPProduct
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock { Get-SPProduct -Local }
            }
            TestScript           = { $false }    # YK: Waarom hier altijd False teruggeven? Betekent dat hij altijd het SetScript gaat uitvoeren.
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch2"
        }

        #Creation log and entry if the second patch has been installed
        Script CreateLogPatch2
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value $using:wsslocPatchFileName
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value $using:wsslocPatchFileName
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]SPProduct"
        }

        #Finalize steps needed during patch installation phase - e.g. addding the server back to the loadbalancer.
        Script FinalizeServerStage1
        {
            SetScript            = { #Finalize logic - create your own logic needed for the finalizing - create your own logic needed for the finalizing
            }
            TestScript           = { #Finalize logic - create your own logic needed for the finalizing - create your own logic needed for the finalizing
            }
            GetScript            = { #Finalize logic - create your own logic needed for the finalizing - create your own logic needed for the finalizing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]CreateLogPatch2"
        }

        #Wait for other servers to have the patches installed
        WaitForAll ServersToHaveBinariesInstalled
        {
            ResourceName     = "[Script]FinalizeServerStage"
            NodeName         = @($secondWFE)
            RetryIntervalSec = 300
            RetryCount       = 120
            DependsOn        = "[Script]FinalizeServerStage1"
        }

        #Preparations needed for starting the patching phase - e.g. removing server from the loadbalancer through a script or perform a gracefull shutdown of the distributed cache.
        #This will only execute if there is no log file or the log file doesnt contain the expected content.
        Script PrepareServerStage2
        {
            SetScript            = {
                $scriptpath = $using:PreparationSourceDestination
                Invoke-SPDscCommand -ScriptBlock {
                    & (Join-Path -Path $args[0] -ChildPath "DistributedCacheGracefulShutdown.ps1" )
                } -Arguments $using:PreparationSourceDestination
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains "$(Get-Date -format "yyyyMMdd") psconfig")
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[WaitForAll]ServersToHaveBinariesInstalled"
        }

        #Make sure these services are running else psconfig might fail
        ServiceSet Services
        {
            Name                 = @("W3SVC", "NetPipeActivator", "NetTcpActivator", "SPTimerV4" , "SPTraceV4")
            StartupType          = "Automatic"
            State                = "Running"
            DependsOn            = "[Script]PrepareServerStage2"
            PsDscRunAsCredential = $CredsSPFarm
        }

        #Run psconfig
        SPConfigWizard PSConfig
        {
            IsSingleInstance     = "Yes"
            Ensure               = "Present"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[ServiceSet]Services"
        }

        #Run PSConfig to make sure everything is upgraded
        Script PSConfig
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock {
                    PSConfig.exe -cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install
                }
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    $statusType = Get-SPDscServerPatchStatus
                    if ($statusType -eq "UpgradeRequired" -or $statusType -eq "UpgradeAvailable" -or $statusType -eq "UpgradeInProgress" )
                    {
                        $false
                    }
                    else
                    {
                        $true
                    }
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPConfigWizard]PSConfig"
        }

        #Creation log and entry if psconfig has been run
        Script CreateLogPSConfig
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value "$(Get-Date -format "yyyyMMdd") psconfig"
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value "$(Get-Date -format "yyyyMMdd") psconfig"
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains "$(Get-Date -format "yyyyMMdd") psconfig")
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]PSConfig"
        }

        #Finalize steps needed for the patch installation phase
        #Finalize steps needed during patch installation phase - e.g. addding the server back to the loadbalancer. Starting the Distributed cache server before that.
        Script FinalizeServerStage2
        {
            SetScript            = {
                $scriptpath = $using:PreparationSourceDestination
                Invoke-SPDscCommand -ScriptBlock {
                    & (Join-Path -Path $args[0] -ChildPath "DistributedCacheStart.ps1" )
                } -Arguments $using:PreparationSourceDestination
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    if (Get-SPServiceInstance | ? { ($_.service.ToString()) -eq "SPDistributedCacheService Name=AppFabricCachingService" -and ($_.server.name) -eq $env:computername } | ? { $_.status -eq "Online" } )
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                }
            }
            GetScript            = { # Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]CreateLogPSConfig"
        }
    }

    Node $secondWFE
    {
        #Wait for other servers to have the patches installed
        WaitForAll FirstWFEToHaveBinariesInstalled
        {
            ResourceName     = "[Script]FinalizeServerStage1"
            NodeName         = @($firstWFE)
            RetryIntervalSec = 300
            RetryCount       = 120
        }

        #Copy scripts needed for patching process.
        File ScriptSources
        {
            Ensure          = "Present"
            Type            = "Directory"
            Recurse         = $true
            SourcePath      = $PreparationSourceLocation
            DestinationPath = $PreparationSourceDestination
            DependsOn       = "[WaitForAll]FirstWFEToHaveBinariesInstalled"
            Checksum        = "modifiedDate"
        }

        #Preparations needed for starting the patching phase - e.g. removing server from the loadbalancer through a script.
        #This will only execute if there is no log file or the log file doesnt contain the expected content.
        Script PrepareServerStage
        {
            SetScript            = { #Preparation logic - create your own logic needed for preparations
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[File]ScriptSources"
        }

        #Installation of the first patch
        SPProductUpdate InstallPatch1
        {
            SetupFile            = $Patch1Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]PrepareServerStage"
        }

        #Creation log and entry if the first patch has been installed
        Script CreateLogPatch1
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value $using:stsPatchFileName
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value $using:stsPatchFileName
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch1"
        }

        #Installation of the second patch
        SPProductUpdate InstallPatch2
        {
            SetupFile            = $Patch2Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]CreateLogPatch1"
        }

        #Run Get-SPProduct
        Script SPProduct
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock { Get-SPProduct -Local }
            }
            TestScript           = { $false }      # YK - Zelfde opmerking als eerder gegeven.
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch2"
        }

        #Creation log and entry if the second patch has been installed
        Script CreateLogPatch2
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value $using:wsslocPatchFileName
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value $using:wsslocPatchFileName
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains $using:stsPatchFileName -and $content -contains $using:wsslocPatchFileName)
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]SPProduct"
        }

        #Wait for other servers to have the patches installed
        WaitForAll ServersToHaveBinariesInstalled
        {
            ResourceName     = "[SPProductUpdate]InstallPatch2"
            NodeName         = @($firstAPP, $secondAPP)
            RetryIntervalSec = 300
            RetryCount       = 120
            DependsOn        = "[Script]CreateLogPatch2"
        }

        #Creation log and entry if psconfig has been run. Perform a gracefull shutdown of the distributed cache.
        Script PSConfigCheck
        {

            SetScript            = {
                $scriptpath = $using:PreparationSourceDestination
                Invoke-SPDscCommand -ScriptBlock {
                    & (Join-Path -Path $args[0] -ChildPath "DistributedCacheGracefulShutdown.ps1" )
                } -Arguments $using:PreparationSourceDestination
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains "$(Get-Date -format "yyyyMMdd") psconfig")
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[WaitForAll]ServersToHaveBinariesInstalled"
        }

        #Make sure these services are running else psconfig might fail
        ServiceSet Services
        {
            Name                 = @("W3SVC", "NetPipeActivator", "NetTcpActivator", "SPTimerV4" , "SPTraceV4")
            StartupType          = "Automatic"
            State                = "Running"
            DependsOn            = "[Script]PSConfigCheck"
            PsDscRunAsCredential = $CredsSPFarm
        }

        #Run psconfig
        SPConfigWizard PSConfig
        {
            IsSingleInstance     = "Yes"
            Ensure               = "Present"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[ServiceSet]Services"
        }

        #Run PSConfig to make sure everything is upgraded
        Script PSConfig
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock {
                    PSConfig.exe -cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install
                }
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    $statusType = Get-SPDscServerPatchStatus
                    if ($statusType -eq "UpgradeRequired" -or $statusType -eq "UpgradeAvailable" -or $statusType -eq "UpgradeInProgress" )
                    {
                        $false
                    }
                    else
                    {
                        $true
                    }
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPConfigWizard]PSConfig"
        }

        #Creation log and entry if psconfig has been run
        Script CreateLogPSConfig
        {
            SetScript            = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    Add-Content -Path $using:PatchLogFile -Value "$(Get-Date -format "yyyyMMdd") psconfig"
                }
                else
                {
                    New-Item -Path $using:PatchLogFile -Type "file" -Force
                    Add-Content -Path $using:PatchLogFile -Value "$(Get-Date -format "yyyyMMdd") psconfig"
                }
            }
            TestScript           = {
                if (Test-Path -PathType leaf -Path $using:PatchLogFile)
                {
                    $content = Get-Content $using:PatchLogFile

                    if ($content -contains "$(Get-Date -format "yyyyMMdd") psconfig")
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                    return $false
                }
                else
                {
                    return $false
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]PSConfig"
        }

        #Finalize steps needed during patch installation phase - e.g. addding the server back to the loadbalancer. Before that add the server back to the distributed cache.
        Script FinalizeServerStage
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock {
                    & (Join-Path -Path $args[0] -ChildPath "DistributedCacheStart.ps1" )
                } -Arguments $using:PreparationSourceDestination
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    if ((Get-SPServiceInstance | ? { ($_.service.ToString()) -eq "SPDistributedCacheService Name=AppFabricCachingService" -and ($_.server.name) -eq $env:computername } | ? { $_.status -eq "Online" }) )
                    {
                        return $true
                    }
                    else
                    {
                        return $false
                    }
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[Script]CreateLogPSConfig"
        }
    }

    Node @($firstAPP)
    {
        #Wait for other servers to have the patches installed
        WaitForAll WFEToHaveBinariesInstalled
        {
            ResourceName     = "[SPProductUpdate]InstallPatch2"
            NodeName         = @($firstWFE, $secondWFE)
            RetryIntervalSec = 300
            RetryCount       = 120
        }

        #Installation of the first patch
        SPProductUpdate InstallPatch1
        {
            SetupFile            = $Patch1Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[WaitForAll]WFEToHaveBinariesInstalled"
        }

        #Installation of the second patch
        SPProductUpdate InstallPatch2
        {
            SetupFile            = $Patch2Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch1"
        }

        #Run Get-SPProduct
        Script SPProduct
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock { Get-SPProduct -Local }
            }
            TestScript           = { $false }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch2"
        }

        #Wait for other servers to complete PSConfig
        WaitForAll PSConfigToBeFinishedOnServers
        {
            ResourceName     = "[SPConfigWizard]PSConfig"
            NodeName         = @($firstWFE, $secondWFE)
            RetryIntervalSec = 300
            RetryCount       = 120
            DependsOn        = "[Script]SPProduct"
        }

        #Make sure these services are running else psconfig might fail
        ServiceSet Services
        {
            Name                 = @("W3SVC", "NetPipeActivator", "NetTcpActivator", "SPTimerV4" , "SPTraceV4")
            StartupType          = "Automatic"
            State                = "Running"
            DependsOn            = "[WaitForAll]PSConfigToBeFinishedOnServers"
            PsDscRunAsCredential = $CredsSPFarm
        }

        #Run psconfig
        SPConfigWizard PSConfig
        {
            IsSingleInstance     = "Yes"
            Ensure               = "Present"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[ServiceSet]Services"
        }

        #Run PSConfig to make sure everything is upgraded
        Script PSConfig
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock {
                    PSConfig.exe -cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install
                }
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    $statusType = Get-SPDscServerPatchStatus
                    if ($statusType -eq "UpgradeRequired" -or $statusType -eq "UpgradeAvailable" -or $statusType -eq "UpgradeInProgress" )
                    {
                        $false
                    }
                    else
                    {
                        $true
                    }
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPConfigWizard]PSConfig"
        }
    }

    Node @($secondAPP)
    {
        #Wait for other servers to have the patches installed
        WaitForAll WFEToHaveBinariesInstalled
        {
            ResourceName     = "[SPProductUpdate]InstallPatch2"
            NodeName         = @($firstWFE, $secondWFE, $firstAPP)
            RetryIntervalSec = 300
            RetryCount       = 120
        }

        #Installation of the first patch
        SPProductUpdate InstallPatch1
        {
            SetupFile            = $Patch1Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[WaitForAll]WFEToHaveBinariesInstalled"
        }

        #Installation of the second patch
        SPProductUpdate InstallPatch2
        {
            SetupFile            = $Patch2Location
            ShutdownServices     = $true
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch1"
        }

        #Run Get-SPProduct
        Script SPProduct
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock { Get-SPProduct -Local }
            }
            TestScript           = { $false }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPProductUpdate]InstallPatch2"
        }

        #Wait for other servers to complete PSConfig
        WaitForAll PSConfigToBeFinishedOnServers
        {
            ResourceName     = "[SPConfigWizard]PSConfig"
            NodeName         = @($firstWFE, $secondWFE, $firstAPP)
            RetryIntervalSec = 300
            RetryCount       = 120
            DependsOn        = "[Script]SPProduct"
        }

        #Make sure these services are running else psconfig might fail
        ServiceSet Services
        {
            Name                 = @("W3SVC", "NetPipeActivator", "NetTcpActivator", "SPTimerV4" , "SPTraceV4")
            StartupType          = "Automatic"
            State                = "Running"
            DependsOn            = "[WaitForAll]PSConfigToBeFinishedOnServers"
            PsDscRunAsCredential = $CredsSPFarm
        }

        #Run psconfig
        SPConfigWizard PSConfig
        {
            IsSingleInstance     = "Yes"
            Ensure               = "Present"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[WaitForAll]PSConfigToBeFinishedOnServers"
        }

        #Run PSConfig to make sure everything is upgraded
        Script PSConfig
        {
            SetScript            = {
                Invoke-SPDscCommand -ScriptBlock {
                    PSConfig.exe -cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install
                }
            }
            TestScript           = {
                Invoke-SPDscCommand -ScriptBlock {
                    $statusType = Get-SPDscServerPatchStatus
                    if ($statusType -eq "UpgradeRequired" -or $statusType -eq "UpgradeAvailable" -or $statusType -eq "UpgradeInProgress" )
                    {
                        $false
                    }
                    else
                    {
                        $true
                    }
                }
            }
            GetScript            = { #Do Nothing
            }
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn            = "[SPConfigWizard]PSConfig"
        }

    }
}

$ConfigData = @{
    AllNodes    = @(
        @{
            NodeName                    = "SPWFE01"
            Role                        = "WFEDC"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true;
        },
        @{
            NodeName                    = "SPWFE02"
            Role                        = "WFEDC"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true;
        },
        @{
            NodeName                    = "SPAPP01"
            Role                        = "APP"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true;
        },
        @{
            NodeName                    = "SPAPP02"
            Role                        = "APP"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true;
        }
    )
    NonNodeData = @{
        setupAccount                 = "contoso\sp_adm"
        PatchLocation                = "\\DSCSERVER01\SPSources\2019\SharePoint\CU\"
        PatchLog                     = "D:\temp\DSCPatching.log"

        PreparationSourceLocation    = "\\DSCSERVER01\SPSources\2019\SharePoint\Scripts"
        PreparationSourceDestination = "D:\Scripts\SharePoint\DSCPatching"

        stsPatchFileName             = "sts2019-kb4484142-fullfile-x64-glb.exe"
        wsslocPatchFileName          = "wssloc2019-kb4484149-fullfile-x64-glb.exe"

    }
}

SPFarmUpdate -ConfigurationData $ConfigData
ykuijs commented 3 years ago

Warning: Long update 😉

I have thought about this scenario a little more. We have several challenges:

  1. How to determine the order in which servers are going to be patched and upgraded (config wizard)?
  2. How to take servers out of load balancing (only applicable for WFEs)?
  3. How to handle reboots?
  4. How to handle farm configurations like the SideBySideToken parameter?
  5. How to orchestrate the whole process?

First, to let DSC know that it has to use ZDP, the SPProductUpdate and SPConfigWizard resource should get new a new parameter called UseZDP (boolean), with a default of $false. When this is set to $true, the ZDP logic is activated. Else it will just run the resources as they do now.

1. Determine server order

According to this article, for ZDP you first have to run the install for each of the WFE servers, than half of all other servers and then the other half of all other servers. That is why we need a way to specify an order.

One option is to include a PatchSet parameter in which you can specify to which "half" of the farm the server belongs. That way you can specify which servers have to be patched first and which second. This option can be used if the only the Custom minrole is used or for example patching all servers in one datacenter first.

Another option is (when the farm uses full MinRole) to just loop through all different MinRoles and take the first half of the servers in that role and later take the second half. First do all servers that are the WebFrontEnd minrole one by one, than do all servers that are WebFrontEnd with Distributed Cache one by one, than half of the Application servers, half of the Application with Search, half of the Distributed Cache Servers, half of the Search servers and last half of the Custom servers. To finalize do all other servers of each MinRole, following the same order. This option divides the servers based on their name, which can be a problem in some cases. Use option 1 in that case.

So if you have a server that is called SPSearch2 which is running the Search minrole, that server will wait until the second half of the Search MinRole are going to be patched. See section "Orchestrating the patch process" below on how to determine the order. This will solve issue 1.

Take servers out load balancing

Since each environment can use a different load balancing solution (all with their own APIs, etc), the easiest way to take a server out of load balancing is to block traffic to the server using the Windows Firewall. The resource will simply create a firewall rule with a clear name, for example: "SPDSC - Block incoming traffic for patching". Since sites can use non-default ports, we have to add a parameter to the resource called BlockedPorts in which admins can provide all ports to be included in the firewall rule. This will solve issue 2.

Handle reboots

If we update the SPProductUpdate resource to allow multiple parameters, we can have a single resource install multiple updates and only reboot once (if required). At the beginning of the resource we can configure generic settings (create firewall rule, set SideBySideToken vlaues, etc). At the end of the resource (depending on the server) we can remove the generic settings if required (firewall rule for WFESet 2 should remain until Config Wizard completes). This will solve issue 3 and part of 4.

Configure generic configurations

By creating new resources to configure generic configurations, like the creation of a firewall rule, you can run into a conflict between two resources where one enables/creates the rule and the other disables/removes that same rule. This means one of the two resources will never be in the desired state. This can be resolved by updating the SPProductUpdate and SPConfigWizard The first SPProductUpdate resource that executes can set the SideBySideToken value and the last SPConfigWizard can update that value to the new value. There should only be a way to determine when this is the case, see next item. This will solve issue 4.

Orchestrating the patch process

To orchestrate the whole process I was thinking about using PropertyBags: The SPFarm (one per farm) and SPServer (one per server) objects have the possibility to store data in PropertyBags. The SPProductUpdate and SPConfigWizard resources can use this information to store and determine what component has which state. I was thinking about:

SPServer

  1. SPDsc_PatchInstall: Stores the current state of the server, like Running, Rebooting, Completed.
  2. SPDsc_PatchVersion: Stores the version of the patch that is going to be installed.
  3. SPDsc_Timestamp: Storing the timestamp for the last update. For diagnostic purposes only.

SPFarm

  1. SPDsc_SideBySide: Stores the version of the SideBySide parameter.
  2. SPDsc_Timestamp: Storing the timestamp for the last update. For diagnostic purposes only.

Requirements

Process

The SPProductUpdate checks the MinRole of the server. If the MinRole is:

What do you guys think about this idea?

ThomasLie commented 3 years ago

Hi Yorick,

I like the approach, but I do have a couple of considerations…

Determine server order I’d prefer a PatchSet parameter paired with MinRole filtering on WebfrontEnd* (because we have to path the WFEs first).

Controlling load balancing with Windows Firewall It could be an issue if Firewall is controlled by group policies… What about simply stopping / disabling IIS service or stopping the websites in IIS while installing CUs and running PSCONFIG? This should be detected by all load balancing solutions.

Handle reboots We have to make sure that we gracefully remove the Distributed Cache before running PSCONFIG and add the Distributed Cache to the server again after completion (in case of MinRole DistributedCache or WebFrontendWithDistributedCache).

Mit freundlichen Grüßen / Kind regards

Thomas Lieftüchter

Lieftüchter IT Consulting & Hausverwaltung GbR Römerweg 10a 63303 Dreieich - Germany

Fon: +49 171.2625166 Email: @.**@.> Web: https://www.lieftuechter.com/TL

Diese E-Mail enthält vertrauliche und/oder rechtlich geschützte Informationen. Wenn Sie nicht der richtige Adressat sind oder diese E-Mail irrtümlich erhalten haben, informieren Sie bitte sofort den Absender und vernichten Sie diese Mail. Das unerlaubte Kopieren sowie die unbefugte Weitergabe dieser Mail ist nicht gestattet. This e-mail may contain confidential and/or privileged information. If you are not the intended recipient (or have received this e-mail in error) please notify the sender immediately and destroy this e-mail. Any unauthorized copying, disclosure or distribution of the material in this e-mail is strictly forbidden.

Von: Yorick Kuijs @.> Gesendet: Dienstag, 5. Oktober 2021 22:32 An: dsccommunity/SharePointDsc @.> Cc: Thomas Lieftüchter @.>; Comment @.> Betreff: Re: [dsccommunity/SharePointDsc] SPProductUpdate: Add possibility to use Zero-Downtime Patching with SPDsc (#1096)

Warning: Long update 😉

I have thought about this scenario a little more. We have several challenges:

  1. How to determine the order in which servers are going to be patched and upgraded (config wizard)?
  2. How to take servers out of load balancing (only applicable for WFEs)?
  3. How to handle reboots?
  4. How to handle farm configurations like the SideBySideToken parameter?
  5. How to orchestrate the whole process?

First, to let DSC know that it has to use ZDP, the SPProductUpdate and SPConfigWizard resource should get new a new parameter called UseZDP (boolean), with a default of $false. When this is set to $true, the ZDP logic is activated. Else it will just run the resources as they do now.

  1. Determine server order

According to thishttps://docs.microsoft.com/en-us/sharepoint/upgrade-and-update/sharepoint-server-2016-zero-downtime-patching-steps#phase-1---patch-install article, for ZDP you first have to run the install for each of the WFE servers, than half of all other servers and then the other half of all other servers. That is why we need a way to specify an order.

One option is to include a PatchSet parameter in which you can specify to which "half" of the farm the server belongs. That way you can specify which servers have to be patched first and which second. This option can be used if the only the Custom minrole is used or for example patching all servers in one datacenter first.

Another option is (when the farm uses full MinRole) to just loop through all different MinRoles and take the first half of the servers in that role and later take the second half. First do all servers that are the WebFrontEnd minrole one by one, than do all servers that are WebFrontEnd with Distributed Cache one by one, than half of the Application servers, half of the Application with Search, half of the Distributed Cache Servers, half of the Search servers and last half of the Custom servers. To finalize do all other servers of each MinRole, following the same order. This option divides the servers based on their name, which can be a problem in some cases. Use option 1 in that case.

So if you have a server that is called SPSearch2 which is running the Search minrole, that server will wait until the second half of the Search MinRole are going to be patched. See section "Orchestrating the patch process" below on how to determine the order. This will solve issue 1.

Take servers out load balancing

Since each environment can use a different load balancing solution (all with their own APIs, etc), the easiest way to take a server out of load balancing is to block traffic to the server using the Windows Firewall. The resource will simply create a firewall rule with a clear name, for example: "SPDSC - Block incoming traffic for patching". Since sites can use non-default ports, we have to add a parameter to the resource called BlockedPorts in which admins can provide all ports to be included in the firewall rule. This will solve issue 2.

Handle reboots

If we update the SPProductUpdate resource to allow multiple parameters, we can have a single resource install multiple updates and only reboot once (if required). At the beginning of the resource we can configure generic settings (create firewall rule, set SideBySideToken vlaues, etc). At the end of the resource (depending on the server) we can remove the generic settings if required (firewall rule for WFESet 2 should remain until Config Wizard completes). This will solve issue 3 and part of 4.

Configure generic configurations

By creating new resources to configure generic configurations, like the creation of a firewall rule, you can run into a conflict between two resources where one enables/creates the rule and the other disables/removes that same rule. This means one of the two resources will never be in the desired state. This can be resolved by updating the SPProductUpdate and SPConfigWizard The first SPProductUpdate resource that executes can set the SideBySideToken value and the last SPConfigWizard can update that value to the new value. There should only be a way to determine when this is the case, see next item. This will solve issue 4.

Orchestrating the patch process

To orchestrate the whole process I was thinking about using PropertyBags: The SPFarm (one per farm) and SPServer (one per server) objects have the possibility to store data in PropertyBags. The SPProductUpdate and SPConfigWizard resources can use this information to store and determine what component has which state. I was thinking about:

SPServer

  1. SPDsc_PatchInstall: Stores the current state of the server, like Running, Rebooting, Completed.
  2. SPDsc_PatchVersion: Stores the version of the patch that is going to be installed.
  3. SPDsc_Timestamp: Storing the timestamp for the last update. For diagnostic purposes only.

SPFarm

  1. SPDsc_SideBySide: Stores the version of the SideBySide parameter.
  2. SPDsc_Timestamp: Storing the timestamp for the last update. For diagnostic purposes only.

Requirements

Process

The SPProductUpdate checks the MinRole of the server. If the MinRole is:

What do you guys think about this idea?

— You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://github.com/dsccommunity/SharePointDsc/issues/1096#issuecomment-934786089, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ALR4I4Y6EHSCVSIZ3HO45YTUFNOEHANCNFSM4IE6N33A. Triage notifications on the go with GitHub Mobile for iOShttps://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Androidhttps://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.