Finding success with SCCM – trigger Schedule

If you’ve ever dealt with SCCM you’ll understand to get a client to forcibly download patches / software from SCCM you’ll need to call WMI to trigger a schedule.

The list of triggers can be found here. Trigger schedule

This post is about how to find / determine success for one of the triggers in this list:

To start with I chose to create a hashTable to hold the triggers with a name:


function New-CMSccmTriggerHashTable
{
    $Sccmhash =@{HardwareInventoryCollectionTask='{00000000-0000-0000-0000-000000000001}'
		SoftwareInventoryCollectionTask='{00000000-0000-0000-0000-000000000002}'
		HeartbeatDiscoveryCycle='{00000000-0000-0000-0000-000000000003}'
		SoftwareInventoryFileCollectionTask='{00000000-0000-0000-0000-000000000010}'
		RequestMachinePolicyAssignments='{00000000-0000-0000-0000-000000000021}'
		EvaluateMachinePolicyAssignments='{00000000-0000-0000-0000-000000000022}'
		RefreshDefaultMPTask='{00000000-0000-0000-0000-000000000023}'
		RefreshLocationServicesTask='{00000000-0000-0000-0000-000000000024}'
		LocationServicesCleanupTask='{00000000-0000-0000-0000-000000000025}'
		SoftwareMeteringReportCycle='{00000000-0000-0000-0000-000000000031}'
		SourceUpdateManageUpdateCycle='{00000000-0000-0000-0000-000000000032}'
		PolicyAgentCleanupCycle='{00000000-0000-0000-0000-000000000040}'
		CertificateMaintenanceCycle='{00000000-0000-0000-0000-000000000051}'
		PeerDistributionPointStatusTask='{00000000-0000-0000-0000-000000000061}'
		PeerDistributionPointProvisioningStatusTask='{00000000-0000-0000-0000-000000000062}'
		ComplianceIntervalEnforcement='{00000000-0000-0000-0000-000000000071}'
		SoftwareUpdatesAgentAssignmentEvaluationCycle='{00000000-0000-0000-0000-000000000108}'
		SendUnsentStateMessages='{00000000-0000-0000-0000-000000000111}'
		StateMessageManagerTask='{00000000-0000-0000-0000-000000000112}'
		ForceUpdateScan='{00000000-0000-0000-0000-000000000113}'
		AMTProvisionCycle='{00000000-0000-0000-0000-000000000120}'}
    $Sccmhash
}

Now any of triggers can be accounted for with this hashtable.


$sccmHash = New-CMSccmTriggerHashTable
$sccmHash['RequestMachinePolicyAssignments']
{00000000-0000-0000-0000-000000000021}

With a human readable form for the trigger a  function to Invoke the schedule for the trigger can be constructed:

function Invoke-CMRequestMachinePolicyAssignments
{
    param([Parameter(Mandatory=$true)]$computername, 
    [Parameter(Mandatory=$true)]$Path = 'c:\windows\ccm\logs',
            [pscredential]$credential)
    $Sccmhash = New-CMSccmTriggerHashTable
    if(Test-CCMLocalMachine $computername)
    {
        $TimeReference =(get-date)
    }
    else
    {
        $TimeReference = invoke-command -ComputerName $computername `
        -scriptblock {get-date} -credential $credential
    }
    if($credentials)
    {
        Invoke-WmiMethod -Class sms_client -Namespace 'root\ccm' 
       ​-ComputerName $computername -credential $credential 
      ​-Name TriggerSchedule 
      ​-ArgumentList "$($Sccmhash["RequestMachinePolicyAssignments"])" 
    }
    else
    {
        $SmsClient =[wmiclass]("\\$ComputerName\ROOT\ccm:SMS_Client")
        $SmsClient.TriggerSchedule`
        ($Sccmhash["RequestMachinePolicyAssignments"])
    }
    $RequestMachinePolicyAssignments = $false

    # can see when this is requested from the Policy agentlog:
    $RequestMachinePolicyAssignments = Test-CMRequestMachinePolicyAssignments`
     -computername $computername -Path $Path -TimeReference $TimeReference `
     -credential $credential

    [PSCustomObject]@{'RequestMachinePolicyAssignments' = $RequestMachinePolicyAssignments
                      'TimeReference' = ($TimeReference)}
}

