Dam Good Admin

Or at least not entirely useless

Fully Automate Software Update Maintenance in Configuration Manager

Recently I got on a kick to fully automate everything I could as it relates to software updates in Configuration Manager.  While other scripts exist I wanted that one script to rule them all.  I also wanted it to be completely PowerShell, flexible enough so that it could work in nearly every environment, and have decent logging.  I believe I’ve achieved those goals and encourage you to try it out and give me any feedback you may have:

First A Word from My Legal Department

For the love of all you hold dear, test the ever-living-crap out of this script by running it with the -WhatIf switch and pouring through the logs to understand what the script would do.  I also recommend using the UpdateListOutputFile parameter to output a comma-delimited list of updates that will be declined.  Seriously, anything you do to your organization with this script is totally not my fault.  Once an update has been declined in WSUS and synced to Configuration Manager I honestly don’t know how you bring it back.  I’m … sure … there’s a way somehow.

Edited to Add: Jon Warnken over at mrbodean.net answered the question I was too lazy to even ask. To bring back a declined update you can approve it in the WSUS (via the console or script) and then initiate a full sync.  When you sync updates from within the console you only do a delta or incremental sync so that won’t work  To fully synchronize it must be initiated by the Configuration Manager sync schedule or via a script.  John provides the few lines of codes needed to so in this blog post.  If you’re using my Invoke-DGASoftwareUpdatePointSync script to schedule the syncs after Patch Tuesday those will work as well.

What Can it Do?

The script can be used to do any or all of the following:

  • Detect if a synchronization is occurring and wait for success before resuming.
  • Decline superseded updates.
  • Decline updates by a list of titles.
  • Decline updates based on external plugin scripts.
  • Output a comma-delimited list of declined updates.
  • Run the WSUS Cleanup Wizard.
  • Initiate a software update synchronization.
  • Remove expired and declined updates from software update groups.
  • Delete software update groups that have no updates.
  • Combine software update groups into yearly groups.
  • Set the maximum run time for updates by title.
  • Remove unneeded files from the deployment package source folder.
  • Update the deployment packages used by ADRs either monthly or yearly.
  • Directly call the stored procedures to delete obsolete updates.
  • Add crucial indexes that make WSUS run faster overall.
  • Delete updates that have been declined from the WSUS database entirely.

What, Where, When, and How?

While the script’s parameters can be called directly, the download includes the configuration file that I use in my production environments.  The script will default to that configuration file if no other parameters are provided.  The only change required to make actual changes is to remove or comment out the WhatIfPreference parameter.  Before doing that though be sure modify the configuration as desired, run it, and review the logs to confirm that it would perform the actions you desire.  That allows you to call the script very simply:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -File \\#PATH TO SCRIPT FOLDER#\Invoke-DGASoftwareUpdateMaintenance.ps1

Currently this script must be ran on the primary site server as it makes certain queries to WMI classes that only reside there.  If I was a better person or developer I could probably figure it out.  Unfortunately I’m neither of those things.  In theory, when 1706 hits critical mass the newer cmdlets could allow it to be more portable but I don’t really see the value.  If you do for some reason, let me know the use case.  The user running this is going to need a lot of permissions to a bunch of things and the primary site’s system account is perfect on that front.

The script is designed to be ran between the time your Software Update Point(s) sync updates from Microsoft and when your ADRs run.  This allows the plugin scripts to preemptively decline updates that ADR selection can’t properly filter.  Nothing prevents you from running it any time you like of course.  In fact, if you have not been maintaining updates I recommend running this manually outside of your normal patching cycle first.

To make this all happen automatically, I super highly recommend you create a status filter rule that triggers based on the update synchronization ending successfully.  To do so, create a rule that runs your desired command when the SMS_WSUS_SYNC_MANAGER component sends a status with message ID 6702.  Should look a little like this:

Warning: Do NOT use the -Force parameter when creating the status filter rule if you are also using the ReSyncUpdates parameter.  That will create an infinite loop whereby the script is triggered by a synchronization and then initiates another synchronization.

Plugin Scripts You Say?

At some point someone is bound to ask ‘can it decline this’ and the answer will always be yes.  Simply take the template plugin script file I’ve provided and use it to add whatever bat-shit-crazy logic you want in order to add updates to the DeclinedUpdates hash table.  One of the key use-cases for plugin scripts is to to fill gaps in the Automatic Deployment Rule (ADR) filtering capabilities.  There’s a limit to the logic that such filters can support.  The logic you can put in a PowerShell script is only limited by your imagination, ability, and the data at hand.  By declining updates and initiating a sync before the ADRs run you can filter out updates that ADR selection cannot.  Be warned though, it is not a simple matter to bring back declined updates so there is a certain amount of risk in declining updates before you even get to see them in the Configuration Manager console.

