//
you're reading...
PowerShell, SCCM

PowerShell script to query content status for a specific Task Sequence and generate a HTML report

In a perfect world all your SCCM content is managed properly and all Distribution Points are in a healthy status. Unfortunately, in a real-life that may not always be the case. Before running a major upgrade project, or perhaps a new release of your build, it might be good idea to check status of the content for your Task Sequence before you move into production. SCCM does not give you a nice report giving you a breakdown of content status useful in this scenario (not out of the box at lest). While you can create your own queries and reports directly in SCCM, it won’t necessarily give you  many options for customising the output.

The script I have put together uses the same approach as the one available in TechNet gallery (https://gallery.technet.microsoft.com/scriptcenter/Get-the-content-deployment-a2aab406), it does bring however a few improvements such as status of applications and ability to query all Distribution Points by default – among others. Kudos for gathering the status message codes, as I couldn’t find any Microsoft source reference for this…

The script takes four arguments, two of them are optional:

  • SiteCode – SCCM site code you want to query
  • TaskSequenceID – package ID for the Task Sequence you wish to query
  • ReportFileName (optional) – full path to a location where you want to save your HTML report to (if not specified, the script will save the report to <TaskSequenceId>.html in the current directory)
  • DistributionPoints (optional) – list of Distribution Points you want to query (if not specified, the scrip will discover and query ALL Distribution Points)

Dependencies:

  • PowerShell v3.0 or higher
  • Configuration Manager module must be loaded prior to running the script.
    import-module "<path_to_CM_admin_console_install_dir>\bin\ConfigurationManager.psd1"

    for example:

    import-module "C:\Program Files (x86)\ConfigMgr\bin\ConfigurationManager.psd1"

Sample console output

Console output of running the script should look something like this:


PS C:\Temp> Import-Module "C:\Program Files (x86)\ConfigMgr\bin\ConfigurationManager.psd1"

PS C:\Temp> .\Get-ContentStatus.ps1 -SiteCode DEV -TaskSequenceID DEV001CC
2017-08-11 09:30:02Z # Script version 0.2.0 is running.
2017-08-11 09:30:02Z # Fetching the list of Distribution Points...
2017-08-11 09:30:03Z # Getting list of packages associated with DEV001CC...
2017-08-11 09:30:03Z # Querying 1 Distribution Points for status of 8 packages
2017-08-11 09:30:03Z # Processing packge 1 out of 8 ...
2017-08-11 09:30:04Z # Package ID: DEV00002 Package type: Package Package name: Configuration Manager Client Package
2017-08-11 09:30:04Z # Processing packge 2 out of 8 ...
2017-08-11 09:30:05Z # Package ID: DEV00006 Package type: Boot Image Package Package name: DEV Boot Image (x64)
2017-08-11 09:30:05Z # Processing packge 3 out of 8 ...
2017-08-11 09:30:05Z # Package ID: DEV00007 Package type: Operating System Install Package Package name: Windows 7 SP1 Enterprise x64
2017-08-11 09:30:05Z # Processing packge 4 out of 8 ...
2017-08-11 09:30:06Z # Package ID: DEV00010 Package type: Package Package name: Set OSDComputerName
2017-08-11 09:30:06Z # Processing packge 5 out of 8 ...
2017-08-11 09:30:06Z # Package ID: DEV0005B Package type: Package Package name: OSDWait
2017-08-11 09:30:06Z # Processing packge 6 out of 8 ...
2017-08-11 09:30:07Z # Package ID: DEV0017A Package type: Driver Package Package name: Lenovo T470 20JN
2017-08-11 09:30:07Z # Processing packge 7 out of 8 ...
2017-08-11 09:30:08Z # Package ID: DEV0014B Package type: Application Package name: Adobe Flash Player 22 ActiveX
2017-08-11 09:30:08Z # Processing packge 8 out of 8 ...
2017-08-11 09:30:09Z # Package ID: DEV0008D Package type: Application Package name: Adobe Shockwave Player 12.2
2017-08-11 09:30:09Z # Finished processing all packages on all Distribution Points.
2017-08-11 09:30:09Z # Starting to process the results...
2017-08-11 09:30:09Z # Writing the report to C:\Temp\DEV001CC.html
2017-08-11 09:30:09Z # Script finished.

PS C:\Temp>

Fear not if you see error messages relating to Get-CMDistributionPointInfo or Get-CMApplication cmdlets. These should be followed by “Using fallback method to fetch the list of Distribution Points…” or “Using fallback method to get the application info…” messages respectively, where script is trying to recover from it. You should not see those messages on newer versions of SCCM.

Looking at the example above the output may appear to be redundant, but once you run the script in complex environment it does give you an indication on the level of patience still required from you.

Sample HTML report output

The script will generate a HTML file that is nicely colour coded. Every individual package status on a given Distribution Point will be highlighted with appropriate status colour with corresponding status message on top. If there are any statuses for given package that are different from either “success” or “not targeted”, the total number of servers with any of the statuses that may require your attention (“in progress”, “failed” or “unknown”) will be highlighted as well. It allows you to quickly scroll through and spot any problematic packages and/or Distribution Points. It may not be obvious by looking at the sample report below, but it does come in very handy if number of Distribution Points in your estate is more than, let’s say, 10. It really depends on the width of your screen :)

