Dam Good Admin

Or at least not entirely useless

Latest Software Maintenance Script: Making WSUS Suck Slightly Less

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.

MOAR PLUGINS!!!!

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.

What’s Next?

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.

29 Comments

  1. Hello Bryan,

    I have enabled switch DeleteDeclined in config.ini.

    This is log from updatemaint.log:

    Deleted update ‘Upgrade to Windows 10 Education, version 1511, 10586 – nl-nl, Volume’ (ID: 8ec3e4af-1b8e-4238-8c66-dde0ebd34527). Source: Declined

    What does it mean the last bit at log entry “Source: Declined”? Does it mean that updated wasn’t deleted?

    Is it normal that when DeleteDeclined is enabled script runs very very slow?

    Thanks,
    Tomas

    • Bryan Dam

      October 31, 2018 at 6:13 pm

      Define slow. The log is pretty verbose; where’s it sucking up time? If you have a lot of declined updates … sure … it’ll take more time to run the script.

      The source field is really just my attempt to indicate why something is deleted or declined and goes into the output file. In theory there might be some other reason to delete an update other than because it was declined.

  2. Hello Bryan,

    $SupportedUpdateLanguages=@(“en”,”all”) – Will decline all languages except en-us and en-gb

    How to decline en-us as well and leave only en-gb?

    I have tried like this $SupportedUpdateLanguages=@(“en-gb”,”all”) but no luck.

    Thanks,

    Tomas

    • Bryan Dam

      October 31, 2018 at 6:10 pm

      So the problem here is that the language configuration for ConfigMgr/WSUS doesn’t quite match the language field on the actual updates. The script looks at the config and does the best it can while erring on the side of safety. If you want to get really specific it would be pretty simply to write a plugin that looks for specific language codes that you define. Just be sure to also keep updates labelled as ‘all’ since that’s the majority.

  3. Bryan, first thank you for the amazing job you’re doing with this script, which I found just recently.
    However, I can confirm that neither of the plugins are working as they should – they always return zero updates to decline to the main script. I tried to look into it and here are my thoughts:

    In each of the plugins you’re referring to a variable $Updates – I searched through the whole source of the main script and the only place where that variable is defined is inside the IF that sets the maximum runtime for updates (line 1900). I suppose that’s not enough, isn’t it? Could this be the reason plugins don’t work?

    • Alright, I found another variable that was defined and looked exactly what was needed – $AllUpdates
      Then I changed $Updates to $AllUpdates in a plugin’s source and it did the trick!

      • Bryan Dam

        October 26, 2018 at 8:55 am

        You are correct, I had some time to look at this yesterday and I once again screwed up a Git upload/merge. I apparently suck at those and plan to migrate to some real tooling before starting any new serious development. I should have another release out today with the correct plugins.

      • There’s also a mistype in all the Windows10 plugins (haven’t checked others): Test-Exclusions is missing a “c”.

        • Bryan Dam

          October 27, 2018 at 10:08 pm

          Just uploaded the correct plugins. Please test them out and let me know. All my environments are already clean so I can’t really test these properly until next month’s updates are synced.

      • Thanks for this – This worked for me too.

        Also, there appears to be a Typo in a few of the plugins – I’m getting errors about ‘Test-Exlusions’ and I’m figuring that should be ‘Test-Exclusions’.

        • Bryan Dam

          October 26, 2018 at 10:16 am

          Yep, as I mentioned above I uploaded the wrong version of the plugins. Hope to have that fixed today yet.

        • Bryan Dam

          October 27, 2018 at 10:08 pm

          Just uploaded the correct plugins. Please test them out and let me know. All my environments are already clean so I can’t really test these properly until next month’s updates are synced.

  4. Hello Bryan, first of all, thank you for your script.

    Same problem here for using plugin, I set up Decline-Windows10Versions.ps1 as required and nothing happened. I use the script (version 2.4) only for WSUS with config.ini

    It’s not an important problem, it was just to warn you.

    config.ini:

    DeclineByPlugins
    DeclineByTitle=@(‘*Preview of*’,'(Preview)*’,’*Itanium*’,’*ia64*’,’*Beta*’,’*Version Next*’,’*ARM64-based*’,’*ARM-based*’)
    ;DeclineLastLevelOnly
    DeclineSuperseded
    DeleteDeclined
    ;ExcludeByProduct=@(‘*name*’,’*name*’)
    ;ExcludeByTitle=@(‘*name*’,’*name*’)
    ExclusionPeriod=1
    ;FirstRun
    Force
    ;IncludeByProduct=@(‘*name*’,’*name*’)
    LogFile=C:\Scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.log
    ;MaxLogSize=2621440
    RunCleanUpWizard
    StandAloneWSUS=****************
    ;StandAloneWSUSPort=8530
    ;StandAloneWSUSSSL=$False
    ;SyncLeadTime=5
    UpdateListOutputFile
    UseCustomIndexes
    ;WhatIfPreference

    All Updates = 27256 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    All Declined Updates = 13852 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    All Updates Except Declined = 13404 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    All Superseded Updates = 681 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Superseded Updates Declined = 859 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *Version Next* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *ARM64-based* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *ARM-based* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *Preview of* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *Itanium* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *ia64* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for *Beta* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title for (Preview)* = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Title (Total) = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Plugin for Decline-Windows10Versions = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Updates Declined by Plugin (Total Unique) = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Total Newly Declined Updates = 498 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Total Newly Deleted Updates = 0 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Total Active Updates = 12906 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)
    Total Updates = 27256 Invoke-DGASoftwareUpdateMaintenance 22/10/2018 15:23:38 4796 (0x12BC)

    my modification in Decline-Windows10Versions.ps1 :

    $UnsupportedVersions = @(“1507″,”1511″,”1607″,”1703”)

    • Bryan Dam

      October 27, 2018 at 10:09 pm

      Just uploaded the correct plugins. Please test them out and let me know. All my environments are already clean so I can’t really test these properly until next month’s updates are synced.

      • So, I downloaded your new files but now I’m stuck with this :

        the script seems to stop using stand alone wsus parameter.

        With the 2.4.0 version, it’s ok but … I manually cleaned my environnment but I will try the plugin as soon as possible and I keep you posted.

        • Invoke-DGASoftwareUpdateMaintenance started (Version 2.4.1). Invoke-DGASoftwareUpdateMaintenance 30/10/2018 10:13:26 3200 (0x0C80)
          Currently, this script must be ran on a primary site server. When the CM 1706 reaches critical mass this requirement might be removed. Invoke-DGASoftwareUpdateMaintenance 30/10/2018 10:13:26 3200 (0x0C80)

          • THe bug I found was in the config file parsing section. \d will find an integer in hostnames that have numbers in them, which I imagine a whole lot of folks do. This causes the config parse to fail for any string with a number in it that’s in the config file.

            change line (around) 860 to } ElseIf ($Data[1] -match “^\d+$”) {

  5. Have you considered porting this over to GitHub? It would provide several advantages, as well as making easier for the community to get involved (if desirable)

    • Bryan Dam

      October 17, 2018 at 10:46 pm

      Oh, it’s on GitHub in my ‘super secret’ public repository. I haven’t really advertised though for reasons that probably aren’t good reasons. Mostly that I don’t want people running versions that are WIP in their prod environments.

  6. Brian is there anything specific you have to do remove the inplace upgrade to Windows 10? I copied the plugin from disabled folder to the plugins root and executed but it does not detect any IPU titles even though they appear in my WSUS.

    • Bryan Dam

      October 17, 2018 at 10:43 pm

      I’m assuming you’re talking about the new Decline-Windows7IPUs plugin? That one should work without any need for modification. In your WSUS console do you see ‘Windows 7 and 8.1 upgrade to Windows 10’ updates?

      • Yes they do. When I ran the script it comes back with 0 items but the count is about 50. I also have the Office 365 plugin running and set to leave only the current release but all the releases are still listed in the console.

        • Bryan Dam

          October 17, 2018 at 10:53 pm

          Just to clarify, when you say the script comes back with 0 items you mean that the log lists the specific plugins you’re talking about in the summary and that they each found 0 updates?

          • Correct. Here is an exert from the log:

            Updates Declined by Plugin for Decline-Windows7IPUs = 0
            Updates Declined by Plugin for Decline-Windows10Languages = 0
            Updates Declined by Plugin for Decline-Windows10Versions = 0
            Updates Declined by Plugin for Decline-Windows10Editions = 0
            Updates Declined by Plugin for Decline-Office365Editions = 0

            When I execute query for the Windows 7 and 8.1 upgrade to Windows 10* updated I have a count of 378. Same goes for Office 365 I have it configured for the following supported editions:

            “Office 365 Client Update – Monthly Channel Version”,
            “Office 365 Client Update – Monthly Channel \(Targeted\) Version”,
            “Office 365 Client Update – Semi-annual Channel Version”,
            “Office 365 Client Update – Semi-annual Channel \(Targeted\) Version”

            I also have $LatestVersionOnly set to $True.

            Yet my console still shows all the deferred channel, first channel releases and multiple versions. I extract some of the code just to get $updates and then executed both the Decline-Office365Editions and the Decline-Windows7IPUs. Both $Office365Updates and $WindowsIPUUpdates returned values. It seems to me the information collected is not being passed back to the main script when it executes the Plugin.

          • Bryan Dam

            October 27, 2018 at 10:09 pm

            Just uploaded the correct plugins. Please test them out and let me know. All my environments are already clean so I can’t really test these properly until next month’s updates are synced.

Leave a Reply

© 2018 Dam Good Admin

Theme by Anders NorenUp ↑