diff --git a/CHANGELOG.md b/CHANGELOG.md index 8375cc6..82eaf8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - 2023-11-12 +## [1.0.0] - 2023-11-20 ### Changed @@ -29,3 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add CONTRIBUTING.md file - Add release.yml file - Add scripts folder with first version of SPSUpdate +- Wiki Documentation in repository - Add : + - wiki/Configuration.md + - wiki/Getting-Started.md + - wiki/Home.md + - wiki/Usage.md + - .github/workflows/wiki.yml diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5469271..46e7d5b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,6 +1,6 @@ # SPSUpdate - Release Notes -## [Unreleased] - 2024-11-12 +## [1.0.0] - 2024-11-20 ### Changed @@ -26,5 +26,11 @@ - Add CONTRIBUTING.md file - Add release.yml file - Add scripts folder with first version of SPSUpdate +- Wiki Documentation in repository - Add : + - wiki/Configuration.md + - wiki/Getting-Started.md + - wiki/Home.md + - wiki/Usage.md + - .github/workflows/wiki.yml A full list of changes in each version can be found in the [change log](CHANGELOG.md) diff --git a/scripts/Config/CONTOSO-PROD-CONTENT.json b/scripts/Config/CONTOSO-PROD-CONTENT.json new file mode 100644 index 0000000..7e7b56f --- /dev/null +++ b/scripts/Config/CONTOSO-PROD-CONTENT.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/schema#", + "contentVersion": "1.0.0.0", + "ConfigurationName": "PROD", + "ApplicationName": "contoso", + "FarmName": "CONTENT", + "Domain": "contoso.com", + "StoredCredential": "PROD-ADM", + "Binaries": { + "SetupFullPath": "\\\\srvfileshared.contoso.com\\cumulativeupdates", + "SetupFileName": ["uber-subscription-kb5002651-fullfile-x64-glb.exe"], + "ShutdownServices": true + }, + "UpgradeContentDatabase": true, + "SideBySideToken": { + "Enable": true, + "BuildVersion": "16.0.17928.20238" + } +} diff --git a/scripts/Config/CONTOSO-PROD-SEARCH.json b/scripts/Config/CONTOSO-PROD-SEARCH.json new file mode 100644 index 0000000..1bc2943 --- /dev/null +++ b/scripts/Config/CONTOSO-PROD-SEARCH.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/schema#", + "contentVersion": "1.0.0.0", + "ConfigurationName": "PROD", + "ApplicationName": "contoso", + "FarmName": "SEARCH", + "Domain": "contoso.com", + "StoredCredential": "PROD-ADM", + "Binaries": { + "SetupFullPath": "\\\\srvfileshared.contoso.com\\cumulativeupdates", + "SetupFileName": [ + "sts2019-kb5002630-fullfile-x64-glb.exe", + "wssloc2019-kb5002597-fullfile-x64-glb.exe" + ], + "ShutdownServices": false + }, + "UpgradeContentDatabase": false, + "SideBySideToken": { + "Enable": false, + "BuildVersion": "" + } +} diff --git a/scripts/Config/CONTOSO-PROD-SERVICES.json b/scripts/Config/CONTOSO-PROD-SERVICES.json new file mode 100644 index 0000000..9582c39 --- /dev/null +++ b/scripts/Config/CONTOSO-PROD-SERVICES.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/schema#", + "contentVersion": "1.0.0.0", + "ConfigurationName": "PROD", + "ApplicationName": "contoso", + "FarmName": "SERVICES", + "Domain": "contoso.com", + "StoredCredential": "PROD-ADM", + "Binaries": { + "SetupFullPath": "\\\\srvfileshared.contoso.com\\cumulativeupdates", + "SetupFileName": [ + "sts-subscription-kb5002191-fullfile-x64-glb.exe", + "wssloc-subscription-kb5002110-fullfile-x64-glb.exe" + ], + "ShutdownServices": true + }, + "UpgradeContentDatabase": false, + "SideBySideToken": { + "Enable": false, + "BuildVersion": "" + } +} diff --git a/scripts/Modules/credentialmanager/CredentialManager.dll b/scripts/Modules/credentialmanager/CredentialManager.dll new file mode 100644 index 0000000..e14dba9 Binary files /dev/null and b/scripts/Modules/credentialmanager/CredentialManager.dll differ diff --git a/scripts/Modules/credentialmanager/CredentialManager.dll-Help.xml b/scripts/Modules/credentialmanager/CredentialManager.dll-Help.xml new file mode 100644 index 0000000..1de13ba --- /dev/null +++ b/scripts/Modules/credentialmanager/CredentialManager.dll-Help.xml @@ -0,0 +1,782 @@ + + + + + + Get-StoredCredential + Get + StoredCredential + + Gets stored credentials from the Windows Credential Store/Vault + + + + Gets stored credentials from the Windows Credential Store/Vault and returns as either a PSCredential object or as a Credential Object + + + + + Get-StoredCredential + + + AsCredentialObject + + Switch to return the credentials as Credential objects instead of the default PSObject + + SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + Target + + The command will only return credentials with the specified target + + string + + System.String + + + + + + Type + + Specifies the type of credential to return, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + + + + Target + + The command will only return credentials with the specified target + + string + + System.String + + + + + + Type + + Specifies the type of credential to return, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + AsCredentialObject + + Switch to return the credentials as Credential objects instead of the default PSObject + + SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + + + System.String + + + + The command will only return credentials with the specified target + + + + + + + + PSCredentialManager.Common.Credential + + + + + + + System.Management.Automation.PSCredential + + + + + + + ---------- EXAMPLE 1 ---------- + PS C:\> Get-StoredCredential -Target Server01 + + Returns credentials for Server01 as a PSCredential object + UserName Password + -------- -------- + test-user System.Security.SecureString + + + + ---------- EXAMPLE 2 ---------- + PS C:\> Get-StoredCredential -Target Server01 -AsCredentialObject + + Returns credentials for Server01 as a Credential object + Flags : 0 + Type : GENERIC + TargetName : server01 + Comment : + LastWritten : 23/04/2016 10:01:37 + PaswordSize : 18 + Password : Password1 + Persist : ENTERPRISE + AttributeCount : 0 + Attributes : 0 + TargetAlias : + UserName : test-user + + + + + + Online Version + https://github.com/davotronic5000/PowerShell_Credential_Manager/wiki/Get-StoredCredential + + + + + + + New-StoredCredential + New + StoredCredential + + Create a new credential in the Windows Credential Store/Vault + + + + Create a new credential in the Windows Credential Store/Vault + + + + + New-StoredCredential + + + Comment + + Provides a comment to identify the credentials in the store + + + string + + System.String + + + Updated by: Dave on: 10/06/2016 + + + + Password + + Specifies the password in plain text, cannot be used in conjunction with SecurePassword or Credential parameters. + + string + + System.String + + + q7:7T6zBE% + + + + Persist + + sets the persistence settings of the credential, possible values are [SESSION, LOCAL_MACHINE, ENTERPRISE] + + CredPersist + + PSCredentialManager.Common.Enum.CredPersist + + + Session + + + + Target + + Specifies the target of the credentials being added. + + string + + System.String + + + DESKTOP-6O28IQJ + + + + Type + + Type of credential to store, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + UserName + + specified the username to be used for the credentials, cannot be used in conjunction with Credentials parameter. + + string + + System.String + + + Dave + + + + + New-StoredCredential + + + Comment + + Provides a comment to identify the credentials in the store + + + string + + System.String + + + Updated by: Dave on: 10/06/2016 + + + + Persist + + sets the persistence settings of the credential, possible values are [SESSION, LOCAL_MACHINE, ENTERPRISE] + + CredPersist + + PSCredentialManager.Common.Enum.CredPersist + + + Session + + + + SecurePassword + + Specifies the password as a secure string, cannot be used in conjunction with SecurePassword or Credential parameters. + + SecureString + + System.Security.SecureString + + + + + + Target + + Specifies the target of the credentials being added. + + string + + System.String + + + DESKTOP-6O28IQJ + + + + Type + + Type of credential to store, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + UserName + + specified the username to be used for the credentials, cannot be used in conjunction with Credentials parameter. + + string + + System.String + + + Dave + + + + + New-StoredCredential + + + Comment + + Provides a comment to identify the credentials in the store + + + string + + System.String + + + Updated by: Dave on: 10/06/2016 + + + + Credentials + + + + PSCredential + + System.Management.Automation.PSCredential + + + + + + Persist + + sets the persistence settings of the credential, possible values are [SESSION, LOCAL_MACHINE, ENTERPRISE] + + CredPersist + + PSCredentialManager.Common.Enum.CredPersist + + + Session + + + + Target + + Specifies the target of the credentials being added. + + string + + System.String + + + DESKTOP-6O28IQJ + + + + Type + + Type of credential to store, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + + + + Target + + Specifies the target of the credentials being added. + + string + + System.String + + + DESKTOP-6O28IQJ + + + + UserName + + specified the username to be used for the credentials, cannot be used in conjunction with Credentials parameter. + + string + + System.String + + + Dave + + + + Password + + Specifies the password in plain text, cannot be used in conjunction with SecurePassword or Credential parameters. + + string + + System.String + + + 2hxmOG=wM: + + + + SecurePassword + + Specifies the password as a secure string, cannot be used in conjunction with SecurePassword or Credential parameters. + + SecureString + + System.Security.SecureString + + + + + + Comment + + Provides a comment to identify the credentials in the store + + + string + + System.String + + + Updated by: Dave on: 10/06/2016 + + + + Type + + Type of credential to store, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + Persist + + sets the persistence settings of the credential, possible values are [SESSION, LOCAL_MACHINE, ENTERPRISE] + + CredPersist + + PSCredentialManager.Common.Enum.CredPersist + + + Session + + + + Credentials + + + + PSCredential + + System.Management.Automation.PSCredential + + + + + + + + System.Management.Automation.PSCredential + + + + + + + + + + + + PSCredentialManager.Common.Credential + + + + + + + ---------- EXAMPLE 1 ---------- + PS C:\> New-StoredCredential -Target server01 -UserName test-user -Password Password1 + + creates a credential for server01 with the username test-user and password Password1 + Flags : 0 + Type : GENERIC + TargetName : server01 + Comment : Updated by: Dave on: 23/04/2016 + LastWritten : 23/04/2016 10:48:56 + PaswordSize : 18 + Password : Password1 + Persist : SESSION + AttributeCount : 0 + Attributes : 0 + TargetAlias : + UserName : test-user + + + + ---------- EXAMPLE 2 ---------- + PS C:\> Get-Credential -UserName test-user -Message "Password please" | New-StoredCredential -Target Server01 + + Creates a credential for Server01 with the username and password provided in the PSCredential object from Get-Credential + + + + + + Online Version + https://github.com/davotronic5000/PowerShell_Credential_Manager/wiki/New-StoredCredential + + + + + + + Remove-StoredCredential + Remove + StoredCredential + + Deletes a credentials from the Windows Credential Store/Vault + + + + Deletes a credentials from the Windows Credential Store/Vault + + + + + Remove-StoredCredential + + + Target + + specifies a target to identitfy the credential to be deleted + + string + + System.String + + + + + + Type + + Specifies the type of credential to be deleted, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + + + + Target + + specifies a target to identitfy the credential to be deleted + + string + + System.String + + + + + + Type + + Specifies the type of credential to be deleted, possible values are [GENERIC, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, GENERIC_CERTIFICATE, DOMAIN_EXTENDED, MAXIMUM, MAXIMUM_EX] + + CredType + + PSCredentialManager.Common.Enum.CredType + + + Generic + + + + + + System.String + + + + specifies a target to identitfy the credential to be deleted + + + + + + + ---------- EXAMPLE 1 ---------- + PS C:\> Remove-StoredCredential -Target Server01 -Type GENERIC + + Deletes a generic credential with the target Server01 + + + + + + Online Version + https://github.com/davotronic5000/PowerShell_Credential_Manager/wiki/Remove-StoredCredential + + + + + + + Get-StrongPassword + Get + StrongPassword + + Generates a strong password + + + + Generates a strong password based on the parameters provided + + + + + Get-StrongPassword + + + Length + + Length in Characters for the generated password to be. + + int + + System.Int32 + + + 10 + + + + NumberOfSpecialCharacters + + Number of special characters to include in the password, must be less than the length of the password + + int + + System.Int32 + + + 3 + + + + + + + Length + + Length in Characters for the generated password to be. + + int + + System.Int32 + + + 10 + + + + NumberOfSpecialCharacters + + Number of special characters to include in the password, must be less than the length of the password + + int + + System.Int32 + + + 3 + + + + + + + + System.String + + + + + + + + ---------- EXAMPLE 1 ---------- + PS C:\> Get-StrongPassword + + Generates a password 10 characters long with 3 special characters + QTJ(T?wwe) + + + + ---------- EXAMPLE 2 ---------- + PS C:\> Get-StrongPassword -Length 20 -NumberOfSpecialCharacters 5 + + Generates a password 20 characters long with 5 special characters + zPN>C%.f/(l1aGq)n3Ze + + + + + + Online Version + https://github.com/davotronic5000/PowerShell_Credential_Manager/wiki/Get-StrongPassword + + + + \ No newline at end of file diff --git a/scripts/Modules/credentialmanager/CredentialManager.psd1 b/scripts/Modules/credentialmanager/CredentialManager.psd1 new file mode 100644 index 0000000..bf22aec Binary files /dev/null and b/scripts/Modules/credentialmanager/CredentialManager.psd1 differ diff --git a/scripts/Modules/credentialmanager/PSCredentialManager.Api.dll b/scripts/Modules/credentialmanager/PSCredentialManager.Api.dll new file mode 100644 index 0000000..3c05db5 Binary files /dev/null and b/scripts/Modules/credentialmanager/PSCredentialManager.Api.dll differ diff --git a/scripts/Modules/credentialmanager/PSCredentialManager.Common.dll b/scripts/Modules/credentialmanager/PSCredentialManager.Common.dll new file mode 100644 index 0000000..f0f8fd2 Binary files /dev/null and b/scripts/Modules/credentialmanager/PSCredentialManager.Common.dll differ diff --git a/scripts/Modules/sps.util.psm1 b/scripts/Modules/sps.util.psm1 new file mode 100644 index 0000000..f1391bb --- /dev/null +++ b/scripts/Modules/sps.util.psm1 @@ -0,0 +1,428 @@ +function Get-SPSServersPatchStatus { + [CmdletBinding()] + param() + + $farm = Get-SPFarm + $productVersions = [Microsoft.SharePoint.Administration.SPProductVersions]::GetProductVersions($farm) + $servers = Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' } + + [array]$srvStatus = @() + foreach ($server in $servers) { + $serverProductInfo = $productVersions.GetServerProductInfo($server.Id) + if ($null -ne $serverProductInfo) { + $statusType = $serverProductInfo.InstallStatus + if ($statusType -ne 0) { + $statusType = $serverProductInfo.GetUpgradeStatus($farm, $server) + } + } + else { + $statusType = [Microsoft.SharePoint.Administration.SPServerProductInfo+StatusType]::NoActionRequired + } + + $srvStatus += [PSCustomObject]@{ + Name = $server.Name + Status = $statusType + } + } + return $srvStatus +} + +function Start-SPSConfigExe { + [CmdletBinding()] + param () + + # Check if all servers are on the same patch level before running psconfig.exe + $unpatchedServers = Get-SPSServersPatchStatus | Where-Object { $_.Status -ne "UpgradeRequired" -and $_.Status -ne "UpgradeAvailable" } + if ($unpatchedServers.Count -eq 0) { + Write-Verbose -Message "All servers are on the same patch level. Running PSConfig ..." + # Check which version of SharePoint is installed + $pathToSearch = 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions\*\ISAPI\Microsoft.SharePoint.dll' + $fullPath = Get-Item $pathToSearch -ErrorAction SilentlyContinue | Sort-Object { $_.Directory } -Descending | Select-Object -First 1 + $getSPInstalledProductVersion = (Get-Command $fullPath).FileVersionInfo + + if ($getSPInstalledProductVersion.FileMajorPart -eq 15) { + $wssRegKey = 'hklm:SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\15.0\WSS' + $binaryDir = Join-Path $env:CommonProgramFiles "Microsoft Shared\Web Server Extensions\15\BIN" + } + else { + $wssRegKey = 'hklm:SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\16.0\WSS' + $binaryDir = Join-Path $env:CommonProgramFiles "Microsoft Shared\Web Server Extensions\16\BIN" + } + $psconfigExe = Join-Path -Path $binaryDir -ChildPath "psconfig.exe" + + # Read LanguagePackInstalled and SetupType registry keys + $languagePackInstalled = Get-ItemProperty -LiteralPath $wssRegKey -Name 'LanguagePackInstalled' + $setupType = Get-ItemProperty -LiteralPath $wssRegKey -Name 'SetupType' + + # Determine if LanguagePackInstalled=1 or SetupType=B2B_Upgrade. + # If so, the Config Wizard is required + if (($languagePackInstalled.LanguagePackInstalled -eq 1) -or ($setupType.SetupType -eq "B2B_UPGRADE")) { + Write-Verbose -Message "Starting Configuration Wizard" + Write-Verbose -Message "Starting 'Product Version Job' timer job" + $pvTimerJob = Get-SPTimerJob -Identity 'job-admin-product-version' + $lastRunTime = $pvTimerJob.LastRunTime + + Start-SPTimerJob -Identity $pvTimerJob + + $jobRunning = $true + $maxCount = 30 + $count = 0 + Write-Verbose -Message "Waiting for 'Product Version Job' timer job to complete" + while ($jobRunning -and $count -le $maxCount) { + Start-Sleep -Seconds 10 + + $pvTimerJob = Get-SPTimerJob -Identity 'job-admin-product-version' + $jobRunning = $lastRunTime -eq $pvTimerJob.LastRunTime + + $count++ + } + + # Fix for issue with psconfig on SharePoint 2019 + if ($getSPInstalledProductVersion.FileMajorPart -eq 16) { + Upgrade-SPFarm -ServerOnly -SkipDatabaseUpgrade -SkipSiteUpgrade -Confirm:$false + } + + $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" + $psconfig = Start-Process -FilePath $psconfigExe ` + -ArgumentList "-cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install" ` + -RedirectStandardOutput $stdOutTempFile ` + -Wait ` + -PassThru + + $cmdOutput = Get-Content -Path $stdOutTempFile -Raw + Remove-Item -Path $stdOutTempFile + + if ($null -ne $cmdOutput) { + Write-Verbose -Message $cmdOutput.Trim() + } + + Write-Verbose -Message "PSConfig Exit Code: $($psconfig.ExitCode)" + return $psconfig.ExitCode + } + # Error codes: https://aka.ms/installerrorcodes + switch ($result) { + 0 { + Write-Verbose -Message "SharePoint Post Setup Configuration Wizard ran successfully" + } + Default { + $message = ("SharePoint Post Setup Configuration Wizard failed, " + ` + "exit code was $result. Error codes can be found at " + ` + "https://aka.ms/installerrorcodes") + throw $message + } + $null { + Write-Verbose -Message "No need to run SharePoint Post Setup Configuration Wizard" + } + } + } + else { + Write-Verbose -Message "There are still some unpatched servers. Skipping running PSConfig!" + Write-Verbose -Message "The following servers aren't on the correct patch level: $($unpatchedServers -join ", ")" + } +} + +function Start-SPSConfigExeRemote { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + + # Check if all servers are on the same patch level before running psconfig.exe + $unpatchedServers = Get-SPSServersPatchStatus | Where-Object { $_.Status -ne "UpgradeRequired" -and $_.Status -ne "UpgradeAvailable" } + if ($unpatchedServers.Count -eq 0) { + Write-Verbose -Message "All servers are on the same patch level. Running PSConfig ..." + # Check which version of SharePoint is installed + $pathToSearch = 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions\*\ISAPI\Microsoft.SharePoint.dll' + $fullPath = Get-Item $pathToSearch -ErrorAction SilentlyContinue | Sort-Object { $_.Directory } -Descending | Select-Object -First 1 + $getSPInstalledProductVersion = (Get-Command $fullPath).FileVersionInfo + + if ($getSPInstalledProductVersion.FileMajorPart -eq 15) { + $wssRegKey = 'hklm:SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\15.0\WSS' + $binaryDir = Join-Path $env:CommonProgramFiles "Microsoft Shared\Web Server Extensions\15\BIN" + } + else { + $wssRegKey = 'hklm:SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\16.0\WSS' + $binaryDir = Join-Path $env:CommonProgramFiles "Microsoft Shared\Web Server Extensions\16\BIN" + } + $psconfigExe = Join-Path -Path $binaryDir -ChildPath "psconfig.exe" + + # Read LanguagePackInstalled and SetupType registry keys + $languagePackInstalled = Get-ItemProperty -LiteralPath $wssRegKey -Name 'LanguagePackInstalled' + $setupType = Get-ItemProperty -LiteralPath $wssRegKey -Name 'SetupType' + + # Determine if LanguagePackInstalled=1 or SetupType=B2B_Upgrade. + # If so, the Config Wizard is required + if (($languagePackInstalled.LanguagePackInstalled -eq 1) -or ($setupType.SetupType -eq "B2B_UPGRADE")) { + Write-Verbose -Message "Starting Configuration Wizard" + Write-Verbose -Message "Starting 'Product Version Job' timer job" + $pvTimerJob = Get-SPTimerJob -Identity 'job-admin-product-version' + $lastRunTime = $pvTimerJob.LastRunTime + + Start-SPTimerJob -Identity $pvTimerJob + + $jobRunning = $true + $maxCount = 30 + $count = 0 + Write-Verbose -Message "Waiting for 'Product Version Job' timer job to complete" + while ($jobRunning -and $count -le $maxCount) { + Start-Sleep -Seconds 10 + + $pvTimerJob = Get-SPTimerJob -Identity 'job-admin-product-version' + $jobRunning = $lastRunTime -eq $pvTimerJob.LastRunTime + + $count++ + } + + # Fix for issue with psconfig on SharePoint 2019 + if ($getSPInstalledProductVersion.FileMajorPart -eq 16) { + Upgrade-SPFarm -ServerOnly -SkipDatabaseUpgrade -SkipSiteUpgrade -Confirm:$false + } + + $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" + $psconfig = Start-Process -FilePath $psconfigExe ` + -ArgumentList "-cmd upgrade -inplace b2b -wait -cmd applicationcontent -install -cmd installfeatures -cmd secureresources -cmd services -install" ` + -RedirectStandardOutput $stdOutTempFile ` + -Wait ` + -PassThru + + $cmdOutput = Get-Content -Path $stdOutTempFile -Raw + Remove-Item -Path $stdOutTempFile + + if ($null -ne $cmdOutput) { + Write-Verbose -Message $cmdOutput.Trim() + } + + Write-Verbose -Message "PSConfig Exit Code: $($psconfig.ExitCode)" + return $psconfig.ExitCode + } + else { + return $null + } + } + # Error codes: https://aka.ms/installerrorcodes + switch ($result) { + 0 { + Write-Verbose -Message "SharePoint Post Setup Configuration Wizard ran successfully" + } + Default { + $message = ("SharePoint Post Setup Configuration Wizard failed, " + ` + "exit code was $result. Error codes can be found at " + ` + "https://aka.ms/installerrorcodes") + throw $message + } + $null { + Write-Verbose -Message "No need to run SharePoint Post Setup Configuration Wizard" + } + } + } + else { + Write-Verbose -Message "There are still some unpatched servers. Skipping running PSConfig!" + Write-Verbose -Message "The following servers aren't on the correct patch level: $($unpatchedServers -join ", ")" + } +} + +function Update-SPSContentDatabase { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + $getSPContentDb = Get-SPContentDatabase -Identity $Name -ErrorAction SilentlyContinue + if ($null -ne $getSPContentDb) { + Write-Verbose -Message "Checking Upgrading status for $($Name) ..." + if ($getSPContentDb.NeedsUpgrade) { + Write-Verbose -Message "Upgrading SharePoint SPContentDatabase $($Name)" + $updateStarted = Get-date + Write-Verbose -Message "Started at $updateStarted - Please Wait ..." + Upgrade-SPContentDatabase $Name -Confirm:$false -Verbose + $updateFinished = Get-date + Write-Verbose -Message "Update for SharePoint SPContentDatabase $($Name) is finished at $updateFinished" + } + else { + Write-Verbose -Message "SPContentDatabase $($Name) already upgraded - No action needed" + } + } + else { + Write-Verbose -Message "SPContentDatabase $($Name) does not exist - No action needed" + } + +} + +function Set-SPSSideBySideToken { + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $BuildVersion, + + [Parameter()] + [System.Boolean] + $EnableSideBySide + ) + + $webApps = Get-SPWebApplication -ErrorAction SilentlyContinue + if ($null -ne $webApps) { + foreach ($webApp in $webApps) { + $spWebAppName = $webApp.Name + if ($EnableSideBySide) { + if ($webApp.WebService.EnableSideBySide) { + Write-Output "EnableSideBySide is already enabled on $spWebAppName Web Application" + } + else { + Write-Output "Enabling EnableSideBySide on $spWebAppName Web Application" + $webApp.WebService.EnableSideBySide = $true + $webApp.WebService.Update() + } + if ($webApp.WebService.SideBySideToken -eq $BuildVersion) { + Write-Output "SideBySideToken $BuildVersion is already enabled on $spWebAppName Web Application" + } + else { + Write-Output "Enabling SideBySideToken $BuildVersion on $spWebAppName Web Application" + $webApp.WebService.SideBySideToken = $BuildVersion + $webApp.WebService.Update() + } + Write-Output 'Running CmdLet Copy-SPSideBySideFiles' + Copy-SPSideBySideFiles -Verbose + } + else { + if ($webApp.WebService.EnableSideBySide) { + Write-Output "Disabling EnableSideBySide on $spWebAppName Web Application" + $webApp.WebService.EnableSideBySide = $false + $webApp.WebService.Update() + } + else { + Write-Output "EnableSideBySide is already disabled on $spWebAppName Web Application" + } + } + } + } + else { + throw 'Did not find SPWebApplication Object' + } +} +function Copy-SPSSideBySideFilesAllServers { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + Write-Output "Running CmdLet Copy-SPSideBySideFiles on server: $($params.Server)" + Copy-SPSideBySideFiles -Verbose + } + return $result +} +function Initialize-SPSContentDbJsonFile { + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Path + ) + + #Initialize jSON Object, variables and class + New-Variable -Name jsonObject ` + -Description 'jSON object variable' ` + -Option AllScope ` + -Force + + $jsonObject = [PSCustomObject]@{} + $tbSPContentDb1 = New-Object -TypeName System.Collections.ArrayList + $tbSPContentDb2 = New-Object -TypeName System.Collections.ArrayList + $tbSPContentDb3 = New-Object -TypeName System.Collections.ArrayList + $tbSPContentDb4 = New-Object -TypeName System.Collections.ArrayList + class SPDbContent { + [System.String]$Name + [System.String]$Server + [System.String]$WebAppUrl + } + + #Get all content databases + $spAllDatabases = Get-SPContentDatabase -ErrorAction SilentlyContinue + + if ($null -ne $spAllDatabases) { + #Calculate the number of databases in each group + $groupSize = [math]::Floor($spAllDatabases.Count / 4) + #Loop through each content database and assign to groups + for ($i = 0; $i -lt $spAllDatabases.Count; $i++) { + $spDatabase = $spAllDatabases[$i] + #Determine which group to add the database to + if ($i -lt $groupSize) { + [void]$tbSPContentDb1.Add([SPDbContent]@{ + Name = $spDatabase.Name; + Server = $spDatabase.Server; + WebAppUrl = $spDatabase.WebApplication.Url; + }) + } + elseif ($i -lt ($groupSize * 2)) { + [void]$tbSPContentDb2.Add([SPDbContent]@{ + Name = $spDatabase.Name; + Server = $spDatabase.Server; + WebAppUrl = $spDatabase.WebApplication.Url; + }) + } + elseif ($i -lt ($groupSize * 3)) { + [void]$tbSPContentDb3.Add([SPDbContent]@{ + Name = $spDatabase.Name; + Server = $spDatabase.Server; + WebAppUrl = $spDatabase.WebApplication.Url; + }) + } + else { + [void]$tbSPContentDb4.Add([SPDbContent]@{ + Name = $spDatabase.Name; + Server = $spDatabase.Server; + WebAppUrl = $spDatabase.WebApplication.Url; + }) + } + } + #Add each array to jsonObject + $jsonObject | Add-Member -MemberType NoteProperty ` + -Name 'SPContentDatabase1' ` + -Value $tbSPContentDb1 + + $jsonObject | Add-Member -MemberType NoteProperty ` + -Name 'SPContentDatabase2' ` + -Value $tbSPContentDb2 + + $jsonObject | Add-Member -MemberType NoteProperty ` + -Name 'SPContentDatabase3' ` + -Value $tbSPContentDb3 + + $jsonObject | Add-Member -MemberType NoteProperty ` + -Name 'SPContentDatabase4' ` + -Value $tbSPContentDb4 + + #Convert jsonObject to JSON and save to a file + $jsonObject | ConvertTo-Json | Set-Content -Path $Path -Force + } +} diff --git a/scripts/Modules/util.psm1 b/scripts/Modules/util.psm1 new file mode 100644 index 0000000..70c87d9 --- /dev/null +++ b/scripts/Modules/util.psm1 @@ -0,0 +1,234 @@ +#region Import Modules +# Import the custom module 'sps.util.psm1' from the script's directory +$scriptModulePath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Import-Module -Name (Join-Path -Path $scriptModulePath -ChildPath 'sps.util.psm1') -Force +#endregion + +function Get-SPSInstalledProductVersion { + [OutputType([System.Version])] + param () + + $pathToSearch = 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions\*\ISAPI\Microsoft.SharePoint.dll' + $fullPath = Get-Item $pathToSearch -ErrorAction SilentlyContinue | Sort-Object { $_.Directory } -Descending | Select-Object -First 1 + if ($null -eq $fullPath) { + throw 'SharePoint path {C:\Program Files\Common Files\microsoft shared\Web Server Extensions} does not exist' + } + else { + return (Get-Command $fullPath).FileVersionInfo + } +} + +function Invoke-SPSCommand { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, # Credential to be used for executing the command + + [Parameter()] + [Object[]] + $Arguments, # Optional arguments for the script block + + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock, # Script block containing the commands to execute + + [Parameter(Mandatory = $true)] + [System.String] + $Server # Target server where the commands will be executed + ) + $VerbosePreference = 'Continue' + # Base script to ensure the SharePoint snap-in is loaded + $baseScript = @" + if (`$null -eq (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue)) + { + Add-PSSnapin Microsoft.SharePoint.PowerShell + } +"@ + + $invokeArgs = @{ + ScriptBlock = [ScriptBlock]::Create($baseScript + $ScriptBlock.ToString()) + } + # Add arguments if provided + if ($null -ne $Arguments) { + $invokeArgs.Add("ArgumentList", $Arguments) + } + # Ensure a credential is provided + if ($null -eq $Credential) { + throw 'You need to specify a Credential' + } + else { + Write-Verbose -Message ("Executing using a provided credential and local PSSession " + "as user $($Credential.UserName)") + # Running garbage collection to resolve issues related to Azure DSC extension use + [GC]::Collect() + # Create a new PowerShell session on the target server using the provided credentials + $session = New-PSSession -ComputerName $Server ` + -Credential $Credential ` + -Authentication CredSSP ` + -Name "Microsoft.SharePoint.PSSession" ` + -SessionOption (New-PSSessionOption -OperationTimeout 0 -IdleTimeout 60000) ` + -ErrorAction Continue + + # Add the session to the invocation arguments if the session is created successfully + if ($session) { + $invokeArgs.Add("Session", $session) + } + try { + # Invoke the command on the target server + return Invoke-Command @invokeArgs -Verbose + } + catch { + throw $_ # Throw any caught exceptions + } + finally { + # Remove the session to clean up + if ($session) { + Remove-PSSession -Session $session + } + } + } +} + +function Clear-SPSLog { + param ( + [Parameter(Mandatory = $true)] + [System.String] + $path, # Path to the log files + + [Parameter()] + [System.UInt32] + $Retention = 180 # Number of days to retain log files + ) + # Check if the log file path exists + if (Test-Path $path) { + # Get the current date + $Now = Get-Date + # Define LastWriteTime parameter based on $Retention + $LastWrite = $Now.AddDays(-$Retention) + # Get files based on last write filter and specified folder + $files = Get-ChildItem -Path $path -Filter "$($logFileName)*" | Where-Object -FilterScript { + $_.LastWriteTime -le "$LastWrite" + } + # If files are found, proceed to delete them + if ($files) { + Write-Output '--------------------------------------------------------------' + Write-Output "Cleaning log files in $path ..." + foreach ($file in $files) { + if ($null -ne $file) { + Write-Output "Deleting file $file ..." + Remove-Item $file.FullName | Out-Null + } + else { + Write-Output 'No more log files to delete' + Write-Output '--------------------------------------------------------------' + } + } + } + else { + Write-Output '--------------------------------------------------------------' + Write-Output "$path - No needs to delete log files" + Write-Output '--------------------------------------------------------------' + } + } + else { + Write-Output '--------------------------------------------------------------' + Write-Output "$path does not exist" + Write-Output '--------------------------------------------------------------' + } +} + +function Add-SPSScheduledTask { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Description, + + [Parameter(Mandatory = $true)] + [System.String] + $PSArguments, + + [Parameter()] + [System.String] + $StartTime + ) + + $fullScriptPath = Join-Path -Path $scriptRootPath -ChildPath $item.Name + $taskArgs = @{ + TaskName = $Name + TaskPath = 'SharePoint' + ScheduleType = 'Once' + Enable = $true + ActionExecutable = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' + ActionArguments = "-ExecutionPolicy Bypass $($fullScriptPath) $($PSArguments)" + ExecuteAsCredential = $FSP + Description = $Description + RunLevel = 'Highest' + } + + $getScheduledTask = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue + if ($null -eq $getScheduledTask) { + if ([string]::IsNullOrEmpty($StartTime)) { + Set-TargetResource @taskArgs + } + else { + Set-TargetResource @taskArgs -StartTime $StartTime + } + } + else { + Write-Output "Scheduled Task $Name already added in SharePoint Task Path" + } +} + +function Remove-SPSScheduledTask { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + $taskArgs = @{ + TaskName = $Name + TaskPath = 'SharePoint' + ScheduleType = 'Once' + Enable = $false + Ensure = 'Absent' + ActionExecutable = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' + ActionArguments = '' + ExecuteAsCredential = $FSP + Description = $Description + RunLevel = 'Highest' + } + $getScheduledTask = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue + if ($getScheduledTask) { + Set-TargetResource @taskArgs + } + else { + Write-Output "Scheduled Task $Name already removed from SharePoint Task Path" + } +} + +function Start-SPSScheduledTask { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + $getScheduledTask = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue + if ($getScheduledTask) { + Start-ScheduledTask -TaskName $Name ` + -TaskPath 'SharePoint' ` + -ErrorAction SilentlyContinue + } + else { + Write-Output "Scheduled Task $Name does not exist in SharePoint Task Path" + } +} diff --git a/scripts/SPSUpdate.ps1 b/scripts/SPSUpdate.ps1 new file mode 100644 index 0000000..25ee141 --- /dev/null +++ b/scripts/SPSUpdate.ps1 @@ -0,0 +1,464 @@ +<# + .SYNOPSIS + SPSUpdate script for SharePoint Server. + + .DESCRIPTION + SPSUpdate is a PowerShell script tool designed to install cumulative updates in your SharePoint environment. + It's compatible with PowerShell version 5.1 and later. + + .PARAMETER ConfigFile + Need parameter ConfigFile, example: + PS D:\> E:\SCRIPT\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' + + .PARAMETER Sequence + Need parameter Sequence for SPS Farm, example: + PS D:\> E:\SCRIPT\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' -Sequence 1 + + .PARAMETER Install + Use the switch Install parameter if you want to add the SPSUpdate script in taskscheduler + InstallAccount parameter need to be set + PS D:\> E:\SCRIPT\SPSUpdate.ps1 -Install -InstallAccount (Get-Credential) -ConfigFile 'contoso-PROD.json' + + .PARAMETER InstallAccount + Need parameter InstallAccount when you use the switch Install parameter + PS D:\> E:\SCRIPT\SPSUpdate.ps1 -Install -InstallAccount (Get-Credential) -ConfigFile 'contoso-PROD.json' + + .PARAMETER Uninstall + Use the switch Uninstall parameter if you want to remove the SPSUpdate script from taskscheduler + PS D:\> E:\SCRIPT\SPSUpdate.ps1 -Uninstall + + .EXAMPLE + SPSUpdate.ps1 -Install -InstallAccount (Get-Credential) -ConfigFile 'contoso-PROD.json' + SPSUpdate.ps1 -Uninstall -ConfigFile 'contoso-PROD.json' + + .NOTES + FileName: SPSUpdate.ps1 + Author: Jean-Cyril DROUHIN + Date: April 20, 2022 + Version: 1.0.0 + + .LINK + https://spjc.fr/ + https://github.com/luigilink/SPSUpdate +#> +param +( + [Parameter(Position = 1, Mandatory = $true)] + [ValidateScript({ (Test-Path $_) -and ($_ -like '*.json') })] + [System.String] + $ConfigFile, # Path to the configuration file + + [Parameter(Position = 2)] + [ValidateRange(1, 4)] + [System.UInt32] + $Sequence, + + [Parameter(Position = 3)] + [switch] + $Install, # Switch parameter to add scheduled tasks + + [Parameter(Position = 4)] + [System.Management.Automation.PSCredential] + $InstallAccount, # Credential for the InstallAccount + + [Parameter(Position = 5)] + [switch] + $Uninstall # Switch parameter to remove scheduled tasks +) + +#region Initialization +# Clear the host console +Clear-Host + +# Set the window title +$Host.UI.RawUI.WindowTitle = "SPSTrust script running on $env:COMPUTERNAME" + +# Define the path to the helper module +$scriptRootPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$script:HelperModulePath = Join-Path -Path $scriptRootPath -ChildPath 'Modules' + +# Import the helper module +Import-Module -Name (Join-Path -Path $script:HelperModulePath -ChildPath 'util.psm1') -Force + +# Import the credentialmanager module +Import-Module -Name (Join-Path -Path (Join-Path -Path $script:HelperModulePath -ChildPath 'credentialmanager') -ChildPath 'CredentialManager.psd1') -Force + +# Ensure the script is running with administrator privileges +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + Throw "Administrator rights are required. Please re-run this script as an Administrator." +} + +# Load the configuration file +try { + if (Test-Path $ConfigFile) { + $jsonEnvCfg = Get-Content $ConfigFile | ConvertFrom-Json + $Application = $jsonEnvCfg.ApplicationName + $Environment = $jsonEnvCfg.ConfigurationName + $scriptFQDN = $jsonEnvCfg.Domain + $spFarmName = $jsonEnvCfg.FarmName + } + else { + Throw "Configuration file '$ConfigFile' not found." + } +} +catch { + Write-Error "Failed to load configuration file: $_" + Exit +} + +# Define variables +$SPSUpdateVersion = '1.0.0' +$getDateFormatted = Get-Date -Format yyyy-MM-dd +$spsUpdateFileName = "$($Application)-$($Environment)-$($getDateFormatted)" +$spsUpdateDBsFile = "$($Application)-$($Environment)-$($spFarmName)-ContentDBs.json" +$currentUser = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name +$pathLogsFolder = Join-Path -Path $scriptRootPath -ChildPath 'Logs' +$pathConfigFolder = Join-Path -Path $scriptRootPath -ChildPath 'Config' + +# Initialize logs +if (-Not (Test-Path -Path $pathLogsFolder)) { + New-Item -ItemType Directory -Path $pathLogsFolder -Force +} +if ($Sequence) { + $pathLogFile = Join-Path -Path $pathlogFolder -ChildPath ("$($spsUpdateFileName)$($Sequence)_" + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log') +} +else { + $pathLogFile = Join-Path -Path $pathLogsFolder -ChildPath ($spsUpdateFileName + '.log') +} +$DateStarted = Get-Date +$psVersion = ($Host).Version.ToString() + +# Start transcript to log the output +Start-Transcript -Path $pathLogFile -IncludeInvocationHeader + +# Output the script information +Write-Output '-----------------------------------------------' +Write-Output "| SPSUpdate Script - v$SPSUpdateVersion" +Write-Output "| Started on - $DateStarted by $currentUser" +Write-Output "| PowerShell Version - $psVersion" +Write-Output '-----------------------------------------------' +#endregion + +#region Main Process + +# 0. Set power management plan to "High Performance" +Write-Verbose -Message "Setting power management plan to 'High Performance'..." +Start-Process -FilePath "$env:SystemRoot\system32\powercfg.exe" -ArgumentList '/s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' -NoNewWindow + +# 1. Load SharePoint Powershell Snapin or Import-Module +try { + $installedVersion = Get-SPSInstalledProductVersion + if ($installedVersion.ProductMajorPart -eq 15 -or $installedVersion.ProductBuildPart -le 12999) { + if ($null -eq (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue)) { + Add-PSSnapin Microsoft.SharePoint.PowerShell + } + } + else { + Import-Module SharePointServer -Verbose:$false -WarningAction SilentlyContinue + } +} +catch { + # Handle errors during retrieval of Installed Product Version + Write-Error -Message @" +Failed to get installed Product Version for $($env:COMPUTERNAME) +Exception: $_ +"@ +} + +# 2. Initialize or read ContentDatabase json file if UpgradeContentDatabase equal to true +try { + if ($jsonEnvCfg.UpgradeContentDatabase) { + if (-Not (Test-Path -Path $pathConfigFolder)) { + # If the path does not exist, create the directory + New-Item -ItemType Directory -Path $pathConfigFolder + } + if (Test-Path $spsUpdateDBsFile) { + Write-Output "Get ContentDatabase json file for SPFARM: $($spFarmName)" + $jsonDbCfg = Get-Content $spsUpdateDBsFile | ConvertFrom-Json + } + else { + # Initialize contentDb json file + "Initialize ContentDatabase json file for SPFARM: $($spFarmName)" + Initialize-SPSContentDbJsonFile -Path $spsUpdateDBsFile + $jsonDbCfg = Get-Content $spsUpdateDBsFile | ConvertFrom-Json + } + } +} +catch { + # Handle errors during Initialize ContentDatabase json file + Write-Error -Message @" +Failed to Initialize ContentDatabase json file for SPFARM: $($spFarmName) +Exception: $_ +"@ +} + +# Check UserName and Password if Install parameter is used +if ($Install) { + if ($null -eq $InstallAccount) { + Write-Warning -Message ('SPSUpdate: Install parameter is set. Please set also InstallAccount ' + ` + "parameter. `nSee https://github.com/luigilink/SPSUpdate/wiki for details.") + Break + } + else { + $UserName = $InstallAccount.UserName + $Password = $InstallAccount.GetNetworkCredential().Password + $currentDomain = 'LDAP://' + ([ADSI]'').distinguishedName + Write-Output "Checking Account `"$UserName`" ..." + $dom = New-Object System.DirectoryServices.DirectoryEntry($currentDomain, $UserName, $Password) + if ($null -eq $dom.Path) { + Write-Warning -Message "Password Invalid for user:`"$UserName`"" + Break + } + else { + # Add Credential in Credential Manager + try { + $credential = Get-StoredCredential -Target "$($jsonEnvCfg.StoredCredential)" -ErrorAction SilentlyContinue + if ($null -eq $credential) { + New-StoredCredential -Credentials $InstallAccount -Target "$($jsonEnvCfg.StoredCredential)" -Type Generic -Persist LocalMachine + } + } + catch { + # Handle errors during Get or Add Credential in Crededential Manager + Write-Error -Message @" +Failed to Get or Add Credential in Crededential Manager for SPFarm: $($spFarmName) +Exception: $_ +"@ + } + # Add scheduled Task for Update Full Script + try { + Write-Output 'Adding Scheduled Task SPSUpdate-FullScript in SharePoint Task Path' + Add-SPSScheduledTask -Name 'SPSUpdate-FullScript' ` + -Description 'Scheduled Task for Update SharePoint Server after installation of cumulative update' ` + -PSArguments "-ConfigFile $ConfigFile -Verbose" ` + -StartTime '23:55:00' + } + catch { + # Handle errors during Add scheduled Task for Update Full Script + Write-Error -Message @" +Failed to Add Scheduled Task in SharePoint Task Path for SPFarm: $($spFarmName) +Exception: $_ +"@ + } + } + } +} +elseif ($Uninstall) { + # Remove scheduled Task for Update Full Script + try { + Write-Output 'Removing Scheduled Task SPSUpdate-FullScript in SharePoint Task Path' + Remove-SPSScheduledTask -Name 'SPSUpdate-FullScript' + foreach ($taskId in (1..4)) { + Write-Output "Removing Scheduled Tasks SPSUpdate-Sequence$taskId in SharePoint Task Path" + Remove-SPSScheduledTask -Name "SPSUpdate-Sequence$taskId" + } + } + catch { + # Handle errors during Remove scheduled Task for Update Full Script + Write-Error -Message @" +Failed to Remove Scheduled Task in SharePoint Task Path for SPFarm: $($spFarmName) +Exception: $_ +"@ + } + # Remove Credential from Credential Manager + try { + $credential = Get-StoredCredential -Target "$($jsonEnvCfg.StoredCredential)" -ErrorAction SilentlyContinue + if ($null -ne $credential) { + Remove-StoredCredential -Target "$($jsonEnvCfg.StoredCredential)" + } + } + catch { + # Handle errors during Get or Remove Credential in Crededential Manager + Write-Error -Message @" +Failed to Get or Remove Credential in Crededential Manager for SPFarm: $($spFarmName) +Exception: $_ +"@ + } + +} +else { + if ($Sequence -ne 0) { + try { + Write-Output "Update Script in progress | Sequence $Sequence - Please Wait ..." + switch ($Sequence) { + 1 { $dbs = $jsonDbCfg.SPContentDatabase1 } + 2 { $dbs = $jsonDbCfg.SPContentDatabase2 } + 3 { $dbs = $jsonDbCfg.SPContentDatabase3 } + 4 { $dbs = $jsonDbCfg.SPContentDatabase4 } + } + foreach ($db in $dbs) { + Update-SPSContentDatabase -Name $db.Name + } + } + catch { + # Handle errors during Update Script Sequence + Write-Error -Message @" +Failed to Upgrade SPContentDatabse '$($db.Name)' during sequence: $($Sequence) +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + + } + else { + # Initialize Security + $credential = Get-StoredCredential -Target "$($jsonEnvCfg.StoredCredential)" -ErrorAction SilentlyContinue + if ($null -ne $credential) { + New-Variable -Name 'ADM' -Value $credential -Force + } + else { + Throw "The Target $($jsonEnvCfg.StoredCredential) not present in Credential Manager. Please contact your administrator." + } + Write-Output "Update Script in progress | FULL Mode - Please Wait ..." + # Update SPContentDatabase + if ($jsonEnvCfg.UpgradeContentDatabase) { + # Add scheduled Task for Upgrade SPContentDatabase in Parallel + foreach ($taskId in (1..4)) { + try { + Write-Output "Adding Scheduled Tasks SPSUpdate-Sequence$taskId in SharePoint Task Path" + Add-SPSScheduledTask -Name "SPSUpdate-Sequence$taskId" ` + -Description "Scheduled Task Sequence$taskId for Update SharePoint Server after installation of cumulative update" ` + -PSArguments "-ConfigFile $ConfigFile -Sequence $taskId -Verbose" + } + catch { + # Handle errors during Add scheduled Task for Update Full Script + Write-Error -Message @" +Failed to Add Scheduled Task in SharePoint Task Path +Task Name: SPSUpdate-Sequence$taskId +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + } + + # Run scheduled Task for Upgrade SPContentDatabase in Parallel + foreach ($taskId in (1..4)) { + try { + Write-Output "Running Scheduled Tasks SPSUpdate-Sequence$taskId in SharePoint Task Path" + Start-SPSScheduledTask -Name "SPSUpdate-Sequence$taskId" + Write-Output 'Avoid conflicts with OWSTimer process - Pause between 60 to 90 seconds' + Start-Sleep -Seconds (get-random (60..90)) + } + catch { + # Handle errors during Start scheduled Task for Upgrade SPContentDatabase in Parallel + Write-Error -Message @" +Failed to Start Scheduled Task in SharePoint Task Path +Task Name: SPSUpdate-Sequence$taskId +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + + } + + # Wait until all scheduled Tasks are finished + # Define list variable of scheduled tasks + $scheduledTasks = @('SPSUpdate-Sequence1', 'SPSUpdate-Sequence2', 'SPSUpdate-Sequence3', 'SPSUpdate-Sequence4') + + # Continuously check the status of tasks until all are finished + $allTasksFinished = $false + while (-not $allTasksFinished) { + $allTasksFinished = $true + foreach ($scheduledTask in $scheduledTasks) { + $taskStatus = Get-ScheduledTask -TaskName $scheduledTask | Select-Object State + if ($taskStatus.State -ne 'Running') { + Write-Output "Scheduled Task $($scheduledTask) has finished or is not running" + } + else { + $allTasksFinished = $false + } + } + if (-not $allTasksFinished) { + Write-Output 'At least one taskg is still running. Waiting...' + Start-Sleep -Seconds 10 + } + } + Write-Output "All Scheduled Tasks have finished" + } + + # Run SPConfigWizard on Master SharePoint Server + try { + Write-Output "Getting status of Configuration Wizard on server: $($env:COMPUTERNAME)" + Start-SPSConfigExe + } + catch { + # Handle errors during Run SPConfigWizard on Master SharePoint Server + Write-Error -Message @" +Failed to Run SPConfigWizard on Master SharePoint Server +Target Server: $($env:COMPUTERNAME) +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + + + # Run SPConfigWizard on other SharePoint Server + $spServers = Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' -and $_.Address -ne "$($env:COMPUTERNAME)" } + foreach ($server in $spServers) { + try { + $spTargetServer = "$($server.Name).$($scriptFQDN)" + Write-Output "Getting status of Configuration Wizard on server: $($server.Name)" + Start-SPSConfigExeRemote -Server $spTargetServer -InstallAccount $ADM + } + catch { + # Handle errors during Run SPConfigWizard on remote SharePoint Server + Write-Error -Message @" +Failed to Run SPConfigWizard on Remote SharePoint Server +Target Server: $($spTargetServer) +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + } + + # Enable SideBySideToken and run Copy-SPSideBySideFiles on master server + if ($null -ne $jsonEnvCfg.SideBySideToken.BuildVersion) { + try { + Write-Output "Configuring SharePoint SideBySideToken on farm $($spFarmName)" + Set-SPSSideBySideToken -BuildVersion "$($jsonEnvCfg.SideBySideToken.BuildVersion)" -EnableSideBySide $jsonEnvCfg.SideBySideToken.Enable + } + catch { + # Handle errors during Run Set-SPSSideBySideToken + Write-Error -Message @" +Failed to Run Set-SPSSideBySideToken CmdLet +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + } + + # Run Copy-SPSideBySideFiles on other servers + if ($jsonEnvCfg.SideBySideToken.Enable) { + $spServers = Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' -and $_.Address -ne "$($env:COMPUTERNAME)" } + foreach ($server in $spServers) { + try { + $spTargetServer = "$($server.Name).$($scriptFQDN)" + Copy-SPSSideBySideFilesAllServers -Server $spTargetServer -InstallAccount $ADM + } + catch { + # Handle errors during Run Copy-SPSSideBySideFilesAllServers + Write-Error -Message @" +Failed to Run Copy-SPSideBySideFiles CmdLet +Target Server: $($spTargetServer) +Target SPFarm: $($spFarmName) +Exception: $_ +"@ + } + } + } + } +} +#endregion + +# Clean-Up +Trap { Continue } +$DateEnded = Get-Date +Write-Output '-----------------------------------------------' +Write-Output "| SPSUpdate Script Completed" +Write-Output "| Started on - $DateStarted" +Write-Output "| Ended on - $DateEnded" +Write-Output '-----------------------------------------------' +Stop-Transcript +Remove-Variable * -ErrorAction SilentlyContinue +Remove-Module * -ErrorAction SilentlyContinue +$error.Clear() +Exit diff --git a/wiki/Configuration.md b/wiki/Configuration.md new file mode 100644 index 0000000..f49192a --- /dev/null +++ b/wiki/Configuration.md @@ -0,0 +1,56 @@ +# Configuration + +To customize the script for your environment, you need to prepare a JSON configuration file. Below is a sample structure for the file: + +```json +{ + "$schema": "http://json-schema.org/schema#", + "contentVersion": "1.0.0.0", + "ConfigurationName": "PROD", + "ApplicationName": "contoso", + "FarmName": "CONTENT", + "Domain": "contoso.com", + "StoredCredential": "PROD-ADM", + "Binaries": { + "SetupFullPath": "\\\\srvfileshared.contoso.com\\cumulativeupdates", + "SetupFileName": ["uber-subscription-kb5002651-fullfile-x64-glb.exe"], + "ShutdownServices": true + }, + "UpgradeContentDatabase": true, + "SideBySideToken": { + "Enable": true, + "BuildVersion": "16.0.17928.20238" + } +} +``` + +## Configuration, Application and FarmName + +`ConfigurationName` is used to populate the content of `Environment` PowerShell Variable. +`ApplicationName` is used to populate the content of `Application` PowerShell Variable. +`FarmName` is used to populate the content of `FarmName` PowerShell Variable. + +## Credential Manager + +`StoredCredential` is refered to the target of your credential that you used during the installation processus. + +## Binaries settings + +Use `SetupFullPath`, `SetupFileName` and `ShutdownServices` parameters to configure your binaries settings in your environment + +## UpgradeContentDatabase + +The `UpgradeContentDatabase` parameter can be used to run upgrade-SPContentDatabase in parallel. + +The authorized values are : `true`, and `false`. + +## SideBySideToken + +Use `Enable` to enable sidebysidetoken feature. +Use `BuildVersion` to set build version used in sidebysitetoken feature. + +Zero downtime patching is a method of patching and upgrade developed in SharePoint in Microsoft 365. For more details see [SharePoint Server zero downtime patching steps](https://learn.microsoft.com/en-us/sharepoint/upgrade-and-update/sharepoint-server-2016-zero-downtime-patching-steps) + +## Next Step + +For the next steps, go to the [Usage](./Usage) page. diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md new file mode 100644 index 0000000..671afce --- /dev/null +++ b/wiki/Getting-Started.md @@ -0,0 +1,47 @@ +# Getting Started + +## Prerequisites + +- PowerShell 5.0 or later +- CredSSP configured +- Administrative privileges on the SharePoint Server +- StoredCredential configured (if using `Install`) + +## Configure CredSSP + +### Option 1: Manually configure CredSSP + +You can manually configure CredSSP through the use of some PowerShell cmdlet's (and potentially group policy to configure the allowed delegate computers). Some basic instructions can be found at [https://technet.microsoft.com/en-us/magazine/ff700227.aspx](https://technet.microsoft.com/en-us/magazine/ff700227.aspx). + +### Option 2: Configure CredSSP through a DSC resource + +It is possible to use a DSC resource to configure your CredSSP settings on a server, and include this in all of your SharePoint server configurations. This is done through the use of the [xCredSSP](https://github.com/PowerShell/xCredSSP) resource. The below example shows how this can be used. + +```powershell +xCredSSP CredSSPServer { Ensure = "Present"; Role = "Server" } +xCredSSP CredSSPClient { Ensure = "Present"; Role = "Client"; DelegateComputers = $CredSSPDelegates } +``` + +In the above example, `$CredSSPDelegates` can be a wildcard name (such as "\*.contoso.com" to allow all servers in the contoso.com domain), or a list of specific servers (such as "server1", "server 2" to allow only specific servers). + +## Installation + +1. [Download the latest release](https://github.com/luigilink/SPSUpdate/releases/latest) and unzip to a directory on your SharePoint Server. +2. Prepare your JSON configuration file with the required Cumulative Updates and farm details. +3. Add the script in task scheduler by running the following command: + +```powershell +.\SPSUpdate.ps1 -ConfigFile 'contoso-PROD-CONTENT.json' -Install -InstallAccount (Get-Credential) +``` + +> [!IMPORTANT] +> Configure the StoredCredential parameter in JSON before running the script in installation mode. +> Run the Install mode with the same account than you used the in InstallAccount parameter + +## Next Step + +For the next steps, go to the [Configuration](./Configuration) page. + +## Change log + +A full list of changes in each version can be found in the [change log](https://github.com/luigilink/SPSUpdate/blob/main/CHANGELOG.md). diff --git a/wiki/Usage.md b/wiki/Usage.md new file mode 100644 index 0000000..32b8306 --- /dev/null +++ b/wiki/Usage.md @@ -0,0 +1,35 @@ +# Usage + +## Parameters + +| Parameter | Description | +| ----------------- | --------------------------------------------------------- | +| `-ConfigFile` | Specifies the path to the configuration file. | +| `-Sequence` | Specifies the Sequence for parallel upgrade Content DB. | +| `-Install` | Add the SPSUpdate script in task scheduler | +| `-InstallAccount` | Specifies the service account who runs the scheduled task | +| `-Uninstall` | Remove the SPSUpdate script from task scheduler | + +### Basic Usage Example + +```powershell +.\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' +``` + +### Sequence Example + +```powershell +.\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' -Sequence 1 +``` + +### Installation Usage Example + +```powershell +.\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' -Install -InstallAccount (Get-Credential) +``` + +### Uninstallation Usage Example + +```powershell +.\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' -Uninstall +```