HTML_content_report

A sample HTML report for content status for a Task Sequence.

 

Performance and compatibility

The script’s running time will depend on both number of packages and number of Distribution Points it needs to query. Irrespectively, it doesn’t seem to be placing to much burden on server resources. I have tested the script on 7 separate environments and I have received the following timings:

No. of DPs No. of packages Running time
6 63 2m
14 175 12m
15 78 20m
29 99 21m
105 61 36m
128 103 1h 48m
235 115 2h 20m

I have tested the script running on following platforms:

  • Windows 7
  • Windows 10
  • Windows Server 2008 R2
  • Windows Server 2012 R2

I have tested the script running against following versions of SCCM:

  • SCCM 2012 R2 CU5
  • SCCM 2012 R2 SP1
  • SCCM 1702

The code

The script is is fairly well commented, but you need to forgive WordPress for not keeping up with code highlighting. Now, without further ado, the script’s code.

# =============================================================================================================================
# Author:		Mietek Rogala
# Filename:		Get-ContentStatus.ps1
#
# Notes:
# Scripts purpose is to query Task Sequence by ID and generate comprehensive content report.
# The script will generate a HTML report in the current directory called <TaskSequenceId>.html, unless otherwise specified.
# The script will query all Distribution Points, unless a list of distribution points is specified.
#
# Dependencies:
#   - Configuration Manager module must be imported prior to running the script.
#
# Script usage:
# [import-module "<path_to_CM_admin_console_install_dir>\bin\ConfigurationManager.psd1"]
# .\Get-ContentStatus.ps1 -SiteCode <sitecode> -TaskSequenceID <TS_id> [-ReportFileName <full_path_to_html_report>] [-DistributionPoints <Distribution_Points_list>]
#
# Examples:
# .\Get-ContentStatus.ps1 -SiteCode DEV -TaskSequenceID DEV0001E
# .\Get-ContentStatus.ps1 -SiteCode P01 -TaskSequenceID P0100357 -ReportFileName "C:\Temp\content_report.html"
# .\Get-ContentStatus.ps1 -SiteCode P01 -TaskSequenceID P0100366 -DistributionPoints DP1.FQDN.COM,DP2.FQDN.COM,DP3.FQDN.COM
#
# =============================================================================================================================

[CmdletBinding()]
#Requires -Version 3.0
#Requires -Modules ConfigurationManager 

Param
( 

    # The Site Code for the Config Manager Site you wish to perform the check against.
    [Parameter(Mandatory=$true)]
    [String]$SiteCode,
    # The Task Sequence ID to be queried
    [Parameter(Mandatory=$true)]
    [String]$TaskSequenceID,
    # The output file name to write report to.
    [Parameter(Mandatory=$false)]
    [String]$ReportFileName,
    # List of distribution points to be queried (script will query all DPs if not specified). Must be FQDN.
    [Parameter(Mandatory=$false)]
    [String[]]$DistributionPoints
) 