Once the Request Machine Policy Assignments is triggered.  Another function is called.  This is where a search through the client logs determine success of the trigger invocation.

The value that determines success is Evaluation not required. No changes detected. Which can be found in the PolicyEvaluator.log

In order to find this value we first need to find out if the computer name that was passed is a local machine or a remote machine. This is done with the function Test-CCMLocalMachine. This function is used in both the invoke and the test to determine if excution is on the local machine or a remote machine.  To make sure when searching the log a $TimeReference is used. If it is passed in one of the parameters then that value is used to search through the log.  If it is not passed  the current time from the remote or local machine will be used.


function Test-CMRequestMachinePolicyAssignments
{
    param([Parameter(Mandatory=$true)]$computername, 
    [Parameter(Mandatory=$true)]$Path = 'c:\windows\ccm\logs'
    ,[datetime]$TimeReference,
    [pscredential] $credential)
    if ($TimeReference -eq $null)
    {
        if(Test-CCMLocalMachine $computername)
        {
            $TimeReference =(get-date)
        }
        else
        {
            [datetime]$TimeReference = invoke-command 
            ​-ComputerName $computername -scriptblock {get-date}
        }
    }
    $RequestMachinePolicyAssignments = $false
    # can see when this is requested from the Policy agentlog:
    Push-Location
    Set-Location c:
    if(Test-CCMLocalMachine $computername)
    {
        $p = Get-CMLog -path "$path\policyevaluator.log"
        $runResults = $P |Where-Object{$_.Localtime -gt $TimeReference} `
       | where-object {$_.message -like `
         "*Evaluation not required. No changes detected.*"}
    }
    else
    {
        $p = Get-CCMLog -ComputerName $computerName -path $Path -log policyevaluator -credential $credential
        $runResults = $P.policyevaluatorLog |Where-Object{$_.Localtime -gt $TimeReference} | where-object {$_.message -like "*Evaluation not required. No changes detected.*"}
    }
    Pop-Location
    #if in the 

        if($runResults)
        {
            $RequestMachinePolicyAssignments = $true
        }
    $RequestMachinePolicyAssignments
}

Finding this value in the PolicyEvaluator.log can take up to 45 minutes or more depending on the setup of your SCCM environment.

To allow for finding the value described above and two other triggers. The following script demonstrates its usage:

GetAvailUpdates.ps1

All of the functions shown above can be found in this github repository:

SCCMUtilities

 

I hope this helps someone

Until then

 

Keep Scripting

 

thom

Advertisements

Parsing CCM/Logs – Part 2 using a Dynamic Parameter

In the first Blog post Parsing CCM\Logs I showed you how I was able to get a Script from the community and make a few tweaks to allow for it to parse logs for CCM.  In this blog post I’m going to show you how I took the next step.   My next step was to use the parsing logic in a script and incorporate a Dynamic Parameter.

To begin with I wanted to have the user not have to go to the machine and find every log for CCM\logs and then type in the value of that log name.  For instance in my directory for c:\windows\ccm\logs there were 178 files with the .log extension.  Trying to create a Validate set for this many logs is also problematic.  So I chose the dynamic Parameter approach for this function.  Now on to the script.

The first portion of the script is standard parameter values.


param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')

The next portion of this script is where the “magic” is.  The next parameter -log is created dynamically from the first two variables ($computerName, $path).


DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{

$FilePath = "\\$ComputerName\$($path -replace ':','$')"
}
else
{
$FilePath = "\\$computerName\$((get-item $path).FullName -replace ':','$')"
}

$logs = Get-ChildItem "$FilePath\*.log"
$LogNames = $logs.basename

$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'

$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)

$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)

$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)

$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary

}

To explain what is going on here I’ll start with the code I used to get me to this point. Martin Schvartzman wrote a great article that showed how to do most of what I’ve posted here Dynamic ValidateSet in a Dynamic Parameter. 

