ConfigMgr Client TCP Port Tester

This is a little tool I created for testing the required TCP ports on SCCM client systems. It will check that the required inbound ports are open and that the client can communicate to its management point, distribution point and software update point on the required ports. It also includes a custom port checker for testing any inbound or outbound port.

The default ports are taken from the Microsoft documentation, but these can be edited in the case that non-default ports are being used, or additional ports need to be tested.

The tool does not currently test UDP ports.

Requirements

  • Windows 8.1 + / Windows Server 2012 R2 +
  • PowerShell 5
  • .Net Framework 4.6.2 minimum

Download

Download from the Technet Gallery.

Usage

To use the tool, extract the ZIP file, right-click the ‘ConfigMgr Client TCP Port Tester.ps1′¬†and run with PowerShell.

Checking Inbound Ports

Select Local Ports in the drop-down box and click GO to test the required inbound ports.

Checking Outbound Ports

Select the destination in the drop-down box (ie management point, distribution point, software update point).

Enter the destination server name if not populated by the defaults and click GO. The tool will test ICMP connectivity first, then port connectivity.

Custom Port Checking

To test a custom port, select Custom Port Test from the drop-down box. Enter the port number, direction (ie Inbound or Outbound) and destination (Outbound only). Click Add to add the test to the grid. You can add several tests. Click GO.

Adding Default Servers

You can pre-populate server names by editing the Defaults.xml file found in the defaults directory. For example, to add a default management point:

<ConfigMgr_Port_Tester>
  <ServerDefaults>
    <ManagementPoint>
      <Value>SCCMMP01</Value>
    </ManagementPoint>

Editing / Adding Default Ports

You can also edit, add or remove the default ports in the Defaults.xml file. For example, to add port 5985 in the default local port list:

<PortDefaults>
  <LocalPorts>
    <Port Name="80" Purpose="HTTP Communication"/>
    <Port Name="443" Purpose="HTTPS Communication"/>
    <Port Name="445" Purpose="SMB"/>
    <Port Name="135" Purpose="Remote Assistance / Remote Desktop"/>
    <Port Name="2701" Purpose="Remote Control"/>
    <Port Name="3389" Purpose="Remote Assistance / Remote Desktop"/>
    <Port Name="5985" Purpose="WinRM"/>
  </LocalPorts>

Source Code

Source code can be found in my GitHub repo.

Create Collections for SCCM Client Installation Failures by Error Code

Ok, so in a perfect SCCM world you would never have any SCCM client installation failures and this post would be totally unnecessary. But in the real world, you are very likely to have a number of systems that fail to install the SCCM client and the reasons can be many.

To identify such systems, it can be helpful to create collections for some of the common client installation failure codes so you can easily see and report on which type of installation failures you are experiencing and the number of systems affected.

To identify the installation failure error codes you have in your environment for Windows systems, run the following SQL query against the SCCM database:

select 
	Count(cdr.Name) as 'Count',
	cdr.CP_LastInstallationError as 'Last Installation Error Code'
from v_CombinedDeviceResources cdr
where
	cdr.CP_LastInstallationError is not null
	and cdr.IsClient = 0
	and cdr.DeviceOS like '%Windows%'
group by cdr.CP_LastInstallationError
order by 'Count' desc
Client installation error counts

Next simply create a collection for each error code using the following WQL query, changing the LastInstallationError value to the relevant error code:

select 
    SYS.ResourceID,
    SYS.ResourceType,
    SYS.Name,
    SYS.SMSUniqueIdentifier,
    SYS.ResourceDomainORWorkgroup,
    SYS.Client 
from SMS_R_System as SYS 
Inner Join SMS_CM_RES_COLL_SMS00001 as COL on SYS.ResourceID = COL.ResourceID  
Where COL.LastInstallationError = 53 
And (SYS.Client = 0  Or SYS.Client is null)