############################################################## Functions ##############################################################

# Function to connect to SCCM if not already connected
Function Connect-ToSCCM
{
    # Navigate to site code context if not already connected
    If((Get-Location).Drive.Name -ne $SiteCode)
    {
        Try
        {
            Set-Location -path "$($SiteCode):" -ErrorAction Stop
        }
        Catch
        {
            Throw "Unable to connect to Configuration Manager site $SiteCode."
        }
    }
}

# Function to obtain list of all Distribution Points in the environment
Function Get-DistributionPointsList
{
    # Connect to SCCM if needed
    Connect-ToSCCM

    # Get all Distribution Point names
    $DistributionPointsList = (Get-CMDistributionPointInfo | select Name).Name

    # Get DP names using fall-back method (if Get-CMDistributionPointInfo fails)
    If(!($DistributionPointsList))
    {
        write-host "$(Get-Date -format 'u') # Using fallback method to fetch the list of Distribution Points..."
        # Get all Distribution Point names
        $DistributionPointsList = ((Get-CMDistributionPoint | select NetworkOSPath).NetworkOSPath -replace "\\","")
    }
    return $DistributionPointsList
}

# Function to obtain status of all packages
Function Get-TaskSequenceContentStatus
{
    Param
    ( 

        # The Task Sequence ID to be queried
        [Parameter(Mandatory=$true)]
        [String]$TaskSequenceID,

        # List of distribution points to be queried. Must be FQDN.
        [Parameter(Mandatory=$false)]
        [String[]]$DistributionPoints
    )
    # Connect to SCCM if needed
    Connect-ToSCCM

    # Get ConfigMgr site server name
    $CMSiteServerName = $(Get-CMSite -SiteCode $SiteCode|select ServerName).ServerName 

    # Get Task Sequence properties
    write-host "$(Get-Date -format 'u') # Getting list of packages associated with "$TaskSequenceID"..."
    $TaskSequenceInfo = Get-CMTaskSequence -TaskSequencePackageId $TaskSequenceID

    # Select all packages' IDs
    $TaskSequencePackages = $TaskSequenceInfo.References | select Package

    # Placeholder for fallback method of apps discovery
    $AppsList = $null

    # Array used for storing the info
    $arContentStatusInfo = @()

    # Set counter
    $currentPackageNo = 0

    # Select only unique package references
    $TaskSequenceUniquePackages = $TaskSequencePackages | select Package -uniq
    write-host "$(Get-Date -format 'u') # Querying "$DistributionPoints.Count" Distribution Points for status of "$TaskSequenceUniquePackages.Count" packages"

    # Loop through all the package IDs in the Task Sequence
    ForEach ($Package in $TaskSequenceUniquePackages.Package)
    {
        $currentPackageNo++
        write-host "$(Get-Date -format 'u') # Processing packge "$currentPackageNo" out of " $TaskSequenceUniquePackages.Count "..."
        $ResourceType = ""

        # Try if the package is of 'Package' type
        $CurrentPackage = (Get-CMPackage -Id $Package | Select-object Name).Name
        If(($CurrentPackage)) { $ResourceType = "Package" }

        # Try if the package is of 'Driver Package' type
        If(!($CurrentPackage))
        {
            $CurrentPackage = (Get-CMDriverPackage -Id $Package | Select-object Name).Name
            If(($CurrentPackage)) { $ResourceType = "Driver Package" }
        }

        # Try if the package is of 'Operating System Install Package' type
        If(!($CurrentPackage))
        {
            $CurrentPackage = (Get-CMOperatingSystemImage  -Id $Package | Select-object Name).Name
            If(($CurrentPackage)) { $ResourceType = "Operating System Install Package" }
        }

        # Try if the package is of 'Boot Image Package' type
        If(!($CurrentPackage))
        {
	            $CurrentPackage = (Get-CMBootImage -Id $Package | Select-object Name).Name
            If(($CurrentPackage)) { $ResourceType = "Boot Image Package" }
        }

        # Try if the package is of 'Application' type
        If(!($CurrentPackage))
        {
            $CurrentPackage = (Get-CMApplication -ModelName $Package | Select-object LocalizedDisplayName).LocalizedDisplayName
            If($CurrentPackage)
            {
                $Package = (Get-CMApplication -ModelName $Package | Select-object PackageID).PackageID
                $ResourceType = "Application"
            }
            Else
            {
                # Fall-back method, slower.
                write-host "$(Get-Date -format 'u') # Using fallback method to get the application info..."

                # Cache results if running for the first time
                If(!($AppsList))
                {
                    # Get default CM query limit
                    $defaultCMQueryLimit = Get-CMQueryResultMaximum

                    # Temporarily change CM query limit to maximum allowed to cache all applications
                    Set-CMQueryResultMaximum -Maximum 100000

                    write-host "$(Get-Date -format 'u') # Caching application information for fallback query method, this may take some time..."
                    $AppsList = (Get-CMApplication | Select-Object PackageID,ModelName,LocalizedDisplayName)

                    write-host "$(Get-Date -format 'u') # Caching complete."
                    # Restore default CM query limit
                    Set-CMQueryResultMaximum -Maximum $defaultCMQueryLimit
                }

                # Go through cached results to find the application package info
                ForEach($App in $AppsList)
                {
                    If($App.ModelName -eq "$Package")
                    {
                        $CurrentPackage = $App.LocalizedDisplayName
                        $Package = $App.PackageID
                        $ResourceType = "Application"
                    }
                }
            }
        }
        If(!($CurrentPackage))
        {
            $ResourceType = "Unknown"
        }
        $currentDPNo = 0
        write-host "$(Get-Date -format 'u') # Package ID: " $Package " Package type: " $ResourceType " Package name: " $CurrentPackage

        # Loop through all DPs
        ForEach($DP in $DistributionPoints)
        {
            $currentDPNo++

            # Query status of a specific package on a specific DP
            $ContentWMIquery = Get-WmiObject –NameSpace Root\SMS\Site_$SiteCode –Class SMS_DistributionDPStatus -ComputerName $CMSiteServerName –Filter "PackageID='$Package' And Name='$DP'" | Select Name, MessageID, MessageState, LastUpdateDate, ObjectTypeID

            # If no results returned - assume the DP is not targeted for the package
            If($ContentWMIquery -eq $null)
            {
                $Status = "Not targeted"
                $Message = "No status found! Ensure the package content has been deployed to this distribution point." 

                $arContentStatusInfo += [PSCustomObject]@{
                    'Name' = $CurrentPackage
                    'PackageID' = $Package
                    'ResourceType' = $ResourceType
                    'Distribution Point'= $DP
                    'Status' = $Status
                    'Message' = $Message
                }
            }
            Else
            {
                Foreach ($objItem in $ContentWMIquery)
                {
                    $DPName = $null
                    $DPName = $objItem.Name
                    $UpdDate = [System.Management.ManagementDateTimeconverter]::ToDateTime($objItem.LastUpdateDate) 

                    # Get package status
                    switch ($objItem.MessageState)
                    {
                        1{$Status = "Success"}
                        2{$Status = "In Progress"}
                        3{$Status = "Unknown"}
                        4{$Status = "Failed"}
                    }

                    # Get package status message
                    switch ($objItem.MessageID)
                    {
                        2303{$Message = "Content was successfully refreshed"}
                        2323{$Message = "Failed to initialize NAL"}
                        2324{$Message = "Failed to access or create the content share"}
                        2330{$Message = "Content was distributed to distribution point"}
                        2354{$Message = "Failed to validate content status file"}
                        2357{$Message = "Content transfer manager was instructed to send content to Distribution Point"}
                        2360{$Message = "Status message 2360 unknown"}
                        2370{$Message = "Failed to install distribution point"}
                        2371{$Message = "Waiting for prestaged content"}
                        2372{$Message = "Waiting for content"}
                        2380{$Message = "Content evaluation has started"}
                        2381{$Message = "An evaluation task is running. Content was added to Queue"}
                        2382{$Message = "Content hash is invalid"}
                        2383{$Message = "Failed to validate content hash"}
                        2384{$Message = "Content hash has been successfully verified"}
                        2391{$Message = "Failed to connect to remote distribution point"}
                        2398{$Message = "Content Status not found"}
                        8203{$Message = "Failed to update package"}
                        8204{$Message = "Content is being distributed to the distribution Point"}
                        8211{$Message = "Failed to update package"}
                    }

                    $arContentStatusInfo += [PSCustomObject]@{
                    'Name' = $CurrentPackage
                    'PackageID' = $Package
                    'ResourceType' = $ResourceType
                    'Distribution Point'= $DPName
                    'Status' = $Status
                    'Message' = $Message
                    }
                } # Distribution Status Foreach
            } # DP result query If
        } # $DP Foreach
    } # Package Foreach

    write-host "$(Get-Date -format 'u') # Finished processing all packages on all Distribution Points."
    return $arContentStatusInfo
}
########################################################### End of Functions ##########################################################