I have provided a set of standard plugins that you can make use of.  To do so you must first move them out of the Disabled folder and into the root of the Plugins folder.  Running this set of plugins alone has declined over 900 updates in my environment. Note that some plugins will need to be edited before you can run them.

  • Decline-Office365Editions: Use this plugin script to decline Office 365 editions that you do not deploy.  If you leave the LatestVersionOnly variable at its default value of True the script will determine what the highest version is for each edition and decline older versions.  This is particularly important for editions like the Semi-Annual edition that has multiple supported versions.  You will need to un-comment and fill in the SupportedEditions array.  Any edition not listed will be declined.  This script has a variable called KnownEditions that lists all currently known editions which are matched against each title.  This will hopefully prevent the script from going awry when the Office product team decides to get drunk, bring out a dart-board, and come up with a new naming scheme.  When they do, you will have to update this variable.
  • Decline-Windows10Editions: Use this plugin script to decline Windows 10 feature updates for editions you do not support.  Like its Office 365 brethren it has the same SupportedEditions variable you will need to modify and the KnownEditions variable you may need to manage in the future.  With the release of Windows 10 version 1709 all editions are now packaged in two editions (whose updates were named exactly the same … sigh) and each duplicated for x86 and x64.  So long term I don’t think this plugin will be needed.
  • Decline-Windows10Languages: Use this plugin script to decline Windows 10 updates for languages that are not configured to download software update files in the site’s Software Update Point component.  I don’t know why the Windows 10 feature updates ignore this setting but it does and this solves that problem.  There’s nothing to configure here but if you are using ADRs or manually downloading updates for languages not configured in the component I recommend not using this plugin script.
  • Decline-Windows10Versions: Use this plugin script to decline Windows 10 updates for versions your organization no longer supports or never deployed in the first place.  This script has a variable called UnsupportedVersions that you must un-comment and enter the list of versions (ex. 1511) that you do not support.  Updates matching one of these versions will be declined.

The Results

Here’s the summary of declined updates after running this in my production environment for the first time.  This environment was around two years old at the time and had never been maintained apart from Configuration Manager’s built-in feature to run the WSUS Cleanup Wizard.

Summary:
========
All Updates = 10921
All Updates Except Declined = 9768
All Superseded Updates = 5141
Superseded Updates Declined = 4979
Updates Declined by Title for *Itanium* = 807
Updates Declined by Title for *ia64* = 46
Updates Declined by Title for *Beta* = 7
Updates Declined by Title (Total) = 860
Updates Declined by Plugin for Decline-Windows10Languages = 312
Updates Declined by Plugin for Decline-Windows10Versions = 38
Updates Declined by Plugin for Decline-Windows10Editions = 496
Updates Declined by Plugin for Decline-Office365Editions = 58
Updates Declined by Plugin (Total Unique) = 904
Total Newly Declined Updates = 6435
Total Active Updates = 3333
========

You’re reading that right.  By running this script I have reduced the number of updates every single device in my organization scans against from 9,768 to 3, 333 … a 66% reduction.  That’s the kind of thing that makes a huge difference in terms of WSUS and Windows Update Agent performance.  It’s enough to be the difference between success and failure.

Requirements

I have only tested this script on Current Branch 1702 and above.  Some or all of the script may work on earlier versions but I don’t have the resources or inclination to create that many test environments.

The WSUS console and cmdlets must be installed.

The Configuration Manager console and cmdlets must be installed.

The script must be ran on or from the primary site server.

The user needs to be part of the WSUS Administrators group on SUPs.

The user needs full access to the deployment package source folders.

The user needs the appropriate rights in SCCM to modify the objects your chosen parameters will impact.

So yea … run this as the primary server’s system account.  Use psexec /s /i cmd.exe to do so.

The Parameters

Every parameter listed below is optional but you must select at least one of the action parameters for the script to do anything.  I have provided extensive documentation within the script itself and running Get-Help should really be all you need.  But you’re lazy.  I’m lazy.  We’re all basically lazy.  So here you go:

-CleanSources [switch]

Loop through the software update Deployment Packages and remove any sub-folders whose name does not match a GUID of an update in the package.

-CleanSUGs [switch]

Loop through every Software Update Group and remove any expired or newly declined updates.

-CombineSUGs [int]

Combine Software Updates Groups so that only the provided number of groups exist per Automatic Deployment Rule.  The first group created for a given year will be renamed by having its timestamp truncated to just its year and will have the remaining groups updates added to it.

-DeclineByPlugins [switch]