I’ll do my best to explain how his code works.  First step is the DynamicParam Statement.  This tells PowerShell that we are going to create a dynamic Parameter.  Very simply stated a dynamic parameter is a Parameter that is added at runtime only when needed.

The first thing that is done in this dynamic parameter is to create a Runtime Defined Parameter Dictionary.  In order to add a runtime parameter we have to define the parameter and it’s attributes and then add it to the collection to return to the runtime so it will be added properly.

<pre>System.Management.Automation.RuntimeDefinedParameterDictionary</pre>

This next portion of the code creates a object that will contain our parameter attributes.


$logAttribute = New-Object System.Management.Automation.ParameterAttribute

For the purposes of this script we are only going to make the parameter mandatory, set it’s position in the pipeline, and create a help message.  There are other items that can be defined if required.  We can see this by getting the members of the $logAttribute:


$logAttribute  | get-member -properties

TypeName: System.Management.Automation.ParameterAttribute

Name MemberType Definition
---- ---------- ----------
DontShow Property bool DontShow {get;set;}
HelpMessage Property string HelpMessage {get;set;}
HelpMessageBaseName Property string HelpMessageBaseName {get;set;}
HelpMessageResourceId Property string HelpMessageResourceId {get;set;}
Mandatory Property bool Mandatory {get;set;}
ParameterSetName Property string ParameterSetName {get;set;}
Position Property int Position {get;set;}
TypeId Property System.Object TypeId {get;}
ValueFromPipeline Property bool ValueFromPipeline {get;set;}
ValueFromPipelineByPropertyName Property bool ValueFromPipelineByPropertyName {get;set;}
ValueFromRemainingArguments Property bool ValueFromRemainingArguments {get;set;}

Since we are going to need this set of attributes in our parameter we need to add this to the Attribute collection ($logCollection) that will in turn be added to the runtime Parameter $logParam.

Next we’ll create our Validate set item from the list of logs on the remote machine which was gathered with the FilePath variable then added to a new object that will contain our ValidateSet attributes. Then add it to our LogCollection.


$FilePath = "\\$ComputerName\$($path -replace ':','$')"

<br>

$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)

 $logCollection.add($logValidateSet)

Finally we’ll add our parameter name and LogCollection to a Runtime Defined parameter.  Then put this all in our Runtime Defined Parameter Dictionary. Then hand it back to PowerShell.


$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)

$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
 $logDictionary.Add($ParameterName,$logParam)
 return $logDictionary

Now  that we have the Full explanation of the Dynamic Parameter we can stitch our previous Log Parser together with this function to give us back any one of the logs on our remote machine.  We’ll put this in our Process block of our function:


 $sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
 $sb1 = [scriptblock]::Create($sb2)
 $results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
 [PSCustomObject]@{"$($log)Log"=$results}

Now when we call Get-CcmLog we’ll get a return with a parsed log that has Log appended in the object name.

dynparam3

Full code is posted in a gist here:

Parsing CCM\Logs

If you’ve ever worked with Configuration manager you’ll understand that there are quite a few logs on the Client side.  Opening and searching through them for actions that have taken place can be quite a task.  I needed to find when an item was logged during initial startup/build of a vm.  So I sought out tools to parse these logs to find out the status of  Configuration Manager client side. This post is about the tools/scripts I found and what I added to them to make it easier to discover and parse all the log files.

I started with the need to be able to just parse the log files.  I discovered that Rich Prescott in the community had done the work of parsing these log files with this script:

http://blog.richprescott.com/2017/07/sccm-log-parser.html

With that script in had I made two changes to the script.  The first change was to allow for all the files in the directory to be added to the return object.

 if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}

The second change allowed for the user to specify a tail amount. This allows for just a portion of the end of the log to be retrieved instead of the entire log.   That script can be found on one of my gists at the Tail end of this article.

 if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-Content -path $file
}
ForEach($l in $lines )

 

I hope this helps someone.

Until then

Keep scripting

Thom