write-host "$(Get-Date -format 'u') # Script is running."

$dateGenerated = get-date -Format "dd/MM/yyyy HH:mm:ss"
$currentDirectory = Get-Location

# Set default report file name if one hasn't been provided as parameter
If([string]::IsNullOrEmpty($ReportFileName))
{
    $ReportFileName = $currentDirectory.ToString()+"\"+$TaskSequenceID+".html"
}

# Get list of all DPs if none specified
If([string]::IsNullOrEmpty($DistributionPoints))
{
    write-host "$(Get-Date -format 'u') # Fetching the list of Distribution Points..."
    $DistributionPoints = Get-DistributionPointsList
}
else
{
    write-host "$(Get-Date -format 'u') # Using user-supplied list of Distribution Points"
}

# Get status of content
$aContentStatusResults = Get-TaskSequenceContentStatus -TaskSequenceID $TaskSequenceID -DistributionPoints $DistributionPoints

write-host "$(Get-Date -format 'u') # Starting to process the results..."

# Convert the result array to hashtable for ease of processing
$hashContentInfo = $aContentStatusResults | group PackageID -AsHashTable

######################################
#        Generate HTML report        #
######################################

# Build HTML content
$htmlContentStatusTable = "
<table>
<caption>Content information</caption>
<tr>
<th>Package ID</th>
<th>Package Name</th>
<th>Package type</th>
<th>Total</th>
<th>Success</th>
<th>In progress</th>
<th>Failed</th>
<th>Unknown</th>
<th>Not targeted</th>
"
ForEach($DP in $DistributionPoints)
{
    $htmlContentStatusTable += "
<th>$DP</th>
"
}

