From d4de5b2154e2b2c471e8cf259f4fb15ea9d59b72 Mon Sep 17 00:00:00 2001 From: awarre <30052685+awarre@users.noreply.github.com> Date: Fri, 31 Jul 2020 15:06:34 -0400 Subject: [PATCH] Create Optimize-WsusServer.ps1 Initial code commit! --- Optimize-WsusServer.ps1 | 871 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 871 insertions(+) create mode 100644 Optimize-WsusServer.ps1 diff --git a/Optimize-WsusServer.ps1 b/Optimize-WsusServer.ps1 new file mode 100644 index 0000000..8278f31 --- /dev/null +++ b/Optimize-WsusServer.ps1 @@ -0,0 +1,871 @@ +<# +.SYNOPSIS + Comprehensive Windows Server Update Services (WSUS) configuration and optimization script. +.DESCRIPTION + Comprehensive Windows Server Update Services (WSUS) configuration and optimization script. + Features: + -Deep cleaning search and removal of unnecessary updates and drives by product title and update title. + -IIS Configuration validation and optimization. + -Disable device driver syncronization and caching. + -WSUS integrated update and computer cleanup + -Microsoft best practice WSUS database optimization and re-indexing + -Creation of daily and weekly optimization scheduled tasks. + +.PARAMETER FirstRun + Presents a series of prompts for user to initiate all recommended first run optimization tasks. + +.PARAMETER DisableDrivers + Disable device driver syncronization and caching. + +.PARAMETER DeepClean + Searches through most likely categories for unneeded updates and drivers to free up massive amounts of storage and improve database responsiveness. Prompts user to approve removal before deletion. + +.PARAMETER CheckConfig + Validates current WSUS IIS configuration against recommended settings. Helps prevent frequent WSUS/IIS/SQL service crashes and the "RESET SERVER NODE" error. + +.PARAMETER OptimizeServer + Runs all of Microsoft's built-in WSUS cleanup processes. + +.PARAMETER OptimizeDatabase + Runs Microsoft's recommended SQL reindexing script. + +.PARAMETER InstallDailyTask + Creates a scheduled task to run the OptimizeServer function nightly. + +.PARAMETER InstallWeeklyTask + Creates a scheduled task to run the OptimizeDatabase function weekly. + +.NOTES + Version: 1.0 + Author: Austin Warren + Creation Date: 2020/07/31 + +.EXAMPLE + Optimize-WsusServer.ps1 -FirstRun + Optimize-WsusServer.ps1 -DeepClean + Optimize-WsusServer.ps1 -InstallDailyTask -CheckConfig -OptimizeServer +#> + + +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $FirstRun = $false, + [Parameter()] + [switch] + $DisableDrivers = $false, + [Parameter()] + [switch] + $DeepClean = $false, + [Parameter()] + [switch] + $CheckConfig = $false, + [Parameter()] + [switch] + $InstallDailyTask = $false, + [Parameter()] + [switch] + $InstallWeeklyTask = $false, + [Parameter()] + [switch] + $OptimizeServer = $false, + [Parameter()] + [switch] + $OptimizeDatabase = $false +) +#----------------------------------------------------------[Declarations]---------------------------------------------------------- + + +# Recommended IIS settings: https://www.reddit.com/r/sysadmin/comments/996xul/getting_2016_updates_to_work_on_wsus/ +$recommendedIISSettings = @{ + QueueLength = 25000 + LoadBalancerCapabilities = 'TcpLevel' + CpuResetInterval = 15 + RecyclingMemory = 0 + RecyclingPrivateMemory = 0 + ClientMaxRequestLength = 204800 + ClientExecutionTimeout = 7200 +} + +<# +DeepClean Tips + +To find potentially unneeded updates: + 1. WSUS management console + 2. Updates > All Updates + 3. Approval: Approved, Status: No Status + 4. Look for unused products + 5. Add titles to respective arrays below + +Get-WsusProduct - Lists all Microsoft WSUS product categories. +#> + +# Common unneeded updates by ProductTitles +$unneededUpdatesbyProductTitles = @( + "Forefront Identity Manager 2010", + "Microsoft Lync Server 2010", + "Microsoft Lync Server 2013", + "Office 2003", + "Office 2007", + "Office 2010", + "Office 2002/XP", + "SQL Server 2000", + "SQL Server 2005", + "SQL Server 2008", + "Virtual PC", + "Windows 2000", + "Windows 7", + "Windows 8 Embedded", + "Windows 8", + "Windows 8.1", + "Windows Server 2003", + "Windows Server 2003", + "Windows Server 2008 R2", + "Windows Server 2008", + "Windows Ultimate Extras", + "Windows Vista", + "Windows Vista", + "Windows XP Embedded", + "Windows XP x64 Edition", + "Windows XP" +) + +# Common unneeded updates by Title +$unneededUpdatesbyTitle = @( + "Internet Explorer 6", + "Internet Explorer 7", + "Internet Explorer 8", + "Internet Explorer 9", + "Language Interface Pack", + "Windows 10 (consumer editions)", + "Windows 10 Education", + "Windows 10 Enterprise N", + "Itanium", + "ARM64" +) + +<# +REFERENCES + The complete guide to Microsoft WSUS and Configuration Manager SUP maintenance + https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint + + Invoke-WsusServerCleanup + https://docs.microsoft.com/en-us/powershell/module/wsus/Invoke-WsusServerCleanup?view=win10-ps + + Reindex the WSUS Database + https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd939795(v=ws.10) + + Invoke-Sqlcmd + https://docs.microsoft.com/en-us/powershell/module/sqlserver/invoke-sqlcmd?view=sqlserver-ps + + How to Check if an Index Exists on a Table in SQL Server + https://littlekendra.com/2016/01/28/how-to-check-if-an-index-exists-on-a-table-in-sql-server/ +#> + +<# + "[U]sed to create custom indexes in the SUSDB database. This is a one-time process, which is optional but recommended, as doing so will greatly improve performance during subsequent cleanup operations." + Modified to check if indexes already exist before creating them. +#> +$createCustomIndexesSQLQuery = @" +USE [SUSDB] +IF 0 = (SELECT COUNT(*) as index_count + FROM sys.indexes + WHERE object_id = OBJECT_ID('[dbo].[tbLocalizedPropertyForRevision]') + AND name='nclLocalizedPropertyID') +BEGIN +-- Create custom index in tbLocalizedPropertyForRevision + CREATE NONCLUSTERED INDEX [nclLocalizedPropertyID] ON [dbo].[tbLocalizedPropertyForRevision] + ( + [LocalizedPropertyID] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +END +ELSE +BEGIN + PRINT '[nclLocalizedPropertyID] ON [dbo].[tbLocalizedPropertyForRevision] already exists' +END ; +GO +IF 0 = (SELECT COUNT(*) as index_count + FROM sys.indexes + WHERE object_id = OBJECT_ID('[dbo].[tbRevisionSupersedesUpdate]') + AND name='nclSupercededUpdateID') +BEGIN +-- Create custom index in tbRevisionSupersedesUpdate + CREATE NONCLUSTERED INDEX [nclSupercededUpdateID] ON [dbo].[tbRevisionSupersedesUpdate] + ( + [SupersededUpdateID] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]; +END +ELSE +BEGIN + PRINT '[nclSupercededUpdateID] ON [dbo].[tbRevisionSupersedesUpdate] already exists' +END ; +GO +"@ + +<# + Microsoft recommended database maintenance script + + "The performance of large Windows Server Update Services (WSUS) deployments will degrade over time if the WSUS database + is not maintained properly. The WSUSDBMaintenance script is a T-SQL script that can be run by SQL Server administrators + to re-index and defragment WSUS databases. It should not be used on WSUS 2.0 databases.This script contributed by the + Microsoft WSUS team." + + Reference: https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint +#> +$wsusDBMaintenanceSQLQuery = @" +/****************************************************************************** +This sample T-SQL script performs basic maintenance tasks on SUSDB +1. Identifies indexes that are fragmented and defragments them. For certain + tables, a fill-factor is set in order to improve insert performance. + Based on MSDN sample at http://msdn2.microsoft.com/en-us/library/ms188917.aspx + and tailored for SUSDB requirements +2. Updates potentially out-of-date table statistics. +******************************************************************************/ + +USE SUSDB; +GO +SET NOCOUNT ON; + +-- Rebuild or reorganize indexes based on their fragmentation levels +DECLARE @work_to_do TABLE ( + objectid int + , indexid int + , pagedensity float + , fragmentation float + , numrows int +) + +DECLARE @objectid int; +DECLARE @indexid int; +DECLARE @schemaname nvarchar(130); +DECLARE @objectname nvarchar(130); +DECLARE @indexname nvarchar(130); +DECLARE @numrows int +DECLARE @density float; +DECLARE @fragmentation float; +DECLARE @command nvarchar(4000); +DECLARE @fillfactorset bit +DECLARE @numpages int + +-- Select indexes that need to be defragmented based on the following +-- * Page density is low +-- * External fragmentation is high in relation to index size +PRINT 'Estimating fragmentation: Begin. ' + convert(nvarchar, getdate(), 121) +INSERT @work_to_do +SELECT + f.object_id + , index_id + , avg_page_space_used_in_percent + , avg_fragmentation_in_percent + , record_count +FROM + sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'SAMPLED') AS f +WHERE + (f.avg_page_space_used_in_percent < 85.0 and f.avg_page_space_used_in_percent/100.0 * page_count < page_count - 1) + or (f.page_count > 50 and f.avg_fragmentation_in_percent > 15.0) + or (f.page_count > 10 and f.avg_fragmentation_in_percent > 80.0) + +PRINT 'Number of indexes to rebuild: ' + cast(@@ROWCOUNT as nvarchar(20)) + +PRINT 'Estimating fragmentation: End. ' + convert(nvarchar, getdate(), 121) + +SELECT @numpages = sum(ps.used_page_count) +FROM + @work_to_do AS fi + INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id + INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id + +-- Declare the cursor for the list of indexes to be processed. +DECLARE curIndexes CURSOR FOR SELECT * FROM @work_to_do + +-- Open the cursor. +OPEN curIndexes + +-- Loop through the indexes +WHILE (1=1) +BEGIN + FETCH NEXT FROM curIndexes + INTO @objectid, @indexid, @density, @fragmentation, @numrows; + IF @@FETCH_STATUS < 0 BREAK; + + SELECT + @objectname = QUOTENAME(o.name) + , @schemaname = QUOTENAME(s.name) + FROM + sys.objects AS o + INNER JOIN sys.schemas as s ON s.schema_id = o.schema_id + WHERE + o.object_id = @objectid; + + SELECT + @indexname = QUOTENAME(name) + , @fillfactorset = CASE fill_factor WHEN 0 THEN 0 ELSE 1 END + FROM + sys.indexes + WHERE + object_id = @objectid AND index_id = @indexid; + + IF ((@density BETWEEN 75.0 AND 85.0) AND @fillfactorset = 1) OR (@fragmentation < 30.0) + SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE'; + ELSE IF @numrows >= 5000 AND @fillfactorset = 0 + SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90)'; + ELSE + SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD'; + PRINT convert(nvarchar, getdate(), 121) + N' Executing: ' + @command; + EXEC (@command); + PRINT convert(nvarchar, getdate(), 121) + N' Done.'; +END + +-- Close and deallocate the cursor. +CLOSE curIndexes; +DEALLOCATE curIndexes; + +IF EXISTS (SELECT * FROM @work_to_do) +BEGIN + PRINT 'Estimated number of pages in fragmented indexes: ' + cast(@numpages as nvarchar(20)) + SELECT @numpages = @numpages - sum(ps.used_page_count) + FROM + @work_to_do AS fi + INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id + INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id + + PRINT 'Estimated number of pages freed: ' + cast(@numpages as nvarchar(20)) +END +GO + +--Update all statistics +PRINT 'Updating all statistics.' + convert(nvarchar, getdate(), 121) +EXEC sp_updatestats +PRINT 'Done updating statistics.' + convert(nvarchar, getdate(), 121) +GO +"@ + +#-----------------------------------------------------------[Functions]------------------------------------------------------------ + +function Confirm-Prompt ($prompt) { + <# + .SYNOPSIS + Y/N confirmation prompt. + + .DESCRIPTION + Displays Y/N confirmation prompt and returns true or false. + + .PARAMETER prompt + String displayed as prompt + + .EXAMPLE + Confirm-Prompt "Is this a question?" + #> + Write-Host "$prompt Y/N: " -BackgroundColor Blue -ForegroundColor White -NoNewline + $confirm = Read-Host + + if ($confirm.ToLower() -eq 'y') { + return 1 + } else { + return 0 + } +} + +function Optimize-WsusUpdates { + <# + .SYNOPSIS + Runs all built-in WSUS cleanup processes. + + .DESCRIPTION + Runs all built-in WSUS cleanup processes. + + .LINK + https://docs.microsoft.com/en-us/powershell/scripting/developer/help/examples-of-comment-based-help?view=powershell-7 + #> + + Write-Host "Deleting obsolete computers from WSUS database" + Invoke-WsusServerCleanup -CleanupObsoleteComputers + + Write-Host "Deleting obsolete updates" + Invoke-WsusServerCleanup -CleanupObsoleteUpdates + + Write-Host "Deleting unneeded content files" + Invoke-WsusServerCleanup -CleanupUnneededContentFiles + + Write-Host "Deleting obsolete update revisions" + Invoke-WsusServerCleanup -CompressUpdates + + Write-Host "Declining expired updates" + Invoke-WsusServerCleanup -DeclineExpiredUpdates + + Write-Host "Declining superceded updates" + Invoke-WsusServerCleanup -DeclineSupersededUpdates +} + +function Optimize-WsusDatabase { + <# + .SYNOPSIS + Runs WSUS database optimization. + + .DESCRIPTION + Runs Microsoft's recommended WSUS database optimization. + + .LINK + https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint + + .LINK + https://devblogs.microsoft.com/scripting/10-tips-for-the-sql-server-powershell-scripter/ + #> + + # Check registry for WSUS database install type (SQL or WID) + $wsusSqlServerName = (get-itemproperty "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "SqlServername").SqlServername + + # Set the named pipe to use based on WSUS db type + if ($wsusSqlServerName -match 'SQLEXPRESS') { + $serverInstance = 'np:\\.\pipe\MSSQL$SQLEXPRESS\sql\query' + } + elseif (($wsusSqlServerName -match '##WIN') -Or ($wsusSqlServerName -match '##SSEE')) { + #Check OS version + $osVersion = [decimal]("$(([environment]::OSVersion.Version).Major).$(([environment]::OSVersion.Version).Minor)") + + if ($osVersion -gt 6.2) { + $serverInstance = 'np:\\.\pipe\MICROSOFT##WID\tsql\query' + } + else { + $serverInstance = 'np:\\.\pipe\MSSQL$MICROSOFT##SSEE\sql\query' + } + } + else { + write-host "No supported WSUS database found: $serverInstance" + } + + # Setting query timeout value because both of these scripts are prone to timeout + # https://devblogs.microsoft.com/scripting/10-tips-for-the-sql-server-powershell-scripter/ + + Write-Host "Creating custom indexes in WSUS index if they don't already exist. This will speed up future database optimizations." + #Create custom indexes in the database if they don't already exist + Invoke-Sqlcmd -query $createCustomIndexesSQLQuery -ServerInstance $serverInstance -QueryTimeout 120 + + Write-Host "Running WSUS SQL database maintenence script. This can take an extremely long time on the first run." + #Run the WSUS SQL database maintenance script + Invoke-Sqlcmd -query $wsusDBMaintenanceSQLQuery -ServerInstance $serverInstance -QueryTimeout 40000 +} + +function New-WsusMaintainenceTask($interval) { + <# + .SYNOPSIS + Creates a new WSUS optimization scheduled tasks. + + .DESCRIPTION + Creates or overwrites daily or weekly scheduled tasks for WSUS update and database optimization. + + .PARAMETER interval + Specifies "Daily" or "Weekly" tasks + + .LINK + https://docs.microsoft.com/en-us/powershell/module/scheduledtasks/?view=win10-ps + #> + + $taskName = "Optimize WSUS Server ($interval)" + $scriptPath = 'C:\Scripts' + + # Delete scheduled task with the same name if it already exists + If (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + Write-Host "Unregistered Schedule Task: $taskName" + } + + # Change scheduled action based on Daily or Weekly + switch ($interval) { + 'Daily' { + $trigger = New-ScheduledTaskTrigger -Daily -At "12pm" + $scriptAction = "-OptimizeServer" + } + 'Weekly' { + $trigger = New-ScheduledTaskTrigger -Weekly -At "2am" -DaysOfWeek Sunday + $scriptAction = "-OptimizeDatabase" + } + Default {} + } + + $scriptName = Split-Path $MyInvocation.PSCommandPath -Leaf + + #Create "C:\Scripts" to store PS script + $null = New-Item -Path "$scriptPath" -ItemType Directory -Force + Write-Host "Created Directory: $scriptPath" + + # Copy current script to script + Copy-Item -Path $PSCommandPath -Destination $scriptPath -Force + Write-Host "Copied Script: $scriptName" + + # Create and register the scheduled task + $task = New-ScheduledTaskAction ` + -Execute "powershell.exe" ` + -Argument "-Command `"&'$($scriptPath)`\$($scriptName)'$scriptAction`"" + + $settings = New-ScheduledTaskSettingsSet + $principal = New-ScheduledTaskPrincipal ` + -UserId "NT AUTHORITY\SYSTEM" ` + -LogonType ServiceAccount ` + -RunLevel Highest + + # Sending to $null to supress output + $null = Register-ScheduledTask $taskName -Action $task -Trigger $trigger -Settings $settings -Principal $principal + + Write-Host "Registered Scheduled Task: $taskName" +} + +function Get-WsusIISConfig { + <# + .SYNOPSIS + Returns a hash of all WSUS optimization related IIS settings. + + .DESCRIPTION + Determines WSUS IIS Site and Pool, and then forms hash of all relevant optimization settings. + + .LINK + https://docs.microsoft.com/en-us/powershell/module/webadminstration/get-webapplication?view=winserver2012-ps + + .LINK + https://docs.microsoft.com/en-us/powershell/module/iisadministration/get-iissite?view=win10-ps + #> + + # Get WSUS IIS Index from registry + $iisSiteIndex = Get-ItemPropertyValue "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "IISTargetWebSiteIndex" + + # IIS Site + $iisSiteName = Get-IISSite | Where-Object -Property "Id" -Eq $iisSiteIndex | Select-Object -ExpandProperty "Name" + + # Site Application Pool + $iisAppPool = Get-WebApplication -site $iisSiteName -Name "ClientWebService" | Select-Object -ExpandProperty "applicationPool" + + # Application Pool Config + $iisApplicationPoolConfig = Get-IISConfigCollection -ConfigElement (Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools") + + # WSUS Pool Config Root + $wsusPoolConfig = Get-IISConfigCollectionElement -ConfigCollection $iisApplicationPoolConfig -ConfigAttribute @{"name" = "$iisAppPool" } + + # Queue Length + $queueLength = Get-IISConfigAttributeValue -ConfigElement $wsusPoolConfig -AttributeName "queueLength" + + #Load Balancer Capabilities + $wsusPoolFailureConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "failure" + $loadBalancerCapabilities = Get-IISConfigAttributeValue -ConfigElement $wsusPoolFailureConfig -AttributeName "loadBalancerCapabilities" + + # CPU Reset Interval + $wsusPoolCpuConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "cpu" + $cpuResetInterval = (Get-IISConfigAttributeValue -ConfigElement $wsusPoolCpuConfig -AttributeName "resetInterval").TotalMinutes + + # Recycling Config Root + $wsusPoolRecyclingConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "recycling" | Get-IISConfigElement -ChildElementName "periodicRestart" + + $recyclingMemory = Get-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "memory" + $recyclingPrivateMemory = Get-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "privateMemory" + + $clientWebServiceConfig = Get-WebConfiguration -PSPath 'IIS:\Sites\WSUS Administration\ClientWebService' -Filter "system.web/httpRuntime" + + $clientMaxRequestLength = $clientWebServiceConfig | select-object -ExpandProperty maxRequestLength + $clientExecutionTimeout = ($clientWebServiceConfig | select-object -ExpandProperty executionTimeout).TotalSeconds + + # Return hash of IIS settings + @{ + QueueLength = $queueLength + LoadBalancerCapabilities = $loadBalancerCapabilities + CpuResetInterval = $cpuResetInterval + RecyclingMemory = $recyclingMemory + RecyclingPrivateMemory = $recyclingPrivateMemory + ClientMaxRequestLength = $clientMaxRequestLength + ClientExecutionTimeout = $clientExecutionTimeout + } +} + +function Test-WsusIISConfig ($settings, $recommended) { + <# + .SYNOPSIS + Compares current WSUS IIS settings to recommended values. + + .DESCRIPTION + Compares current WSUS IIS settings to recommended values. Prompts user to commit changes. + + .PARAMETER settings + Hash of current WSUS IIS settings. + + .PARAMETER recommended + Hash of recommended WSUS IIS settings. + #> + + # Delay IIS configuration commits until we're done updating all necessary settings + Start-IISCommitDelay + + foreach ($key in $recommended.Keys) { + # If the current configuration setting doesn't match the recommended value, prompt the user to update + # This could be better designed to match minimum requirements instead of specific values, but it isn't. + If ($recommended[$key] -ne $settings[$key]) { + Write-Host "$key`n`tCurrent:`t$($settings[$key])`n`tRecommended:`t$($recommended[$key])" -BackgroundColor Black -ForegroundColor Red + + if (Confirm-Prompt "Update $key to recommended value?") { + Update-WsusIISConfig $key $recommended[$key] + } + } + else { + Write-Host "$key`n`tCurrent:`t$($settings[$key])`n`tRecommended:`t$($recommended[$key])" -BackgroundColor Black -ForegroundColor Green + } + } + + # Allow IIS config commits again + Stop-IISCommitDelay +} + +function Update-WsusIISConfig ($settingKey, $recommendedValue) { + <# + .SYNOPSIS + Modifies IIS configuration for specified setting. + + .DESCRIPTION + Modifies specified IIS setting for WSUS IIS Site/App Pool optimization. + + .PARAMETER settingKey + String used to reference specific IIS configuration setting. + + .PARAMETER recommendedValue + Recommended value for WSUS IIS configuration setting. + #> + + # WSUS IIS Index + $iisSiteIndex = Get-ItemPropertyValue "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "IISTargetWebSiteIndex" + + # IIS Site + $iisSiteName = Get-IISSite | Where-Object -Property "Id" -Eq $iisSiteIndex | Select-Object -ExpandProperty "Name" + + # Site Application Pool + $iisAppPool = Get-WebApplication -site $iisSiteName -Name "ClientWebService" | Select-Object -ExpandProperty "applicationPool" + + # Application Pool Config + $iisApplicationPoolConfig = Get-IISConfigCollection -ConfigElement (Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools") + + # WSUS Pool Config Root + $wsusPoolConfig = Get-IISConfigCollectionElement -ConfigCollection $iisApplicationPoolConfig -ConfigAttribute @{"name" = "$iisAppPool" } + + # Recycling Config Root + $wsusPoolRecyclingConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "recycling" | Get-IISConfigElement -ChildElementName "periodicRestart" + + switch ($settingKey) { + 'QueueLength' { + # Queue Length + Set-IISConfigAttributeValue -ConfigElement $wsusPoolConfig -AttributeName "queueLength" -AttributeValue $recommendedValue + } + 'LoadBalancerCapabilities' { + # Failure Config Root + $wsusPoolFailureConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "failure" + + # Load Balancer Capabilities + Set-IISConfigAttributeValue -ConfigElement $wsusPoolFailureConfig -AttributeName "loadBalancerCapabilities" -AttributeValue $recommendedValue + } + 'CpuResetInterval' { + # CPU Reset Interval + $wsusPoolCpuConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "cpu" + Set-IISConfigAttributeValue -ConfigElement $wsusPoolCpuConfig -AttributeName "resetInterval" -AttributeValue ([timespan]::FromMinutes($recommendedValue)) + } + 'RecyclingMemory' { + Set-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "memory" -AttributeValue $recommendedValue + + } + 'RecyclingPrivateMemory' { + Set-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "privateMemory" -AttributeValue $recommendedValue + } + 'ClientMaxRequestLength' { + Set-WebConfigurationProperty -PSPath 'IIS:\Sites\WSUS Administration\ClientWebService' -Filter "system.web/httpRuntime" -Name "maxRequestLength" -Value $recommendedValue + } + 'ClientExecutionTimeout' { + Set-WebConfigurationProperty -PSPath 'IIS:\Sites\WSUS Administration\ClientWebService' -Filter "system.web/httpRuntime" -Name "executionTimeout" -Value ([timespan]::FromSeconds($recommendedValue)) + } + Default {} + } + + Write-Host "Updated IIS Setting: $settingKey, $recommendedValue" -BackgroundColor Green -ForegroundColor Black +} + +function RemoveUpdates($searchStrings, $updateProp, $force=$false) { + [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null + $wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer(); + $scope = New-Object Microsoft.UpdateServices.Administration.UpdateScope + $updates = $wsusServer.GetUpdates($scope) + $declinedCount = 0 + $searchCount = 0 + $userMsg = 'Found' + $color = 'Yellow' + + if ($force) { + $userMsg = 'Declined' + $color = 'Red' + } + + Write-Host "Update Property: $updateProp" + + foreach ($searchString in $searchStrings) + { + $confirm = $false + Write-Host " - Update Search: $searchString" + $searchCount = 0 + foreach ($update in $updates){ + if ($update.$($updateProp) -match "$searchString"){ + if($update.IsApproved){ + + if ($force){ + #$update.Decline() + } + $searchCount = $searchCount + 1 + Write-Host " [*]$($userMsg): $($update.Title), $($update.ProductTitles) ($searchString)" -ForegroundColor $color + } + } + } + + if ($searchCount -gt 0) { + Write-Host "$searchCount `"$searchString`" Updates $userMsg!" -ForegroundColor "Blue" -BackgroundColor White + } else { + Write-Host " n$searchCount `"$searchString`" Updates $userMsg" -ForegroundColor "White" + } + + #Prompt user to confirm declining updates. Do no prompt if force flag is enable to prevent loop + if ((-not $force) -and ($searchCount -ne 0)){ + $confirm = Confirm-Prompt "Are you sure you want to decline all ($searchCount) listed ($searchString) updates?" + + if ($confirm) { + RemoveUpdates @($searchString) $updateProp $true | out-null + } + } + + if (($confirm) -or $force){ + $declinedCount = ($declinedCount + $searchCount) + } + } + + return $declinedCount +} + +function DeepClean ($titles, $productTitles) { + <# + .SYNOPSIS + Checks for unneeded WSUS updates to be deleted. + + .DESCRIPTION + Checks for unneeded WSUS updates by product category to be deleted. This + + .PARAMETER titles + Parameter description + + .PARAMETER productTitles + Parameter description + + .EXAMPLE + An example + + .NOTES + WSUS GetUpdates Method + https://docs.microsoft.com/en-us/previous-versions/windows/desktop/aa350127(v=vs.85) + + WSUS IUpdate Properties + https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms752741(v=vs.85) + + WSUS Product List + Get-WsusProduct + https://docs.microsoft.com/en-us/powershell/module/wsus/get-wsusproduct?view=win10-ps + + WSUS Classification List + Get-WsusClassification + https://docs.microsoft.com/en-us/powershell/module/wsus/get-wsusclassification?view=win10-ps + #> + + $declinedTotal = 0 + + #Remove updates by Title + Write-Host "Searching for unneeded updates by Title. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue + $declinedTotal += RemoveUpdates $titles 'Title' + + #Remove updates by ProductTitles + Write-Host "Searching for unneeded updates by ProductTitle. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue + $declinedTotal += RemoveUpdates $productTitles 'ProductTitles' + + #Remove drivers + Write-Host "Searching for drivers to be removed from WSUS. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue + $declinedTotal += RemoveUpdates @('Drivers') 'UpdateClassificationTitle' + + Write-Host "================DEEPCLEAN COMPLETE==================" -BackgroundColor White -ForegroundColor Blue + Write-Host "$declinedTotal Total Updates Declined" -BackgroundColor White -ForegroundColor Blue +} + +function Disable-Drivers { + <# + .SYNOPSIS + Disable WSUS device driver syncronization and caching. + + .DESCRIPTION + Disable WSUS device driver syncronization and caching. Automatic driver sychronization is one of the primary causes of WSUS slowness, crashing, and wasted storage space. + + .LINK + https://docs.microsoft.com/en-us/powershell/module/updateservices/set-wsusclassification?view=win10-ps + #> + + Get-WsusClassification | Where-Object -FilterScript {$_.Classification.Title -Eq "Drivers"} | Set-WsusClassification -Disable + Get-WsusClassification | Where-Object -FilterScript {$_.Classification.Title -Eq "Driver Sets"} | Set-WsusClassification -Disable +} + + +#-----------------------------------------------------------[Execution]------------------------------------------------------------ + +# Check commandline parameters. This cannot be a switch statement because all of these could be run together +If ($FirstRun) { + Write-Host "All of the following processes are highly recommended!" -ForegroundColor Blue -BackgroundColor White + + if (Confirm-Prompt "Run WSUS IIS configuration optimization?") { + $wsusIISConfig = Get-WsusIISConfig + Test-WsusIISConfig $wsusIISConfig $recommendedIISSettings + } + + if (Confirm-Prompt "Disable device driver synchronization?") { + Disable-Drivers + } + + if (Confirm-Prompt "Run WSUS database optimization?") { + Optimize-WsusDatabase + } + + if (Confirm-Prompt "Run WSUS server optimization?") { + Optimize-WsusUpdates + } + + if (Confirm-Prompt "Create daily WSUS server optimization scheduled task?") { + New-WsusMaintainenceTask('Daily') + } + + if (Confirm-Prompt "Create weekly WSUS database optimization scheduled task?") { + New-WsusMaintainenceTask('Weekly') + } +} else { + if ($DisableDrivers) { + Disable-Drivers + } + + If ($DeepClean) { + DeepClean $unneededUpdatesbyTitle $unneededUpdatesbyProductTitles + } + + If ($InstallDailyTask) { + New-WsusMaintainenceTask('Daily') + } + + If ($InstallWeeklyTask) { + New-WsusMaintainenceTask('Weekly') + } + + If ($CheckConfig) { + $wsusIISConfig = Get-WsusIISConfig + Test-WsusIISConfig $wsusIISConfig $recommendedIISSettings + } + + If ($OptimizeServer) { + Optimize-WsusUpdates + New-Item -Path "c:\Daily.txt" -ItemType File + } + + If ($OptimizeDatabase) { + Optimize-WsusDatabase + New-Item -Path "c:\Weekly.txt" -ItemType File + } +}