Loop through every PowerShell script file (.PS1) in the Plugins folder relative to the script and call its Invoke-SelectUpdatesPlugin function.  This allows users to decline updates using whatever logic they desire.  The download includes some examples as well as a template to build from.  In order to use the provided plugin scripts you will need to copy them from the Disabled folder into the root of the Plugins folder.  Some of them need to be edited before they are ran.  See the Plugin Script section above above for more details.

-DeclineByTitle [string[]]

Provide an array of titles that will be used to decline updates in WSUS.  Wildcards are of course supported.  Note that passing in an array via the command line is a bit tricky and you must use the -command option when using this parameter.  See the example above for how to do this.

-DeclineLastLevelOnly [switch]

Only declines updates that do not supersede other updates.  Must be used with the DeclineSuperseded parameter.  Honestly, I have no idea why you want to use this but I put this in for completeness as compared to other scripts.

-DeclineSuperseded [switch]

Declines updates that are superseded in WSUS.  Refer to the DeclineLastLevelOnly and ExclusionPeriod parameters for options regarding which superseded update you decline.

-DeleteDeclined [switch]

When this switch is used the script will track the date that updates are declined in a datafile stored in the script’s directory.  After twice the exclusion period has passed the script will delete the update from WSUS entirely using the WSUS API calls.  This can drastically reduce the amount of data stored in the WSUS database.

-ExcludeByProduct [string[]]

An array of strings that can be used to exclude updates from certain products from being declined regardless of the method used to decline them.

-ExcludeByTitle [string[]]

An array of strings that can be used to exclude updates from being decline regardless of the method used to decline them.

-ExclusionPeriod [int]

An integer that controls how many months to wait before declining a superseded update.  Must be used with the DeclineSuperseded parameter and only impacts updates declined by that parameter.  If this parameter is not defined then the script will use the exclusion period configured in the Software Update component for declining superseded updates.

-FirstRun [switch]

If your environment or a the environment of a close ‘friend’ has never been maintained and the script times out when ran then run the script manually one time using this option.  It will connect directly to the WSUS database (SUSDB) and get a list of obsolete updates using the spGetObsoleteUpdatesToCleanup stored procedure.  It then loops through and deletes each one using the spDeleteUpdate stored procedure.  This mimics what the WSUS cleanup wizard does but with a  24 hours timeout instead of the default 30 second one.  Be aware that this may take hours, days, weeks, or even longer to complete.  However, it most likely will complete unlike the wizard. Afterwards you should be able to run the script normally.

-Force [switch]

The script creates an empty file in the its directory to mark the last time it ran and will not run again if that file is newer than 24 hours unless the Force parameter is used.  Be very very careful with this parameter.  If used in conjunction with the ReSyncUpdates parameter as part of a status filter rule you can create a forever loop.

-IncludeByProduct [string[]]

An array of string that can be used to define which products to decline updates from.  No updates outside of the listed products will be declined.

-LogFile [string]

Specify an alternate log file path.  By default the script will create the log in the same folder as the script.

-MaxLogSize [int]

Specify the maximum size of the log file before it rolls over and rename the current log with the ‘.lo_’ extension.  The default is 2 MB.

-MaxUpdateRuntime [hashtable]

Provide a hash-table where the keys are titles and the values are the number of minutes to set matching updates’ maximum run-time to.  Wildcards are of course supported for the titles.  Note that passing in a hash-table via the command line is a bit tricky and you must use the -command option when using this parameter.  See the example above for how to do this.

-RemoveCustomIndexes [switch]

Remove the custom indexes from the WSUS database if they exist.

-RemoveEmptySUGs [switch]

Remove any Software Update Groups that do not contain any software updates.  Must be used with the CleanSUGs parameter.

-ReSyncUpdates [switch]

Initiate a full Configuration Manager software update synchronization.  This will run after the updates have been declined in WSUS and will expire the declined updates in Configuration Manager.

-RunCleanupWizard [switch]

Runs the WSUS Cleanup Wizard with all the options selected after declining updates.  Even though Configuration Manager includes this feature now I’ve included this for completeness.  To be honest, I’m not certain what Configuration Manager is actually doing on this front.  I’ve had the built-in feature enabled from day one and yet when I ran this manually in my environments the first cleanup run took 1-2 hours and cleaned up thousands of items.  Subsequent runs took minutes.  As an added bonus it will write the results to the log file.

-SiteCode [string]

Provide the site code of the site you wish to connect to.  By default the script will try to identify the site by the PS-Drives available or from the registry.  In environments with a CAS this parameter must be provided.

-StandAloneWSUS [string]