$htmlContentStatusTable += "</tr>
"

# Go through all unique package IDs
ForEach($key in $hashContentInfo.Keys)
{

    $iTotal = 0
    $iSuccess = 0
    $iInProgress = 0
    $iFailed = 0
    $iUnknown = 0
    $iNotTargeted = 0

    # Count statuses of each package across all of DPs
    foreach($PackageInfo in $hashContentInfo[$key])
    {
        $iTotal++
        If($PackageInfo.Status -eq "Success") {$iSuccess++}
        If($PackageInfo.Status -eq "In Progress") {$iInProgress++}
        If($PackageInfo.Status -eq "Unknown") {$iUnknown}
        If($PackageInfo.Status -eq "Failed") {$iFailed++}
        If($PackageInfo.Status -eq "Not targeted") {$iNotTargeted++}
    }

    $htmlContentStatusTable += "
<tr>
<td>"+$PackageInfo.PackageID+"</td>
<td>"+$PackageInfo.Name+"</td>
<td>"+$PackageInfo.ResourceType+"</td>
<td>"+$iTotal+"</td>
<td>"+$iSuccess+"</td>
"

    # Set cell bacground color depending on the status
    $inprogressbgcolor = "#FFFFFF"
    $unknownbgcolor = "#FFFFFF"
    $failedbgcolor = "#FFFFFF"
    $nottargetedbgcolor = "#FFFFFF"

    # Change cell colour only if number of DPs with non-success statuses is greater than 0
    If($iInProgress -gt 0) { $inprogressbgcolor = "#FFFF44" }
    If($iUnknown -gt 0) { $unknownbgcolor = "#CECECE" }
    If($iFailed -gt 0) { $failedbgcolor = "#FF0000" }

    $htmlContentStatusTable += "
<td bgcolor="+$inprogressbgcolor+">"+$iInProgress+"</td>
<td bgcolor="+$failedbgcolor+">"+$iFailed+"</td>
<td bgcolor="+$unknownbgcolor+">"+$iUnknown+"</td>
<td>"+$iNotTargeted+"</td>
"

    # List every package status against every DP, including appropriate colour coding
    foreach($PackageInfo in $hashContentInfo[$key])
    {
        # Set cell bacground color depending on the status
        $bgcolor = "#FFFFFF"
        If($PackageInfo.Status -eq "Success") { $bgcolor = "#44FF44" }
        If($PackageInfo.Status -eq "In Progress") { $bgcolor = "#FFFF44" }
        If($PackageInfo.Status -eq "Unknown") { $bgcolor = "#CECECE" }
        If($PackageInfo.Status -eq "Failed") { $bgcolor = "#FF0000" }
        If($PackageInfo.Status -eq "Not targeted") { $bgcolor = "#8C8C8C" }

        $htmlContentStatusTable += "
<td bgcolor="+$bgcolor+">"+$PackageInfo.Message+"</td>
"
    }

    $htmlContentStatusTable += "</tr>
`r`n"
}

