Script to Repair Distribution Point Content Library Inconsistencies

An improved maintenance script that builds on ideas from the community.

The Problem

If you're a young SCCM administrator, you may have noticed that many of your distribution points have a warning status in the Distribution Point Configuration Status pane of the ConfigMgr console.  Like this:

Warning statuses

And when you comb through the detail records, you find a warning message like this:

Detail warning message

And if you're like me, you probably did some testing and discovered that your DPs were still working just fine.  You might have even looked for a way to manually reset the warning flag and discovered that there's no obvious way to do that.

Since I usually have 100 other things on my to-do list, I ignored these warning statuses for a long time.  But I recently set out to better understand why this was happening and fix it, if possible.  I quickly found some helpful resources.  In particular, this post from user CSE505 in the Technet forums explains the nuts and bolts.  I'll recap his/her findings here for your convenience.

What's Going On?

There are essentially 2 different content package lists on a distribution point (DP), and they need to match each other.  One is in WMI, and the other is the list of file objects in the PkgLib folder of the DP's content library.  Also, any packages listed on the distribution point need to also be listed in the master content package list on your primary site server.  

If the two lists on the DP don't match, you'll get a warning status. Or, if a package ID exists on your DP, but no longer exists in the master list on your primary site server (probably because you deleted the application/package that created it), then you've got some orphaned package IDs on the DP that need to be cleaned up.  That will also cause a warning status.

Working To Resolve

The comments on that technet forum led me to this script from Bart Serneels.  Bart's script automates the process of comparing WMI on the distribution point to the master list on the primary site server.  So, I started by giving that a try, and it did exactly what it was designed to do and purged a bunch of orphaned records from WMI on all my DPs.

So, I triggered content validation on all my DPs and let it run through the night.  The next morning, I came back expecting to see all green checkmarks, but nope.  Still warnings.  As it turned out, and as explained by CSE505 (I really wish I knew his/her name so I could give proper credit), I still needed to reconcile the PkgLib folder with WMI on each distribution point.  At that point, here's what I was seeing in smsdpmon.log:

The package data in WMI is not consistent to PkgLib

Bart's script might be enough to repair your distribution point in certain cases, but in my case, I still had a lot of work to do.  

My Contribution

I set out to build on Bart's script and expand it to automate the full maintenance operation.  I wanted to add 3 new features:

  • Automate comparing PkgLib to WMI and removing orphaned records from PkgLib,
  • Detect package IDs that are present in WMI but missing from PkgLib and prompt the script user to manually redistribute that content to the DP, and
  • Add a mechanism to optionally prompt the script user for confirmation before deleting records, thereby making this script a little safer to run in a production environment.

After a couple hours of scripting and testing, I managed to accomplish all 3.  I then ran it against all my DPs, triggered one more round of content validation, and jackpot.

The Script

My complete script is below.  You'll need to edit the first 2 variables to match your SCCM environment.  Also, by default I have set the warning threshold to 0, which means it will always pause and ask for confirmation before any deletions.  If you want things to move along more quickly, you can increase that threshold and you will only be prompted for confirmation if the number of deletions exceeds your input value.  If you want to see how many orphaned packages you have in your production environment without deleting anything,  just leave the threshold at 0 and respond 'No' each time you're prompted.

# UPDATE THESE VARIABLES FOR YOUR ENVIRONMENT
[string]$SiteServer = "server.domain.com"
[string]$SiteCode = "ABC"
[int32]$WarnThreshold = 0