If you wish to run the script against a stand-alone WSUS server then specify the FQDN of the WSUS server using this parameter.  When doing so the script will prevent you from also including parameters that only apply to a Configuration Manager environment.

-StandAloneWSUSSSL [switch]

If you specified the StandAloneWSUS parameter you may optionally specify whether to connect via SSL or not.  If this parameter is not used the script will try to connect without using SSL.

-StandAloneWSUSPort [int]

If you specified the StandAloneWSUS parameter you may optionally specify the port to connect upon.  If not specified the port will default to 8531 if the StandAloneWSUSSSL switch is used and 8530 if it is not.

-SyncLeadTime [int]

Specifies the number of minutes to wait after a software update synchronization has occurred before continuing script execution.  The default is 5 minutes.

-UpdateADRDeploymentPackages [string (‘Yearly’,’Monthly’)]

Update Automatic Deployment Rules to use the appropriate software update Deployment Package and create a new package when necessary.  Valid values are ‘Yearly’ or ‘Monthly’.  When used, the name of the deployment package and the source folder for the package must end with four digits that represent the desired period.  For yearly packages a four-digit year (ex. 2017) must be used and for monthly packages use YYMM (ex. 1710 for October 2017).

-UpdateListOutputFile [string]

Specify a file path where the script will output a comma delimited text file listing the details of every update it declines and the reason why it was declined.

-UseCustomIndexes [switch]

Check for and if necessary create additional indexes that help run WSUS perform better.  This feature is highly recommended and can be crucial to successfully running the FirstRun feature or the WSUS Cleanup Wizard in general.  Note, these indexes are not supported by Microsoft.

-WhatIf [switch]

Last, but not least, use this to test what the script will do without making any changes.  Refer to the log or use the UpdateListOutputFile  parameter to output a list of declined updates.

 

