Troubleshooting Configuration Manager: Parsing client logs using Powershell and Configuration Items

Introduction

This particular post and topic, originates from me troubleshooting the Office 365 updating issue, where updating Office 365 through Configuration Manager failed and generated error 80070057.

The issue got some attention on both on reddit as well as on Twitter, as the issue on the affected devices, was breaking the ability to apply subsequent Office 365 updates.

The updates which introduced the said issue was quickly pulled, but some devices obviously managed to install the updates anyway.

So how do we fix that and how do we know which devices that are affected? This specific issue produced a specific error in some of the ConfigMgr Client Side Logs, so I came up with a solution involving Powershell and a Configuration Item.

Powershell

The script I’m using within Configuration Manager, is available on my GitHub page as well as down below: https://github.com/imabdk/Powershell

The script is available in 2 editions, as I prefer to strip the script for comments and whatnot, when using it with a Configuration Item. Another edition of the script with logging to the screen, for when testing manually, to more clearly see what’s happening.

The script does following in headlines

  • Create function for locating path to CCM Log Directory
  • Create function for parsing CM log files
  • Formats date and time to match local culture
  • Define how far back in the log the parsing is done
  • Parse the actual log, searching for the string provided in $errorReason
  • Return either COMPLIANT or NON-COMPLIANT
<#
.SYNOPSIS
    Find devices affected by issue described in KB4532996: https://support.microsoft.com/en-us/help/4532996/office-365-version-1910-updates-in-configmgr-do-not-download-or-apply
  
.DESCRIPTION
    Script loops through CMBitsManager.log and searches for entries which matches the issue described in KB4532966. If the script finds the log entry which is associated with the issue, the script returns NON-COMPLIANT.
    This is intended to be used with a configuration item/baseline in ConfigMgr.

.NOTES
    Filename: Find-BrokenO365Clients.ps1
    Version: 1.0
    Author: Martin Bengtsson
    Blog: www.imab.dk
    Twitter: @mwbengtsson
#>

function Get-CCMLogDirectory {
    $ccmLogDir = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\CCM\Logging\@Global').LogDirectory
    if ($ccmLogDir -eq $null) { 
        $ccmLogDir = "$env:SystemDrive\windows\ccm\Logs" 
    }
    Write-Output $ccmLogDir
}

function Parse-CMLogFile {
    param(
        [Parameter(Mandatory=$true)]$LogFile,
        [Parameter(Mandatory=$true)][String[]]$SearchString,
        [datetime]$StartTime = [datetime]::MinValue
    )

    $LogData = Get-Content $LogFile -ErrorAction SilentlyContinue

    if ($LogData) {
 
        :loop for ($i=($LogData.Count - 1);$i -ge 0; $i--) {

            try {
                $LogData[$i] -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
                $LogTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
                $LogMessage = $matches.message
            }
            catch {
                continue
            }

            if ($LogTime -lt $StartTime) {
                break loop
            }

            foreach ($String in $SearchString) {
                if ($LogMessage -match $String) {
		    Write-Output $LogData[$i]
		    break loop
                }
            }
        }
    }
}

$localCulture = Get-Culture
$regionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDateTimePattern
$dateEnd = Get-Date -f $RegionDateFormat
$dateStart = $dateEnd.AddDays(-3)
$ccmLogDir = Get-CCMLogDirectory
# This can of course be any CM log file :-)
$ccmLogName = "CMBITSManager.log"
$errorReason = "RemoteURL should be in this format - cmbits"

$updatesBroken = Parse-CMLogFile -LogFile "$ccmLogDir\$ccmLogName" -SearchString $errorReason -StartTime $dateStart

if ($updatesBroken) {
    Write-Output "NON-COMPLIANT"
}
else {
    Write-Output "COMPLIANT"
}

Configuration Manager

Configuration Item

Using the script with ConfigMgr and a Configuration Item is straightforward. Create a new Configuration Item in your ConfigMgr console, in the Assets and Compliance work space, in the Compliance Settings node.

For your convenience, here’s a few snippets of my CI taken directly from my production environment:

  • The CI is configured with a Powershell script (above Powershell script).

  • And the Compliance Rule to your CI is configured like below. This will make sure that, if the CI returns anything but COMPLIANT, the CI will signal noncompliance.

Configuration Baseline

The CI needs to be attached to a Configuration Baseline. Attach it to an existing CB or simply create a new CB.

Deployment

The Configuration Baseline is being deployed, and when done so, you have the option to Create New Collections based on this deployment. This is where I typically create 2 new collections respectively for Compliant and Non-compliant devices.

Now that the NON-COMPLIANT devices are gathered in a collection, this enables us to quickly deploy a fix or anything similar to the affected devices.

Again, below a snippet of the 2 collections, also taken directly from my production environment:

Resolving the Issue

Resolving the specific issue at hand, obviously involves locating the affected devices. When the Powershell script is run manually on a device affected by the issue, you will see the script correctly return NON-COMPLIANT.

In this scenario, because the CMBITSManager.log contains following errors, which translates into the issue I described in the introduction:

Solution

The only solution that worked for me in this scenario, considering that my Office 365 build was out of scope for the official fix to work, was to copy the entire ClickToRun folder from a working device, and copy that onto the affected device.

I did so with a simple batch script, also making sure to stop and start the ClickToRun (some files being locked if not doing this) service in the process.

@echo off
net stop ClickToRunSvc
timeout /t 5
ren "C:\Program Files\Common Files\microsoft shared\ClickToRun" "ClickToRun_old"
if not exist "C:\Program Files\Common Files\microsoft shared\ClickToRun" md "C:\Program Files\Common Files\microsoft shared\ClickToRun"
xcopy /y /e "%~dp0ClickToRun\*.*" "C:\Program Files\Common Files\microsoft shared\ClickToRun"
net start ClickToRunSvc
timeout /t 5
for /F "tokens=3 delims=: " %%H in ('sc query "ClickToRunSvc" ^| findstr "STATE"') do (
  if /I "%%H" NEQ "RUNNING" (
    exit 1
  ) else ( exit 0 )
)

ENJOY 🙂

1 thought on “Troubleshooting Configuration Manager: Parsing client logs using Powershell and Configuration Items”

  1. thanks for the write-up, i didn’t have the issue mentioned, but i learned about using named anchors in regex and also using :labels in pshell to break out of specific loops. Very useful stuff. Thanks. /Brian

    Reply

Leave a Reply to Brian Gonzalez Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.