Open ykuijs opened 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.
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.
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?
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.
Following color codes for the drawing:
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 ..
Hi @jimbrayDel Not entirely sure what you mean here. Can you elaborate a little more?
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.
@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.
@ykuijs @andikrueger Trying to make this as simple as possible.
Steps:
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.
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.
@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 {
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?
Did you have a look at this idea: #1097?
@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?
@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?
@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:
Am i still missing something?
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:
@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.
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.
@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 = "r
n"
$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."}
}
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 = "r
n"
$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."}
}
$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
@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.
@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.
@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?
@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.
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.
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.
@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.
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
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.
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
Warning: Long update 😉
I have thought about this scenario a little more. We have several challenges:
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.
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.
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.
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.
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.
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
SPFarm
The SPProductUpdate checks the MinRole of the server. If the MinRole is:
What do you guys think about this idea?
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:
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.
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
SPFarm
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.
@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:
We are curious what other think of this solution? If there are any other ideas that might be better as the ones suggested above.