Error codes are all fine and dandy, but unless you have an error code database in your head you’ll want to translate those codes to friendly descriptions. To do that, I use a PowerShell function I created that pulls the description from the SrsResources.dll which you can find in any SCCM console installation. There’s more than one way to translate error codes though – see my blog post here. Better yet, create yourself an error code SQL database which you can join to in your SQL queries and is super useful for reporting purposes – see my post here.

Anyway, once you’ve translated the error codes, you can name your collections with them for easy reference:

Client installation failure collections

Now comes the hard part – figuring out how to fix those errors and working through all the affected systems ūüė¨

Get ConfigMgr Client Versions with PowerShell

When upgrading your ConfigMgr site, or installing an update that creates a new ConfigMgr client package, it can be helpful to monitor the rollout of the new client version in your environment.

I put together this PowerShell function which uses my New-WPFMessageBox function to graphically display the count and percentage of client versions in the ConfigMgr site. The data comes from a SQL query, so you’ll need minimum db_datareader access to your ConfigMgr database with your logged-in account, as well as the New-WPFMessageBox function.

By default, it shows only active systems, but you can include inactive systems by checking the box.

img1

img2

Forcing a ConfigMgr Client to Send a New CCMEval Report

In order to maintain a healthy ConfigMgr environment, it is important to know that your clients have successfully run the Configuration Manager Health Evaluation task and reported the results to the Site server. ¬†Sometimes you will find a number of systems that have not reported any health status to the Site server. ¬†In the Devices node of the ConfigMgr Console, you will find “No Results” for the Client Check Result, and the Client Check Detail tab displays nothing, even though the system may be active.

capture

capture

To identify the list of active systems that either have not reported health evaluation results, or have failed the evaluation, I use the following SQL query:


select
sys.Name0 as 'Computer Name',
sys.User_Name0 as 'User Name',
summ.ClientStateDescription,
case when summ.ClientActiveStatus = 0 then 'Inactive'
 when summ.ClientActiveStatus = 1 then 'Active'
 end as 'ClientActiveStatus',
summ.LastActiveTime,
case when summ.IsActiveDDR = 0 then 'Inactive'
 when summ.IsActiveDDR = 1 then 'Active'
 end as 'IsActiveDDR',
case when summ.IsActiveHW = 0 then 'Inactive'
 when summ.IsActiveHW = 1 then 'Active'
 end as 'IsActiveHW',
case when summ.IsActiveSW = 0 then 'Inactive'
 when summ.IsActiveSW = 1 then 'Active'
 end as 'IsActiveSW',
case when summ.ISActivePolicyRequest = 0 then 'Inactive'
 when summ.ISActivePolicyRequest = 1 then 'Active'
 end as 'ISActivePolicyRequest',
case when summ.IsActiveStatusMessages = 0 then 'Inactive'
 when summ.IsActiveStatusMessages = 1 then 'Active'
 end as 'IsActiveStatusMessages',
summ.LastOnline,
summ.LastDDR,
summ.LastHW,
summ.LastSW,
summ.LastPolicyRequest,
summ.LastStatusMessage,
summ.LastHealthEvaluation,
case when LastHealthEvaluationResult = 1 then 'Not Yet Evaluated'
 when LastHealthEvaluationResult = 2 then 'Not Applicable'
 when LastHealthEvaluationResult = 3 then 'Evaluation Failed'
 when LastHealthEvaluationResult = 4 then 'Evaluated Remediated Failed'
 when LastHealthEvaluationResult = 5 then 'Not Evaluated Dependency Failed'
 when LastHealthEvaluationResult = 6 then 'Evaluated Remediated Succeeded'
 when LastHealthEvaluationResult = 7 then 'Evaluation Succeeded'
 end as 'Last Health Evaluation Result',
case when LastEvaluationHealthy = 1 then 'Pass'
 when LastEvaluationHealthy = 2 then 'Fail'
 when LastEvaluationHealthy = 3 then 'Unknown'
 end as 'Last Evaluation Healthy',
case when summ.ClientRemediationSuccess = 1 then 'Pass'
 when summ.ClientRemediationSuccess = 2 then 'Fail'
 else ''
 end as 'ClientRemediationSuccess',