$htmlContentStatusTable += "</table>
"

# Get Task Sequence properties
$TaskSequenceInfo = Get-CMTaskSequence -TaskSequencePackageId $TaskSequenceID

# Generate table with Task Sequence properties
$htmlTSTable = "
<table>
<caption>General information</caption>

`r`n"
$htmlTSTable += "
<tr>
<th>Property</th>
<th>Value</th>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence ID</td>
<td>"+$TaskSequenceInfo.PackageID+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence name</td>
<td>"+$TaskSequenceInfo.Name+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence description</td>
<td>"+$TaskSequenceInfo.Description+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence version</td>
<td>"+$TaskSequenceInfo.Version+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence creation date</td>
<td>"+$TaskSequenceInfo.SourceDate+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Task Sequence edited date</td>
<td>"+$TaskSequenceInfo.LastRefreshTime+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Boot image ID</td>
<td>"+$TaskSequenceInfo.BootImageID+"</td>
</tr>
`r`n"
$htmlTSTable += "
<tr>
<td>Date generated</td>
<td>"+$dateGenerated+"</td>
</tr>
`r`n"
$htmlTSTable += "</table>
`r`n"

# Define style via CSS
$style = @'
<style type="text/css">
table {
    border: 1px solid #000000;
    border-collapse: collapse;
}
td {
    font-family: calibri, verdana, arial, helvetica, sans-serif;
    border: 1px solid #000000;
    white-space: nowrap;
}
body {
    font-family: calibri, verdana, arial, helvetica, sans-serif;
    font-size:10pt;
}
th {
    font-family: calibri, verdana, arial, helvetica, sans-serif;
    background-color:black;
    color:white;
    font-weight: bold;
    white-space: nowrap;
}
</style>

'@

write-host "$(Get-Date -format 'u') # Writing the report to " $ReportFileName

# Write the results into HTML file
ConvertTo-Html -Head "<title>Content report for $TaskSequenceID</title>$style" -Body "$htmlTSTable

$htmlContentStatusTable"| out-file $ReportFileName

# Return to the start location
Set-Location $currentDirectory

write-host "$(Get-Date -format 'u') # Script finished."

Happy Task Sequence content reporting!

Discussion

2 thoughts on “PowerShell script to query content status for a specific Task Sequence and generate a HTML report

  1. This is the bananas. Is it on GitHub? I’d love to contribute.

    Like

    Posted by Adam | 2019-01-30, 09:28
  2. After copy and paste, i needed to do the following find-and-replace – with –

    Thanks for sharing the script!

    /BG

    Like

    Posted by Brian Gonzalez (@brianfgonzalez) | 2020-07-23, 13:36

Leave a comment

Categories