62 Comments

  1. H Bryani. Fantastic script, however I have a question about your ADR updating. Scripts look for both Package name and package path to end in 4 digits to indicate either year or month (so I’d assume you’d use 00 for date part you don’t want to use?) However do you have a pointer to any previous posts that explains your ADR & Distribution Package naming / updating methods / rationale? Had a quick dig but haven’t come across anything that explains what you’re achieving and why.

    Thanks again for a fantastic blog. I’m relatively new (just over a year) to the SCCM world but enjoying the journey so far 🙂

    Richard

    • Richard, take a look at the documentation for the UpdateADRDeploymentPackages above. It tries to explain what the requirements are. I’m not sure that I’ve explained why you’d want to do either of those though in depth. Sounds like a short blog post.

  2. Hello Bryan,

    I get confused now how to use this script with config.ini file.

    Before I ran Task like this:

    -NoLogo -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\DeclinedUpdates.csv -DeclineSuperseded -DeclineByTitle @(‘*Itanium*’,’*ia64*’,’*Beta*’) -DeclineByPlugins -CleanSUGs -RemoveEmptySUGs -RunCleanUpWizard -Force

    but now config.ini contains all switches inside. So how should run now? Run like this: -NoLogo -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1 ?

    Thanks,
    Tomas

  3. Michael Ziegler

    October 31, 2018 at 9:22 am

    Hi Bryan, this looks great and I would love to use it. But I am getting an error when I run it. I have SCCM on one server and WSUS on another.

    Invoke-DGASoftwareUpdateMaintenance started (Version 2.4).]LOG]!>
    The script was run in the last 24 hours but is being forced to run.]LOG]!>
    Connecting to site: B01]LOG]!>
    The active software update point is WUSP07B1.xxxxxx.xxxxx.]LOG]!>
    Trying to connect to WUSP07B1.xxxxxx.xxxxx on Port 8531 using SSL.]LOG]!>
    Connected to WSUS server WUSP07B1.xxxxxx.xxxxx.]LOG]!>
    Failed to connect to the (SUSDB) database on MICROSOFT##WID.]LOG]!>
    Error (-2146233087): Exception calling “ConnectToDatabase” with “0” argument(s): “A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 – Could not open a connection to SQL Server)”]LOG]!>
    At D:\Scripts\Invoke-DGASoftwareUpdateMaintenance.ps1:688 char:9
    + $WSUSServerDB.ConnectToDatabase()
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~]LOG]!>
    Failed to get the WSUS database configuration.]LOG]!>

    • Bryan Dam

      October 31, 2018 at 9:50 am

      First, my condolences on using WID. AFAIK, you can’t connect to WID remotely so you will need to run the script on the WSUS/SUP server itself in WSUS Standalone mode. You should only need to do this when using the FirstRun and Add/RemoveCustomIndex features that connect to the DB directly. Those can be ran a single time and then automate the rest.

      • Hmmm… While I know this is true, I have WID for my WSUS and the script run without any problem. SCCM and WSUS on different server and it’s working.
        .\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile ‘path\UpdateListOutputFile.csv’ -RunCleanUpWizard -DeclineSuperseded -DeclineByPlugins -ReSyncUpdates -CleanSUGs -RemoveEmptySUGs -MaxUpdateRuntime @{‘*Security Monthly Quality Rollup For Windows*’=60;’*Security and Quality Rollup for .NET*’=60} -CleanSources

        That’s what I use

        • Bryan Dam

          October 31, 2018 at 10:29 am

          Correct, because you’re not using any of the features that need to directly connect to the WSUS DB: FirstRun and Add/RemoveCustomIndex

  4. I am trying to use:
    . .\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile .\DeclineSecurityOnlyUpdates.csv -StandaloneWSUS MyWSUS -DeclineByTitle @(‘*Security Only*’)
    I am getting some updates that have not been superseded:
    Update Id Revision Number Title KB Articles Security Bulletin Has Superseded Updates Creation Date Declined Reason
    937168f0-026b-4480-bcff-77be5cb1576c 202 April, 2017 Security Only Update for .NET Framework 3.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2 on Windows 7 (KB4014985) 4014985 FALSE 4/11/2017 17:00 ByTitle: *Security Only*
    Is there a way to use the above syntax and exclude updates that have not been superseded?

    • Decline By Title mean decline anything that contain x. If I understand, you want to decline only those superseeded that contain a specific title? This is already done since all superseeded updates are declined based on your settings (unless you removed this setting). Else, you’ll have to create your own plugin. Start with the decline by title plugin and in it, on all match, add a check to verify if it’s superseeded or not.

      • as mentioned; exclude updates that have not been superseded because I am currently using -DeclineByTitle @(‘*Security Only*’). I am getting results that show updates ex:(KB4014985 or 4054521) that have not been superseded, so I don’t want to decline these…
        https://i.imgur.com/EqcWg3V.png
        or am I missing the boat? 🙂

        • Like I said, the plugin “Decline by title” decline by title only. It means it decline anything that match the title. If you want to decline all superseed update regardless of title, that’s what this does without any plugin. So what are you trying to achieve with the declinebytitle? Declinebytitle is to add other things that aren’t decline. If you run it without any plugins, per default, is the result missing something?

          • Bryan Dam

            October 30, 2018 at 9:40 am

            What Jean-Sebastien said is correct. The various Decline parameters work independently of one another. The DeclineSuperseded will decline all superseded updates regardless of title. The DeclineByTitle will decline all updates that match those titles regardless of their supersedence status. If you want to only decline superseded updates with specific titles you will need to create your own plugin to do so.

          • “If you want to decline all superseed update regardless of title”
            No
            I want to decline just what I have set in the -DeclineByTitle @(‘*Security Only*’) except the KB’s that have not been superseded.
            Something like this:
            -DeclineByTitle @(‘*Security Only*’) -Superseded:$True
            This way it declines only the (‘*Security Only*’) updates that have been superseded…
            How do I exclude the FALSE and not the TRUE while keying in on the (‘*Security Only*’) updates by title?:
            https://i.imgur.com/e7BuLLO.png

            Thank you for your help and sorry for the confusion 🙁

          • Bryan Dam

            October 30, 2018 at 9:50 am

            If you want to only decline superseded updates with specific titles you will need to create your own plugin to do so.

        • Thanks Bryan and Jean, and thank you Bryan for this wonderful script!
          LOL BTW “Du.Du hast.Du hast mich” this is great!

          • Bryan Dam

            October 30, 2018 at 10:16 am

            I love it when people actually read the code so I like to reward them with a bit of humor. Glad you found it.

        • You can use a template or, like me, instead of using the declinebytitle, I created a pluging for everything I want. Your plugin would look like this. Bear in mind, I haven’t tried it with the superseed option in the $UpdateList =. Also, this script won’t check if the update has superseded updates that are declined or not.

          $UnwantedUpdates = @(“Beta”,”Preview”,”Security Only”,”Internet Explorer 8″,”Internet Explorer 9″,”Internet Explorer 10″,”Itanium”,”ia64″,”ARM64″)

          Function Invoke-SelectUpdatesPlugin{

          $DeclinedUpdates = @{}

          ForEach ($Product in $UnwantedUpdates) {
          $UpdateList = ($Updates | Where{$_.Title -match $Product -and !$_.IsDeclined -and $_.IsSuperseded})
          ForEach ($Update in $UpdateList) {
          $DeclinedUpdates.Set_Item($Update.Id.UpdateId, “Unwanted Update: $($Product)”)
          }
          }

          Return $DeclinedUpdates
          }

          • Since I can’t edit. The script is missing a } at the end [Bryan: Luckily I can and did. Thanks for posting.]

  5. I may have missed it, but what is up with this log format? What tool will open these in a readable format? (I mean beyond just a text editor)

  6. Hi Brayan, For some reason my plugins are not working. They are detected in logs while execution but they are not expiring/declining any updates. Can you please help to fix it ?

    From the Log File:

    Enumerating and calling plugins. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows10Languages plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows10Versions plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows7IPUs plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsItanium plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsX86 plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsX86_Minus2008 plugin. Invoke-DGASoftwareUpdateMaintenance
    0 updates were selected to be declined. Invoke-DGASoftwareUpdateMaintenance

    Plugin Info: Kept most of the plugins as it is and just changed Decline-Windows10Versions plugin and added supported versions.

    1. Decline-Windows10Versions plugin –> Un-commented this line and kept 3 versions –> $UnsupportedVersions = @(“1507″,”1511”, “1607”)

    • 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.

  7. Hi, I get this error:

    In C:\Users\Administrator\Desktop\Invoke-DGASoftwareUpdateMaintenance\Invoke
    -DGASoftwareUpdateMaintenance.ps1:187 car:37
    + [ValidateRange(1,[int]::MaxValue <<<< )]
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordEx
    ception
    + FullyQualifiedErrorId : ParameterAttributeArgumentNeedsToBeConstantOrScr
    iptBlock

    I'm on Windows 2008R2 standard. What can I do?
    Thanks.

    • Bryan Dam

      October 17, 2018 at 8:07 am

      First, try the latest version I just uploaded. Second, update PoSH. My guess is you’re still running v2 which _could_ work but I’m not testing for.

      • I updated powershell to 5.1, PoshWSUS to 2.3.1.6 and downloaded the latest version of your script. But I still have this in updatemaint:

        • Sorry, there was no error log in the post above. The error reported in updatemaint is this:

          [LOG[The Update Services module was not found. Please make sure that WSUS Admin Console is installed on this machine]LOG]!><time="09:31:05.356+120" date="10-19-2018" component="Invoke-DGASoftwareUpdateMaintenance" context="NT AUTHORITY\SYSTEM" type="3" thread="7996" file=""

          • Bryan Dam

            October 19, 2018 at 8:26 am

            That one’s kind of self-explanatory: Please make sure that WSUS Admin Console is installed on this machine. Is it?

  8. Set this script running on Thursday and it seemed to be making pretty good progress, but has been sat on the WSUS cleanup for over 24 hours… i know your guide says this can take some time to complete, but wondering if there is a way to check progress, or even check that is is actually doing something 🙂

    • Bryan Dam

      October 13, 2018 at 6:47 pm

      Aaron, I forget if the API for the cleanup wizard has events or not but I do know that the script doesn’t try and read them. It’s food for thought though. That being said, when implementing the script I’d suggest manually running it with the FirstRun parameter by itself. That will call the stored procedures that the cleanup wizard calls but with a much larger time-out (30 minutes) in an attempt to get around the wizard’s tendency to crash and burn when it hasn’t ran in a long time or … ever. That function has a progress bar so you can watch with joyful anticipation. And then get all sad when it tells you it’s going to take days to finish … but hopefully it WILL finish which is the whole point.

      • Bryan, the command i initiated was as follows.

        C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -Command “& ‘C:\Temp\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1’ -FirstRun -UpdateListOutputFile ‘c:\temp\UpdateListOutputFile.csv’ -RunCleanUpWizard -DeclineSuperseded -ReSyncUpdates -CleanSUGs -RemoveEmptySUGs -MaxUpdateRuntime @{‘*Security Monthly Quality Rollup For Windows*’=60;’*Security and Quality Rollup for .NET*’=60} -CleanSources

        So i already have the firstrun parameter listed, i did have a progress bar during the first stage of the script when it went through declining a tonne of updates, but its moved part that part and is now reporting this in the log;

        ======== Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Updates = 32796 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Updates Except Declined = 32498 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Superseded Updates = 20495 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Superseded Updates Declined = 20264 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Total Newly Declined Updates = 20264 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Total Active Updates = 12234 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        ======== Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Software update point synchronization states confirmed. Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Starting the WSUS cleanup wizard. Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)

        and has been at that point since 12/10/2018 08:43:29, running a profiler trace on the SQL box, the SUSDB database is doing stuff, but ill be the first to admit my SQL knowledge isnt brilliant 🙁

        Should i just let it carry on in this state and see if it finishes, or would you suggest terminating it and try running it as just C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -Command “& ‘C:\Temp\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1’ -FirstRun

        • Random add-on question… if we wanted to CTRL+C this script while its running the cleanup wizard, would that be advisable ?

          • Bryan Dam

            October 30, 2018 at 10:15 am

            Yea, that shouldn’t be too terrible. It might not actually stop the clean-up wizard mind you but I don’t think it should do anything catastrophic. Not that I’ve tested that mind you.

  9. Hello,

    I’m having a bit of issue right now. What I see in SCCM doesn’t match WSUS and doesn’t match powershell. In Powershell, the command GetUpdates return me about 14 000 objects. Once I filter the declined, I have 9304 updates. In WSUS Console, I have 9431 updates. In SCCM, I have 6134 (which includes third party updates from Adobe and HP). Why I don’t have the same number everywhere (and worst, why WSUS see more then SCCM, and powershell cmdlet even more!).

    I’m trying to clean my WSUS, but this “bug” is giving me a headache. Shouldn’t I see the same number of updates everywhere? WSUS list me 7435 out of 9431 updates as declined. I only deploy 612 updates and WSUS have only 2 updates approved. There’s something I don’t get. I’m trying to get the wsus database to “match” what I’m deploying, all the rest can be declined since, well, I don’t deploy them.

    • Bryan Dam

      October 12, 2018 at 3:11 pm

      >Shouldn’t I see the same number of updates everywhere?
      Simply put, no. What you describe makes total sense to me. The WSUS console will not show obsolete or 3rd party updates. So it’s pretty much guaranteed to show fewer updates than the PoSH call which is unfiltered. In turn, ConfigMgr doesn’t necessarily sync all the updates from WSUS. The bigger problem is that until 1806, ConfigMgr would expire superseded updates in ConfigMgr but not do anything in WSUS. Eventually those get removed from ConfigMgr but remain in WSUS … which was the initial reason a script like this needed to exist in the first place.

      • Ah, that make sens. I tried to find a way using get-cmsoftwareupdate to match all deployed update with those on the wsus server and decline all other update, but I failed at finding a matching item. I tried using update.id but they all returned the same ID from get-cmsoftwareupdate when I looped on it. Any idead how this could be done? I was planning on doing a last past with all update from 30+ not deployed to be declined, I had the query, just missing what I could use to fetch them through wsus.

  10. Nicklas Eriksson

    October 1, 2018 at 3:39 am

    Hello Bryan Dam,

    Thank you for this awesome script.

    We have encountered a problem when running this in our enviroment:
    Connecting to site: XXX Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:58
    The active software update point is XXXX.xxx.xxxx Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:59
    Trying to connect to XXXX.xxxx.xxxx on Port 8530. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:59
    Connected to WSUS server xxxx.xxxx.xxx Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01 24240 (0x5EB0)
    User selected FirstRun. Will try to delete obsolete updates by directly calling the database store procedures. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01
    Successfully tested the connection to to the (SUSDB) database on MICROSOFT##WID. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01
    Successfully connected to to the database. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01 24240
    Failed to call the WSUS spGetObsoleteUpdatesToCleanup stored procedure. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01
    Error: Exception calling “Fill” with “1” argument(s): “Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.” Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01
    At C:\Users\xxxx\Desktop\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:866 char:6
    + [void]$SqlDataAdapter.Fill($ObsoleteUpdates)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01

    Have you seen this problem anywhere when the inital cleanup command line.
    Powershell -File .\Invoke-DGASoftwareUpdateMaintenance.ps1 -FirstRun -Force

    Kind regards,
    Nicklas

    • Bryan Dam

      October 10, 2018 at 10:48 am

      Nicklas, you’re the second person to come across that. Just yesterday I was working with a friend who had the same problem. In the Invoke-SQLCMD function of the script change the
      $SqlCmd.CommandTimeout value (in seconds) to something higher. I plan on setting it to 24 hours (86400 seconds) in the next release and do some better error handling on those calls. Give that a shot and let me know.

  11. What the actual f—– ? Only 16 Comments.. SO Just posting.. O—M—G -This is the one. Script to run! 🙂 So many options.. So good… Shiiiinyyyyy…… Precious……………..

  12. Hi there
    Thanks for sharing this great script. There is a small error in two lines where the percentcomplete is calculated. If you remove the “* 100” it works great. Otherwise you get errors about the percent complete going over 100 in the write-progress function.

    • Bryan Dam

      October 30, 2018 at 10:17 am

      Late to the game here Peter but this should be resolved at this point. I found out that the IDE doesn’t show you the progress bars for some reason and thus didn’t notice.

  13. Hi Bryan,
    great post and script, even for a PS Newbie like me.
    I checked out the script and ran with the -whatif switch to see the number of obsolete updates that would be removed – returning 2667 updates.
    Then all it says in the log file is Attempting to delete update 96183 (1/2667 but how can I match that update inside SCCM to see what update it is and if it is really obsolote. I do not find any reference between number 96183 and a specific update.

    Thanks for all your hard work and making life less complicated for other people.

    • Sorry, I really gotta fix the comment notification. It’s on my to-do list post-MMS but I couldn’t find a quick way to relate the ID used by the stored procedures to an actual update.

  14. Hi Bryan,

    I keep getting the error “The software update point [redacted] failed its last synchronization with error code 2148734217. Synchronize successfully before running Invoke-DGASoftwareUpdateMaintenance.”

    But I don’t see my synchronizations failing. Have you seen this error come up before?

  15. Hello 🙂 Thank you up front for your work and your insights! It’s helping me understand some things better, and re-affirming that the world still ‘makes sense’ (regarding IT that is).
    One question though; How long should this run? I’ve started the script, and the logs have enumerated superseeded stuff etc, but stops at “Starting Cleanup Script”….
    The PS1 is still running though; Can that be the case? Or is there something broken 🙂

    Thank you in advance!

  16. Hi Bryan,
    We’re getting the following error:
    Failed to connect to the active software update point on port
    Error: Method invocation failed because [Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlArrayItems] doesn’t contain a method named ‘Where’.
    At C:\Scripts\Rasmus\Invoke-DGASoftwareUpdateMaintenance.ps1:576 char:5
    + $ActiveSoftwareUpdatePointName = ((Get-CMSoftwareUpdatePointComponent).Props …

    Any ideas of what the issue is?

  17. Well done! Thank you for this community contribution, Bryan.

  18. DGA, you are my hero. This is extremely helpful.

    I have a question about the relationship between ADRs and Deployment Packages. Let’s say I have a package for Patch Tuesday for Windows 10 clients with a handful of products. If I’m aggressively expiring, declining and superseding AND using the CleanSources parameter, I don’t know if I see a reason to ever break apart the Deployment Packages into monthly packages. I’m new to all this so I’m expecting that I’ve overlooked something. Is this something you’d do for performance reasons or for operational sanity?

    • Bryan Dam

      December 7, 2017 at 10:57 am

      The key is to remember that the purpose of Deployment Packages is only … only … for content distribution of the update binaries or installers. They are totally unrelated to Software Update Groups or deployments. As such, there’s no technical reason to have more than one deployment package … technically one package can and will hold every update binary ever released. Practically speaking though, your DPs need to handle the validation of that package and packages can and therefore will go sideways on you forcing you to recreate them. The chance of a deployment package corrupting itself and the impact of recreating it is tied directly to the number of updates or the size of the deployment package. So most organizations, mine included, usually create a new deployment package yearly. Doing so manually isn’t hard and on a yearly basis it’s almost hardly worth automating but if you don’t then it’s a small manual task that will be forgotten. One downside to using the same deployment package each month is that you can’t stagger how that data is distributed. The source is essentially updated and all the DPs are going to try to get it immediately. If your organization needs to stagger the content distribution you might consider creating monthly deployment packages and add DPs or DP Groups over a period of time either manually or some other automation. To be clear, that’s a pretty edge use case but I figured that those using monthly SUGs are the ones who could most benefit from the UpdateADRDeploymentPackages option.

  19. Hey, It was nice seeing you at the NSCUG as a presenter. This script has work wonders. Tested this in my homelab and in production. Everything from the customized plugins to dumping the logs into something readable. You have seriously saved a lot of professionals some serious time with this and couldn’t thank you enough.

    • Thanks for the kinds word Brandon, I had a lot of fun presenting. I’m also glad to hear you are getting some use out of the script.

  20. I am getting an error of:
    Key cannot be null.
    Parameter name: key
    At C:\Scripts\Invoke-DGASoftwareUpdateMaintenance.ps1:1120 char:127
    + … .UpdateID)). Source: $($DeclinedUpdates.(Update.Id.UpdateID Error: …

    • Bryan Dam

      November 30, 2017 at 4:46 pm

      That was an failure in the error logging for the CleanSources routine. I’ve uploaded a new version which should resolve the error logging so try it again. My expectation is though that you’ll now get the actual error going on so let me know what it is. Honestly, in all my testing I never saw that section of code do much of anything. It’s only there for completeness … Configuration Manager really should be doing a good job of cleaning those out already.

Leave a Reply

© 2018 Dam Good Admin

Theme by Anders NorenUp ↑