summ.ExpectedNextPolicyRequest
from v_CH_ClientSummary summ
inner join v_R_System sys on summ.ResourceID = sys.ResourceID
where summ.LastEvaluationHealthy in (2,3)
and summ.ClientActiveStatus = 1
order by summ.LastActiveTime Desc

In most cases where the evaluation status reports “Unknown” by this query, you will find that the client has actually run the health evaluation task, it just hasn’t reported the results to the management point for some reason. ¬†I published a PowerShell script previously that lets you view the current health evaluation status on any remote computer by reading the CCMEvalReport.xml file – you can find the script here.

For these “Unknown” status systems, however, I want to force the client to send a health evaluation¬†report to its management point, so I prepared the following PowerShell script to do that. ¬†It can run either against the local computer, or a remote computer. ¬†Admin rights are required on the target system, and if running against the local computer the script must be run as administrator.

The script simply sets the SendAlways flag for CCMEval reports in the registry to “TRUE”, triggers the CM Health Evaluation task to run, waits for it to finish, then changes the SendAlways flag back to “FALSE”. ¬†When the CCMEval program runs with the SendAlways flag set, it will always send the report to the management point even if the client health status has not changed since the last report.

You can verify that from the CcmEval.log on the client:

capture

Within a few minutes you should find that the status for that system has been updated in the ConfigMgr Console with the health evaluation results.

To run the script against the local machine, run PowerShell as administrator and simply do:


Send-CCMEvalReport

To run against a remote computer:


Send-CCMEvalReport -ComputerName PC001

The script also supports verbose output:

Send-CCMEvalReport -ComputerName PC001 -Verbose

capture

Here’s the full code:

Send-CCMEvalReport.ps1


[CmdletBinding()]
    param(
        [Parameter(Mandatory=$False)]
        [String]$ComputerName = $env:COMPUTERNAME
        )

# Code to set 'SendAlways' in registry
$SendAlways = {
    Param($Value)
    $Path = "HKLM:\Software\Microsoft\CCM\CcmEval"

    Try
    {
        $null = New-ItemProperty -Path $Path -Name 'SendAlways' -Value $Value -Force -ErrorAction Stop
    }
    Catch
    {
        $_
    }
}

