12/20/18 Update: Removed a check for WSUS cmdlets that prevented the script from working on 2008 R2 and the ReSyncUpdates from the WSUS standalone config file.
12/10/18 Update: Fixed a configuration file parsing problem and added licensing information for GPLv3.
Note: When updating you will need to update any existing plugins as well.
Despite a seemingly quiet few months I have continued to enhance my script for maintaining software updates in WSUS and ConfigMgr. In fact, I’ve silently released new versions of the script from time to time by simply updating the binary hosted here. However, now there’s enough new stuff worth talking about.
Before we get into it I’d like to thank Chad Simmons for his contributions. Chad submitted several fixes and plugins that I suspect many will enjoy. Also, he fixed my atrocious spelling and has basically shamed me into using Visual Studio Code for any future development.
The complete documentation for the script can and will always be found here: Fully Automate Software Update Maintenance in Configuration Manager
Using WhatIf will force the script to run regardless of the 24-hour timeout.
By default the script will only run once every 24 hours. This is an arbitrary amount of time and eventually I’ll probably make it configurable. However, this timeout serves two purposes. The first purpose is to avoid an infinite loop when you trigger the script from a status filter rule while using the ReSyncUpdates feature. Second, if you’re syncing multiple times a day for Defender updates it would seem overkill to run maintenance every time. When first implementing the script you should be running it a whole bunch of times using WhatIf. Therefore, it makes sense to ignore the timeout when changes are not being made.
WhatIf mode don’t check sync status when declining updates
Similar to the above, when running in WhatIf mode it doesn’t matter if your environment is syncing or not. Since it takes a non-trivial amount of time to check for sync status there’s no point doing so if no changes are going to be made anyways.
Added ExcludeByTitle, ExcludeByProduct, and IncludeByProduct options
This was a request I heard a few times so I’ve added it in. You can use these parameters to either exclude updates from being declined or to limit the scope to a certain product family. Honestly, I don’t think this feature has all that much value. To my thinking it would mean running the script multiple times to decline updates based on multiple different characteristics at the same time. That entire use case is what plugins are for. Speaking of plugins …
Updated Windows 10 Plugins
The ever sharp Justin Chalfant keyed me into the fact that although they don’t show up in ConfigMgr the Win 7 and 8.1 in-place upgrades to Win 10 are synced in WSUS. So I’ve updated the Win 10 plugins to handle those updates as well as created a plugin to remove them entirely. I’ve also updated the plugins to handle the name change from Home/Education/Pro/Enterprise/etc. to just Consumer/Business.
I was super excited to see that the aforementioned Chad Simmons groked how powerful the plugin concept could be and was willing to share his work. There’s zero chance that the main script will support every use case administrators might come up with. I have zero interest in even attempting that goal. However, that’s exactly what the plugins are designed to handle. I really don’t care what convoluted logic you ‘need’ … just put it in a plugin and return a list of update IDs for the main script to decline. If you think others might benefit then by all means reach out and I’ll consider adding it in with the release. Thanks to Chad we now have plugins with more advanced logic for declining Itanium and 32-bit updates. I took the later plugin and released one that excludes Server 2008 (yea … I know). I likewise created a Windows 10 version script that excludes LTSC if you happen to be using that channel.
For Card Holding Members of the ‘Command Line is Too Damn Long’ Party
So … ok … things have gotten a little out of hand in terms of how many features and parameters this script supports. Add into that trying to pass in arrays and hash-tables as parameters and things get awkward real quick. So what I’ve done is created a new feature that will read the parameters out of a configuration INI file. Further, if you provide the script with no parameters at all it will default to use the config.ini file that is in the same folder as the script. An example default config has been provided that represents what I run in my own organization. Modify that example as you see fit and remove the WhatIf parameter from it when you are satisfied with the results. Note that relative paths are now supported to point to the configuration file itself as well as the log and output files.
Let’s Make WSUS Less … Terrible … Again
So this might be news to you but WSUS is … how can we say this … kind of long in the tooth at this point. As in, take it to the vet and do the right thing. Beyond just declining updates there’s two other things you can do.
The first thing is to make the database faster by adding indexes. A while ago Steve Thompson and Benjamin Reynolds went looking at the stored procedure for deleting obsolete updates to figure out why it took so long. The result was a great blog post you can read here: Enhancing WSUS database cleanup performance SQL script. TL;DR: The product team failed to index certain fields they rely heavily on. Fix that problem and suddenly things run hundreds of times faster.
The latest version of the script supports two new parameters: UseCustomIndexes and RemoveCustomIndexes. The first will create the indexes Steve and Ben talk about as well as some others that the community has found to be helpful. This, in theory, should solve the last mile problem for the WSUS Cleanup Wizard and make WSUS run faster in general. Keep in mind that you still need to do DB maintenance on WSUS’s database just like you would any other. I’m told the WSUS product group plans to add the indexes Steve and Ben found in the next release. Until then though, adding custom indexes isn’t exactly supported by Microsoft. For those fearful of living on the edge we call ‘unsupported’ I’ve added the RemoveCustomIndexes parameter to remove them if you so desire. Premier support will never be the wiser.
Less WSUS is Always More WSUS
The second thing you can do is to minimize the amount of updates in the database as a whole. Declining updates removes them from the catalog that WSUS generates and that clients process but it doesn’t actually remove them from the database. I recently ran the script on a client whose WSUS instance goes back nearly a decade. They had something like 40k active updates in the database. While I reduced that number dramatically they were still in the database and it was performing poorly. To remedy this I’ve added a parameter called DeleteDeclined which will actually delete updates from the database using the WSUS API calls. When used, the script will create a local data file in the script’s folder that tracks when an update was declined. It will then delete any update that was declined based on the ExclusionPeriod value. So if you decline updates after 3 months of being superseded they will be removed from the database 3 months after that for a total of 6 months after being superseded. Note that once deleted, it’s not easy to bring that update back. You may be able to manually import it from the Update Catalog or you may need to de-select the corresponding product, sync, select it again, and resync the entire product again.
The next thing I want to dive into is how to orchestrate this kind of maintenance when you have multiple SUP databases and in CAS scenarios. In my research the recommendation seems to be to decline updates top-down but run the WSUS Cleanup Wizard bottom-up. I want to dive into that a bit and see how the script might be able to handle such scenarios and what it takes to configure. It seems like a dark hole of sadness so if you don’t hear from me for a while … pour one out for your fellow comrade. Until then, keep you stick on the ice.