# function for pausing the script and prompting the user for confirmation
Function Pause-Script {
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory=$false)]
        [ValidateRange(0,5)]
        [int]$Buttons = 0,

        [Parameter(Mandatory=$false)]
        [switch]$Warning
    )

    # Check for Powershell ISE
    if ($psISE) {
        Add-Type -AssemblyName System.Windows.Forms
        $x = [System.Windows.Forms.MessageBox]::Show("$message","Script Execution Paused",$Buttons)
    } else {
        #translate buttons
        switch ($Buttons) {
            0 { $options = "Type OK to continue" }
            1 { $options = "OK or Cancel" }
            2 { $options = "Abort, Retry, or Ignore" }
            3 { $options = "Yes, No, or Cancel" }
            4 { $options = "Yes or No" }
            5 { $options = "Retry or Cancel" }
        }

        if ($Warning) {
            $message = "Warning: $message"
            $color = "Red"
        } else {
            $color = "Yellow"
        }

        $invalid = $true
        while ($invalid) {
            Write-Host "$message" -ForegroundColor $color
            $x = Read-Host -Prompt "$options"

            switch ($Buttons) {
                0 {
                    if ($x -iin @("OK")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
                1 {
                    if ($x -iin @("OK","Cancel")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
                2 {
                    if ($x -iin @("Abort","Retry","Ignore")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
                3 {
                    if ($x -iin @("Yes","No","Cancel")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
                4 {
                    if ($x -iin @("Yes","No")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
                5 {
                    if ($x -iin @("Retry","Cancel")) {
                        $invalid = $false
                    } else {
                        Write-Warning "Invalid input."
                    }
                }
            }
        }
    }

    return $x
}


# Get all valid packages from the primary site server
$Namespace = "root\SMS\Site_" + $SiteCode
Write-Host "Getting all valid packages... " -NoNewline
$ValidPackages = Get-WMIObject -ComputerName $SiteServer -Namespace $Namespace -Query "Select * from SMS_ObjectContentExtraInfo"
Write-Host ([string]($ValidPackages.count) + " packages found.")

# Get all distribution points
Write-Host "Getting all valid distribution points... " -NoNewline
$DistributionPoints = Get-WMIObject -ComputerName $SiteServer -Namespace $Namespace -Query "select * from SMS_DistributionPointInfo where ResourceType = 'Windows NT Server'"
Write-Host ([string]($DistributionPoints.count) + " distribution points found.")
Write-Host ""

# iterate through all DPs
foreach ($DistributionPoint in $DistributionPoints) {

    # first, clean up orphaned records in WMI by comparing to master content lib
    $InvalidPackages = @()
    $DistributionPointName = $DistributionPoint.ServerName
    if ( -not(Test-Connection $DistributionPointName -Quiet -Count 1)) {
        Write-error "Could not connect to DistributionPoint $DistributionPointName - Skipping this server..."
    } else {
        Write-Host "$DistributionPointName is online." -ForegroundColor Green
        Write-Host "Getting packages from WMI on $DistributionPointName ... " -NoNewline
        $CurrentPackageList = @(Get-WMIObject -ComputerName $DistributionPointName -Namespace "root\sccmdp" -Query "Select * from SMS_PackagesInContLib")
        Write-Host ([string]($CurrentPackageList.Count) + " packages found.")

        if (($CurrentPackageList.Count -eq 0) -or ($CurrentPackageList -eq $null)){
            Write-Host "Skipping this distribution point"
        } else{
            Write-Host "Validating WMI packages on $DistributionPointName ..."

            $result = @(Compare-Object -ReferenceObject $CurrentPackageList -DifferenceObject $ValidPackages -Property PackageID -PassThru)
            $InvalidPackages = @($result |Where-Object {$_.sideindicator -eq '<='})

            if ($InvalidPackages.Count -eq 0){
                Write-Host "All WMI packages on $DistributionPointName are valid" -ForegroundColor Green
            } else {
                $response = $null
                if ($InvalidPackages.Count -gt $WarnThreshold) {
                    Write-Host "Number of invalid packages exceeds threshold [$($InvalidPackages.Count) invalid]." -ForegroundColor Yellow
                    $InvalidPackages.PackageID
                    $response = Pause-Script -Message "Proceed with removing invalid WMI packages on $($Env:COMPUTERNAME)?" -Warning -Buttons 4
                } else {
                    Write-Host "Invalid WMI packages on $DistributionPointName :" -ForegroundColor Yellow
                    $InvalidPackages.PackageID
                }

                if ($response -ieq "No") {
                    Write-Host "Skipping WMI package maintenance on $DistributionPointName." -ForegroundColor Yellow
                } else {
                    $InvalidPackages | foreach {
                        $InvalidPackageID = $_.PackageID
                        Write-Host "Removing invalid package $InvalidPackageID from WMI on $DistributionPointName " -NoNewline
                        Get-WMIObject -ComputerName $DistributionPointName -Namespace "root\sccmdp" -Query ("Select * from SMS_PackagesInContLib where PackageID = '" + ([string]($_.PackageID)) + "'") | Remove-WmiObject
                        Write-Host "-Done"
                    }
                }
            }
            Write-Host ""
        }
    }

    # next, clean up orphaned records from PkgLib by comparing to cleaned WMI
    Invoke-Command -ComputerName $DistributionPointName -ScriptBlock {
        # function for pausing the script and prompting the user for confirmation
		Function Pause-Script {
    		[CmdletBinding()]
    		Param(
        		[Parameter(Mandatory=$true)]
        		[ValidateNotNullOrEmpty()]
        		[string]$Message,

        		[Parameter(Mandatory=$false)]
        		[ValidateRange(0,5)]
        		[int]$Buttons = 0,

        		[Parameter(Mandatory=$false)]
        		[switch]$Warning
    		)

    		# Check for Powershell ISE
    		if ($psISE) {
        		Add-Type -AssemblyName System.Windows.Forms
        		$x = [System.Windows.Forms.MessageBox]::Show("$message","Script Execution Paused",$Buttons)
		    } else {
        		#translate buttons
        		switch ($Buttons) {
            		0 { $options = "Type OK to continue" }
            		1 { $options = "OK or Cancel" }
            		2 { $options = "Abort, Retry, or Ignore" }
            		3 { $options = "Yes, No, or Cancel" }
            		4 { $options = "Yes or No" }
            		5 { $options = "Retry or Cancel" }
        		}

        		if ($Warning) {
            		$message = "Warning: $message"
            		$color = "Red"
        		} else {
            		$color = "Yellow"
        		}

        		$invalid = $true
        		while ($invalid) {
            		Write-Host "$message" -ForegroundColor $color
            		$x = Read-Host -Prompt "$options"

            		switch ($Buttons) {
                		0 {
                    		if ($x -iin @("OK")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
                		1 {
                    		if ($x -iin @("OK","Cancel")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
                		2 {
                    		if ($x -iin @("Abort","Retry","Ignore")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
                		3 {
                    		if ($x -iin @("Yes","No","Cancel")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
                		4 {
                    		if ($x -iin @("Yes","No")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
                		5 {
                    		if ($x -iin @("Retry","Cancel")) {
                        		$invalid = $false
                    		} else {
                        		Write-Warning "Invalid input."
                    		}
                		}
            		}
        		}
    		}

    		return $x
		}
            
        # get list of storage drives in use for current server
        $dataDrives = @(Get-PSDrive -PSProvider FileSystem | ?{$_.Used -gt 0})

        # find SCCMContentLib on server
        $selectedDrive = $null
        foreach ($drive in $dataDrives) {
            if (Test-Path "$($drive.Name):\SCCMContentLib") {
                $selectedDrive = $drive.Name
            }
        }

        # get package list from PkgLib
        if ($null -eq $selectedDrive) {
            Write-Warning "Failed to locate SCCMContentLib on $Env:COMPUTERNAME"
            return
        } else {
            Write-Host "Getting package list from PkgLib on $Env:COMPUTERNAME ... " -NoNewline
            $path = "$($selectedDrive):\SCCMContentLib\PkgLib"
            $pkgs = @()
            Get-ChildItem -Path $path | Select-Object Name,Basename | %{
                $pkgs = $pkgs + [PSCustomObject]@{
                    FileName = $_.Name
                    PackageID = $_.BaseName
                }
            }
            Write-Host ([string]($pkgs.Count) + " packages found.")

            Write-Host "Refreshing WMI package list from $Env:COMPUTERNAME ... " -NoNewline
            $CurrentPackageList = @(Get-WMIObject -Namespace "root\sccmdp" -Query "Select * from SMS_PackagesInContLib")
            Write-Host ([string]($CurrentPackageList.Count) + " packages found.")

            # compare lists
            $result = @(Compare-Object -ReferenceObject $CurrentPackageList -DifferenceObject $pkgs -Property PackageID -PassThru)
            $InvalidPkgLibPackages = @($result |Where-Object {$_.sideindicator -eq '=>'})
            $missingPkgLibPackages = @($result |Where-Object {$_.sideindicator -eq '<='})

            if ($InvalidPkgLibPackages.Count -eq 0){
                Write-Host "All packages in PkgLib on $Env:COMPUTERNAME are valid" -ForegroundColor Green
            } else {
                $response = $null
                if ($InvalidPkgLibPackages.Count -gt $WarnThreshold) {
                    Write-Host "Number of invalid packages exceeds threshold [$($InvalidPkgLibPackages.Count) invalid]." -ForegroundColor Yellow
                    $InvalidPkgLibPackages.PackageID
                    $response = Pause-Script -Message "Proceed with removing invalid PkgLib packages on $($Env:COMPUTERNAME)?" -Warning -Buttons 4
                } else {
                    Write-Host "Orphaned packages in PkgLib on $Env:COMPUTERNAME :" -ForegroundColor Yellow
                    $InvalidPkgLibPackages.PackageID
                }

                if ($response -ieq "No") {
                    Write-Host "Skipping PkgLib package maintenance on $($Env:COMPUTERNAME)." -ForegroundColor Yellow
                } else {
                    $InvalidPkgLibPackages | foreach {
                        $InvalidPackageID = $_.PackageID
                        $InvalidPackageFileName = $_.FileName
                        Write-Host "Removing invalid package $InvalidPackageID from PkgLib on $Env:COMPUTERNAME " -NoNewline
                        try{
                            Remove-Item -Path "$path\$InvalidPackageFileName" -Force
                        } catch {
                            Write-Warning "Failed to remove $InvalidPackageFileName from PkgLib on $Env:COMPUTERNAME"
                        }
                        Write-Host "-Done"
                    }
                }
            }

            # finally, if any package IDs exist in WMI but are missing from PkgLib, prompt user
            if ($missingPkgLibPackages.Count -gt 0) {
                Write-Host "Missing packages in PkgLib on $Env:COMPUTERNAME :" -ForegroundColor Yellow
                $missingPkgLibPackages.PackageID
                $null = Pause-Script -Message "Please manually redistribute above packages to $Env:COMPUTERNAME." -Buttons 0
            }
            Write-Host ""
        }
    }
}

I hope this was helpful. If you have any comments or questions, or if you have an idea about how to further improve this approach, you can connect with me via the comments below or via Twitter.

Show Comments