# Run against local computer
If ($ComputerName -eq $env:COMPUTERNAME)
{
    If (!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
    {
        Write-Warning "This cmdlet must be run as administrator against the local machine!"
        Return
    }

    Write-Verbose "Enabling 'SendAlways' in registry"
    $Result = Invoke-Command -ArgumentList "TRUE" -ScriptBlock $SendAlways 

    If (!$Result.Exception)
    {
        Write-Verbose "Triggering CM Health Evaluation task"
        Invoke-Command -ScriptBlock { schtasks /Run /TN "Microsoft\Configuration Manager\Configuration Manager Health Evaluation" /I }

        Write-Verbose "Waiting for ccmeval to finish"
        do {} while (Get-process -Name ccmeval -ErrorAction SilentlyContinue)

        Write-Verbose "Disabling 'SendAlways' in registry"
        Invoke-Command -ArgumentList "FALSE" -ScriptBlock $SendAlways
    }
    Else
    {
        Write-Error $($Result.Exception.Message)
    }
}
# Run against remote computer
Else
{
    Write-Verbose "Enabling 'SendAlways' in registry"
    $Result = Invoke-Command -ComputerName $ComputerName -ArgumentList "TRUE" -ScriptBlock $SendAlways

    If (!$Result.Exception)
    {
        Write-Verbose "Triggering CM Health Evaluation task"
        Invoke-Command -ComputerName $ComputerName -ScriptBlock { schtasks /Run /TN "Microsoft\Configuration Manager\Configuration Manager Health Evaluation" /I }

        Write-Verbose "Waiting for ccmeval to finish"
        do {} while (Get-process -ComputerName $ComputerName -Name ccmeval -ErrorAction SilentlyContinue)

        Write-Verbose "Disabling 'SendAlways' in registry"
        Invoke-Command -ComputerName $ComputerName -ArgumentList "FALSE" -ScriptBlock $SendAlways
    }
    Else
    {
        Write-Error $($Result.Exception.Message)
    }
}

Reading CCMEval Results Directly from a ConfigMgr Client with PowerShell

Since Configuration Manager 2012, a scheduled task is created by the client installer that runs “CCMEval.exe” periodically to check that the client is healthy and all the components it depends on are functioning as they should be. A number of checks are performed and if anything is found amiss, the default setting is to attempt remediation to fix the problem. ¬†The client health status is reported to ConfigMgr and stored in the database.

By default, if a client has previously reported healthy and continues to be so, no new health report will be sent by the client to the management point, therefore the “last health evaluation time” reported in the console for active devices¬†may be many months ago. ¬†At first, this can seem disconcerting as it is tempting to think something may be wrong, but in reality the client is in all probability healthy.

If we check the ccmeval.log on the client, we can see that a report has recently been generated but it is “not necessary to send”. ¬†The current report still exists on the client however as we will see later.

capture

It is possible to set the following registry key on the client which will cause the report to always be sent, that way ConfigMgr will always display the date of the most recent evaluation, but this will generate more traffic from the client and more work for ConfigMgr, especially if your client count is high, so probably isn’t best practice.

HKLM\SOFTWARE\Microsoft\CCM\CcmEval\SendAlways = TRUE

We can see the last health evaluation time from the ConfigMgr console, either in the Devices node under Assets and Compliance, or in the Client Check node under Monitoring.

capture

We can also find this information from the database directly with the following SQL query:


Select NetBiosName,ClientStateDescription,HealthCheckDescription,LastHealthEvaluation
from dbo.v_CH_ClientSummary ch
inner join dbo.v_CH_EvalResults eval on ch.ResourceID = eval.ResourceID
where NetBiosName = 'PC001'

 

capture

Yet if we look at the registry of the client itself (using PowerShell), we can see that it reports a much more recent date for the last evaluation time:


Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\CCM\CcmEval -Name LastEvalTime | Select -ExpandProperty LastEvalTime | Get-Date

capture

So in this case the difference between the last evaluation date in the database and the last evaluation date on the client itself is 12 months! ¬†But that’s not a problem¬†though, right? The client has been healthy for 12 months and hasn’t needed to report any further health data. Or has it? ¬†What if the client¬†is having trouble sending the current health evaluation report, therefore ConfigMgr is unaware of the actual current status?

That should be a rare occurrence since an inability to communicate with the management point will cause issues in other areas of client function and will probably result in the client being marked inactive in the console, but to be sure, lets look at the results of the most recent CCMEval task on the client. ¬†When the CCMEval.exe runs, it creates an xml file called “CCMEvalReport.xml” with the results of the health evaluation at %windir%\CCM. ¬†If you try to open this file however, you are greeted with an access denied warning:

capture

So first we need to take a copy of it, and open the copy.  Reading an XML file in notepad though is not too much fun:

capture

Of course, you can use an XML editor or a browser for better viewing, but since PowerShell can read XML nicely, lets create a PowerShell function that will open and parse this file into readable format for us.  We can then use this function to retrieve the CCMEval results from any local or remote computer.

First, since we need to run as administrator to take a copy of the file, let’s invoke a separate PowerShell instance¬†using the “runas” verb so we don’t need to open an additional console window, and copy the file to a temp location:


$ComputerName -eq $env:COMPUTERNAME
$TargetFile = "$env:temp\CcmEvalReport.xml"
try
    {
        Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
     }
catch
     {
          $_.Exception.Message
          continue
     }

Then let’s check that the file was copied and exists:


if (!(test-path $TargetFile))
    {
        Write-Error -Message "Could not locate the CcmEvalReport.xml"
        continue
    }

To load the file into PowerShell we need to create a new XML object and call the load method. Then we can remove the file from the temp location.


$xml = New-Object -TypeName System.Xml.XmlDocument
$xml.Load($TargetFile)
Remove-Item $TargetFile -Force

To run the previous code against either the local or a remote machine, we need to encapsulate the code in a script block, then call it using Invoke-Command. In addition, we will add a prompt for credentials in the case access is denied when using PS remoting:


$Script = {
    $TargetFile = ...
}

if ($ComputerName -eq $env:COMPUTERNAME)
{
    $xml = Invoke-Command -ScriptBlock $Script
}
Else
{
    try
    {
        $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
    }
    catch
    {
        if ($Error[0] | Select-String -Pattern 'Access is denied')
        {
            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $ComputerName.  Enter credentials to connect.", '', '')
            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
        }
        Else
        {
            $_.Exception.Message
        }
    }
} 

Now if we browse the $xml object, we can find 2 nodes of importance, a summary node:

capture

And a health check node:

capture

So let’s filter the information we need, and add the results to objects¬†that we can output with our function:


$Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
Select-Object -Property @{
    l = 'Test'
    e = {
        $_.Description
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
} |
Sort-Object -Property Test

$Summary = $xml.ClientHealthReport.Summary |
Select-Object -Property @{
    l = 'ComputerName'
    e = {
        $ComputerName
    }
}, @{
    l = 'EvaluationTime'
    e = {
        [datetime]($_.EvaluationTime)
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
}

Finally, we add this code into a function so we can easily run it against the local computer, or pass a remote computername/s to it as a parameter or along the pipeline to read the CCMEval results from other computers.

Get-CCMEvalResult


function Get-CCMEvalResult
{
    <#             .Synopsis             Get the results of the most recent client health evaluation on a local or remote computer             .DESCRIPTION             Parses the ccmevalreport.xml file into a readable format to view the results of the ccmeval task.  Can be run on the local or remote computer.             .EXAMPLE             Get-CCMEvalResult             Returns the ccmeval results from the local machine             .EXAMPLE             Get-CCMEvalResult -ComputerName PC001             Returns the ccmeval results from a remote machine             .EXAMPLE             'PC001','PC002' | Get-CCMEvalResult             Returns the ccmeval results from a remote machine     #>

    #requires -Version 2

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false,
                ValueFromPipeline = $true
        )]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Begin {
        $Script = {
            $TargetFile = "$env:temp\CcmEvalReport.xml"
            try
            {
                Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
            }
            catch
            {
                $_.Exception.Message
                continue
            }

            if (!(test-path $TargetFile))
                {
                    Write-Error -Message "Could not locate the CcmEvalReport.xml"
                    continue
                }

            $xml = New-Object -TypeName System.Xml.XmlDocument
            $xml.Load($TargetFile)
            Remove-Item $TargetFile -Force
            return $xml
        }
    }

    Process {
        if ($ComputerName -eq $env:COMPUTERNAME)
        {
            $xml = Invoke-Command -ScriptBlock $Script
        }
        Else
        {
            try
                {
                    $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
                }
            catch
                {
                    if ($Error[0] | Select-String -Pattern 'Access is denied')
                        {
                            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $Computername.  Enter credentials to connect.", '', '')
                            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
                        }
                    Else { $_.Exception.Message }
                }
        }

        $Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
        Select-Object -Property @{
            l = 'Test'
            e = {
                $_.Description
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        } |
        Sort-Object -Property Test
        [array]$Summary = $xml.ClientHealthReport.Summary |
        Select-Object -Property @{
            l = 'ComputerName'
            e = {
                $ComputerName
            }
        }, @{
            l = 'EvaluationTime'
            e = {
                [datetime]($_.EvaluationTime)
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        }

        $Summary
        $Checks | Format-Table
    }
}

Now if I run the function against my local machine, I can see each health test performed and the results, as well as the overall status of the evaluation and the time it was performed.

capture

I can also run it against remote computers like this:


Get-CCMEvalResult -ComputerName 'SRV001'
'PC001','PC001' | Get-CCMEvalResult

To troubleshoot any evaluation failures, view the ccmeval.log (%windir%\CCM) and the ccmsetup-ccmeval.log (%windir%\ccmsetup\Logs).