Forcing Installation of the MDT ConfigMgr Integration WMI Classes

Today I encountered an unexpected issue installing the ConfigMgr Integration for MDT. The scenario was an environment with several SMS providers and 2 site servers in a high availability configuration (active / passive). The MDT ConfigMgr Integrations ran successfully on each of the SMS Provider servers, but on the passive site server the BDD_* WMI classes were not created under ROOT\sms\site_XYZ, even though the ConfigMgr Integration wizard completed successfully and reported no error. I ran the wizard with the option to install the task sequence actions to the local server in each case.

Without the WMI classes in place, you get the error “Failed to load class properties and qualifiers for class BDD_*** in task sequence.” when viewing or editing a task sequence containing MDT steps:

The solution was simply to manually compile the MOF file that comes with MDT, which is called Microsoft.BDD.CM12Actions.mof. After the Integration wizard has run, the MOF file be found in Program Files\Microsoft Configuration Manager\AdminConsole\bin. It can also be found in the MDT installation directory Program Files\Microsoft Deployment Toolkit\SCCM.

You need to edit the first line of the MOF file so that it is pointing to the local server, and contains the correct WMI location to install the classes to, eg:

#pragma namespace("\\SCCM001.CONTOSO.COM\root\sms\site_XYZ")

Then compile the MOF file from an admin CMD:

mofcomp <path>\Microsoft.BDD.CM12Actions.mof
BDD_* classes in WMI

HTML Report for SCCM Site Component Warnings and Errors

Just a quick one 🙂

If you’re like me you are too lazy busy to regularly check the component status of an SCCM Site Server for any issues, so why not get PowerShell to do it for you?

The code below will email an html-formatted report of any site components that are currently in an error or warning status, together with the last few error or warning status messages for each component. Run it as a scheduled task or with your favorite automation tool to keep your eye on any current issues. Whether you get annoyed because you now created more work for yourself, or get happy because you can stay on top of issues in your SCCM environment, I leave to you!

The report will display the components that are marked as either critical or warning with the current number of messages:

It will then display the last x status messages for each component for a quick view of what the current issue/s are:

Run the script either on the site server or somewhere where the SCCM console is installed, and set the required parameters in the script.

#####################################################################################################
## ##
## This script checks for any SCCM Site Server components currently in an error or warning ##
## state and emails it as an html report, including the latest status messages for each component. ##
## ##
#####################################################################################################
################
## PARAMETERS ##
################
# Site server FQDN
$SiteServer = "SCCMServer.Contoso.com"
# Site code
$SiteCode = "ABC"
# Location of the resource dlls in the SCCM admin console path
$script:SMSMSGSLocation =$env:SMS_ADMIN_UI_PATH\00000409
# SCCM SQL Server / instance
$script:dataSource = 'SCCMServer'
# SCCM SQL database
$script:database = 'CM_ABC'
# Number of Status messages to report
$SMCount = 5
# Tally interval – see https://docs.microsoft.com/en-us/sccm/develop/core/servers/manage/about-configuration-manager-tally-intervals
$TallyInterval = '0001128000100008'
# Email params
$EmailParams = @{
To = 'joe.bloggs@contoso.com'
From = 'SCCMReports@contoso.com'
Smtpserver = 'contoso-com.mail.protection.outlook.com'
Port = 25
Subject = "SCCM Site Server Component Status Report | $SiteServer | $SiteCode | $(Get-Date Format ddMMMyyyy)"
}
# Html CSS style
$Style = @"
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4286f4;
color: white;
}
h2 {
color: red;
}
</style>
"@
###############
## FUNCTIONS ##
###############
# Function to get data from SQL server
function Get-SQLData {
param($Query)
$connectionString = "Server=$dataSource;Database=$database;Integrated Security=SSPI;"
$connection = New-Object TypeName System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandText = $Query
$reader = $command.ExecuteReader()
$table = New-Object TypeName 'System.Data.DataTable'
$table.Load($reader)
# Close the connection
$connection.Close()
return $Table
}
# Function to get the status message description
function Get-StatusMessage {
param (
$MessageID,
[ValidateSet("srvmsgs.dll","provmsgs.dll","climsgs.dll")]$DLL,
[ValidateSet("Informational","Warning","Error")]$Severity,
$InsString1,
$InsString2,
$InsString3,
$InsString4,
$InsString5,
$InsString6,
$InsString7,
$InsString8,
$InsString9,
$InsString10
)
# Set the resources dll
Switch ($DLL)
{
"srvmsgs.dll" { $stringPathToDLL = "$SMSMSGSLocation\srvmsgs.dll" }
"provmsgs.dll" { $stringPathToDLL = "$SMSMSGSLocation\provmsgs.dll" }
"climsgs.dll" { $stringPathToDLL = "$SMSMSGSLocation\climsgs.dll" }
}
# Load Status Message Lookup DLL into memory and get pointer to memory
$ptrFoo = $Win32LoadLibrary::LoadLibrary($stringPathToDLL.ToString())
$ptrModule = $Win32GetModuleHandle::GetModuleHandle($stringPathToDLL.ToString())
# Set severity code
Switch ($Severity)
{
"Informational" { $code = 1073741824 }
"Warning" { $code = 2147483648 }
"Error" { $code = 3221225472 }
}
# Format the message
$result = $Win32FormatMessage::FormatMessage($flags, $ptrModule, $Code -bor $MessageID, 0, $stringOutput, $sizeOfBuffer, $stringArrayInput)
if ($result -gt 0)
{
# Add insert strings to message
$objMessage = New-Object System.Object
$objMessage | Add-Member type NoteProperty name MessageString value $stringOutput.ToString().Replace("%11","").Replace("%12","").Replace("%3%4%5%6%7%8%9%10","").Replace("%1",$InsString1).Replace("%2",$InsString2).Replace("%3",$InsString3).Replace("%4",$InsString4).Replace("%5",$InsString5).Replace("%6",$InsString6).Replace("%7",$InsString7).Replace("%8",$InsString8).Replace("%9",$InsString9).Replace("%10",$InsString10)
}
Return $objMessage
}
#################
## MAIN SCRIPT ##
#################
# SQL query for component status
$Query = "
Select
ComponentName,
ComponentType,
Case
when Status = 0 then 'OK'
when Status = 1 then 'Warning'
when Status = 2 then 'Critical'
End as 'Status',
Case
when State = 0 then 'Stopped'
when State = 1 then 'Started'
when State = 2 then 'Paused'
when State = 3 then 'Installing'
when State = 4 then 'Re-installing'
when State = 5 then 'De-installing'
End as 'State',
Case
When AvailabilityState = 0 then 'Online'
When AvailabilityState = 3 then 'Offline'
When AvailabilityState = 4 then 'Unknown'
End as 'AvailabilityState',
Infos,
Warnings,
Errors
from vSMS_ComponentSummarizer
where TallyInterval = N'$TallyInterval'
and MachineName = '$SiteServer'
and SiteCode = '$SiteCode '
and Status in (1,2)
Order by Status,ComponentName
"
$Results = Get-SQLData Query $Query
# Convert results to HTML
$HTML = $Results |
ConvertTo-Html Property "ComponentName","ComponentType","Status","State","AvailabilityState","Infos","Warnings","Errors" Head $Style Body "<h2>Components in a Warning or Error State</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
$HTML = $HTML + "<h2></h2><h2>Last $SMCount Error or Warning Status Messages for…</h2>"
If ($Results)
{
# Start PInvoke Code
$sigFormatMessage = @'
[DllImport("kernel32.dll")]
public static extern uint FormatMessage(uint flags, IntPtr source, uint messageId, uint langId, StringBuilder buffer, uint size, string[] arguments);
'@
$sigGetModuleHandle = @'
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
'@
$sigLoadLibrary = @'
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpFileName);
'@
$Win32FormatMessage = Add-Type MemberDefinition $sigFormatMessage name "Win32FormatMessage" namespace Win32Functions PassThru Using System.Text
$Win32GetModuleHandle = Add-Type MemberDefinition $sigGetModuleHandle name "Win32GetModuleHandle" namespace Win32Functions PassThru Using System.Text
$Win32LoadLibrary = Add-Type MemberDefinition $sigLoadLibrary name "Win32LoadLibrary" namespace Win32Functions PassThru Using System.Text
#End PInvoke Code
$sizeOfBuffer = [int]16384
$stringArrayInput = {"%1","%2","%3","%4","%5", "%6", "%7", "%8", "%9"}
$flags = 0x00000800 -bor 0x00000200
$stringOutput = New-Object System.Text.StringBuilder $sizeOfBuffer
# Process each resulting component
Foreach ($Result in $Results)
{
# Query SQL for status messages
$Component = $Result.ComponentName
$SMQuery = "
select
top $SMCount
smsgs.RecordID,
CASE smsgs.Severity
WHEN -1073741824 THEN 'Error'
WHEN 1073741824 THEN 'Informational'
WHEN -2147483648 THEN 'Warning'
ELSE 'Unknown'
END As 'SeverityName',
case smsgs.MessageType
WHEN 256 THEN 'Milestone'
WHEN 512 THEN 'Detail'
WHEN 768 THEN 'Audit'
WHEN 1024 THEN 'NT Event'
ELSE 'Unknown'
END AS 'Type',
smsgs.MessageID,
smsgs.Severity,
smsgs.MessageType,
smsgs.ModuleName,
modNames.MsgDLLName,
smsgs.Component,
smsgs.MachineName,
smsgs.Time,
smsgs.SiteCode,
smwis.InsString1,
smwis.InsString2,
smwis.InsString3,
smwis.InsString4,
smwis.InsString5,
smwis.InsString6,
smwis.InsString7,
smwis.InsString8,
smwis.InsString9,
smwis.InsString10
from v_StatusMessage smsgs
join v_StatMsgWithInsStrings smwis on smsgs.RecordID = smwis.RecordID
join v_StatMsgModuleNames modNames on smsgs.ModuleName = modNames.ModuleName
where smsgs.MachineName = '$SiteServer'
and smsgs.Component = '$Component'
and smsgs.Severity in ('-1073741824','-2147483648')
Order by smsgs.Time DESC
"
$StatusMsgs = Get-SQLData Query $SMQuery
# Put desired fields into an object for each result
$StatusMessages = @()
foreach ($Row in $StatusMsgs)
{
$Params = @{
MessageID = $Row.MessageID
DLL = $Row.MsgDLLName
Severity = $Row.SeverityName
InsString1 = $Row.InsString1
InsString2 = $Row.InsString2
InsString3 = $Row.InsString3
InsString4 = $Row.InsString4
InsString5 = $Row.InsString5
InsString6 = $Row.InsString6
InsString7 = $Row.InsString7
InsString8 = $Row.InsString8
InsString9 = $Row.InsString9
InsString10 = $Row.InsString10
}
$Message = Get-StatusMessage @params
$StatusMessage = New-Object psobject
Add-Member InputObject $StatusMessage Name Severity MemberType NoteProperty Value $Row.SeverityName
Add-Member InputObject $StatusMessage Name Type MemberType NoteProperty Value $Row.Type
Add-Member InputObject $StatusMessage Name SiteCode MemberType NoteProperty Value $Row.SiteCode
Add-Member InputObject $StatusMessage Name "Date / Time" MemberType NoteProperty Value $Row.Time
Add-Member InputObject $StatusMessage Name System MemberType NoteProperty Value $Row.MachineName
Add-Member InputObject $StatusMessage Name Component MemberType NoteProperty Value $Row.Component
Add-Member InputObject $StatusMessage Name Module MemberType NoteProperty Value $Row.ModuleName
Add-Member InputObject $StatusMessage Name MessageID MemberType NoteProperty Value $Row.MessageID
Add-Member InputObject $StatusMessage Name Description MemberType NoteProperty Value $Message.MessageString
$StatusMessages += $StatusMessage
}
# Add to the HTML code
$HTML = $HTML + (
$StatusMessages |
ConvertTo-Html Property "Severity","Date / Time","MessageID","Description" Head $Style Body "<h2>$Component</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
)
}
# Fire the email
Send-MailMessage @EmailParams Body $Html BodyAsHtml
}

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 😬

Monitoring Changes to Active Directory Sites and Subnets with PowerShell

If you work with SCCM and you use AD Forest Discovery to automatically create boundaries from AD Sites or Subnets, you know how important it is for AD to stay up to date with the current information. And when something is changed in Sites or Subnets, you need to be made aware of it so you can reflect the change in your SCCM boundaries and boundary groups. Unfortunately, communication between IT teams is not always what it should be, so I wrote this script to run as a scheduled task and keep an eye on any changes made in AD Sites and IP subnets.

The script works by retrieving the current site and subnet information and exporting it to cache files. The next time the script runs, it will compare the current information with the information in the cached files, and if anything has changed, a report will be sent by email detailing the changes.

It’s one way of ensuring you’re keeping SCCM in sync with your AD!

######################################################################################
## ##
## This script compares the current list of AD sites and subnets with a cached list ##
## If anything has changed, the cached list will be updated and the changes emailed ##
## ##
######################################################################################
################
## PARAMETERS ##
################
# Location of cache files
$ADSitesFile = "G:\Scheduled Task Scripts\Cache Files\ADSites.csv"
$ADSubnetsFile = "G:\Scheduled Task Scripts\Cache Files\ADSubnets.csv"
# Email parameters
$EmailParams = @{
smtpserver = "contoso-com.mail.protection.outlook.com"
To = "SCCMAdmins@contoso.com"
From = "SCCMReports@contoso.com"
Subject = "Active Directory Site and Subnet Changes"
}
# Html CSS style
$Style = @"
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4286f4;
color: white;
}
</style>
"@
#################
## MAIN SCRIPT ##
#################
# ArrayLists to hold the data
$ADSites = [System.Collections.ArrayList]::new()
$ADSubnets = [System.Collections.ArrayList]::new()
# Retrieve the list of AD sites for the current forest
$Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites
# Retrieve the AD subnets for each AD site and convert to a custom object
foreach ($ADSite in $Sites)
{
[void]$ADSites.Add(
[PSCustomObject]@{
'AD Site' = $ADSite.Name
}
)
Foreach ($Subnet in $ADSite.Subnets)
{
[void]$ADSubnets.Add(
[pscustomobject]@{
Name = $Subnet.Name
Site = $Subnet.Site
Location = $Subnet.Location
}
)
}
}
# Test whether the cached lists exist, if not create them assuming first run
If (!(Test-Path $ADSitesFile))
{
$ADSites | Sort 'AD Site' | Export-Csv Path $ADSitesFile NoTypeInformation Force
}
If (!(Test-Path $ADSubnetsFile))
{
$ADSubnets | Sort Name | Export-Csv Path $ADSubnetsFile NoTypeInformation Force
}
# Load in the cached lists
$ADSitesCached = Import-Csv Path $ADSitesFile
$ADSubnetsCached = Import-Csv Path $ADSubnetsFile
# More ArrayLists to hold the data
$ADSitesAdded = [System.Collections.ArrayList]::new()
$ADSitesRemoved = [System.Collections.ArrayList]::new()
$ADSubnetsAdded = [System.Collections.ArrayList]::new()
$ADSubnetsRemoved = [System.Collections.ArrayList]::new()
$ADSubnetsModified = [System.Collections.ArrayList]::new()
# New AD sites
Foreach ($Item in $ADSites)
{
If($Item.'AD Site' -notin $ADSitesCached.'AD Site')
{
[void]$ADSitesAdded.Add($Item)
}
}
# Removed AD Sites
Foreach ($Item in $ADSitesCached)
{
If($Item.'AD Site' -notin $ADSites.'AD Site')
{
[void]$ADSitesRemoved.Add($Item)
}
}
# IP subnet where AD Site has changed, or new IP subnet added
Foreach ($Item in $ADSubnets)
{
$Sub = $ADSubnetsCached.Where({$_.Name -eq $Item.Name})
If ($Sub)
{
If ($Sub.Site -ne $Item.Site)
{
[void]$ADSubnetsModified.Add(
[PSCustomObject]@{
Name = $Item.Name
OldSite = $Sub.Site
NewSite = $Item.Site
}
)
}
}
Else
{
[void]$ADSubnetsAdded.Add($Item)
}
}
# IP subnet removed
Foreach ($Item in $ADSubnetsCached)
{
$Sub = $ADSubnets.Where({$_.Name -eq $Item.Name})
If ($Sub){}
Else
{
[void]$ADSubnetsRemoved.Add($Item)
}
}
# Prepare the HTML
If ($ADSitesAdded.Count -ge 1)
{
$HTML1 = $ADSitesAdded |
ConvertTo-Html Head $Style Property 'AD Site' Body "<h2>The following AD Sites have been ADDED in Active Directory</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
}
If ($ADSitesRemoved.Count -ge 1)
{
$HTML2 = $ADSitesRemoved |
ConvertTo-Html Head $Style Property 'AD Site' Body "<h2>The following AD Sites have been REMOVED from Active Directory</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
}
If ($ADSubnetsAdded.Count -ge 1)
{
$HTML3 = $ADSubnetsAdded |
ConvertTo-Html Head $Style Property Name,Site,Location Body "<h2>The following IP Subnets have been ADDED in Active Directory</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
}
If ($ADSubnetsRemoved.Count -ge 1)
{
$HTML4 = $ADSubnetsRemoved |
ConvertTo-Html Head $Style Property Name,Site,Location Body "<h2>The following IP Subnets have been REMOVED from Active Directory</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
}
If ($ADSubnetsModified.Count -ge 1)
{
$HTML5 = $ADSubnetsModified |
ConvertTo-Html Head $Style Property Name,OldSite,NewSite Body "<h2>The AD Site for the following IP Subnets has been MODIFIED in Active Directory</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String
}
$HTML = $HTML1 + $HTML2 + $HTML3 + $HTML4 + $HTML5
# Send the email report and update the cached lists if required
If ($HTML.Length -ge 1)
{
Try
{
Send-MailMessage Body $HTML @EmailParams BodyAsHtml Priority High ErrorAction Stop
}
Catch
{
$_
Break
}
$ADSites | Sort 'AD Site' | Export-Csv Path $ADSitesFile NoTypeInformation Force
$ADSubnets | Sort Name | Export-Csv Path $ADSubnetsFile NoTypeInformation Force
}

Creating ADR Deployments in SCCM with PowerShell

Today I needed to create a number of deployments for Software Update Automatic Deployment Rules in SCCM, so I turned to PowerShell and used the New-CMAutoDeploymentRuleDeployment cmdlet available in the ConfigurationManager module. It works well enough, however there are a couple of options that the cmdlet cannot set, namely:

  • If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
  • If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart

Turns out that these can easily be set though by manipulating the XML deployment template in the object returned by the cmdlet. You can actually set all the deployment properties that way if you wanted, so long as you know the parameters and values from the deployment template XML.

Here is an example that creates the ADR deployments for an array of collections and also sets the two options above:

# ADR name
$ADRName = "Windows 10 Updates"

# Collections to create deployments for
$Collections = @(
    'SUP - Pilot - ABC - All'
    'SUP - Pilot - XYZ - All'
    'SUP - Production - ABC - All'
    'SUP - Production - XYZ - All'

)

# Import ConfigMgr Module
Import-Module $env:SMS_ADMIN_UI_PATH.Replace('i386','ConfigurationManager.psd1')
$SiteCode = (Get-PSDrive -PSProvider CMSITE).Name
Set-Location ("$SiteCode" + ":")

# Get the ADR
$ADR = Get-CMAutoDeploymentRule -Name $ADRName

# Create the deployments
Foreach ($Collection in $Collections)
{
    # Create the deployment
    $Params = @{
        CollectionName = $Collection
        EnableDeployment = $true
        SendWakeupPacket = $false
        VerboseLevel = 'OnlySuccessAndErrorMessages'
        UseUtc = $true
        AvailableTime = 2
        AvailableTimeUnit = 'Days'
        DeadlineImmediately = $true
        UserNotification = 'DisplaySoftwareCenterOnly'
        AllowSoftwareInstallationOutsideMaintenanceWindow = $true
        AllowRestart = $false
        SuppressRestartServer = $true
        SuppressRestartWorkstation = $true
        WriteFilterHandling = $true
        NoInstallOnRemote = $false 
        NoInstallOnUnprotected = $false
        UseBranchCache = $true
    }
    $null = $ADR | New-CMAutoDeploymentRuleDeployment @Params

    # Update the deployment with some additional params not available in the cmdlet
    $ADRDeployment = Get-CMAutoDeploymentRuleDeployment -Name $ADRName -Fast | where {$_.CollectionName -eq $Collection}
    [xml]$DT = $ADRDeployment.DeploymentTemplate
    # If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
    $DT.DeploymentCreationActionXML.AllowWUMU = "true" 
    # If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart
    If ($DT.DeploymentCreationActionXML.RequirePostRebootFullScan -eq $null)
    {
        $NewChild = $DT.CreateElement("RequirePostRebootFullScan")
        [void]$DT.SelectSingleNode("DeploymentCreationActionXML").AppendChild($NewChild)
    }
    $DT.DeploymentCreationActionXML.RequirePostRebootFullScan = "Checked" 
    $ADRDeployment.DeploymentTemplate = $DT.OuterXml
    $ADRDeployment.Put()
}

Monitor Content Downloads Between an SCCM Distribution Point and a Client

Sometimes you want to monitor the progress of a content download on an SCCM client from a distribution point. You can use the Get-BitsTransfer PowerShell cmdlet, but it doesn’t currently support running on remote computers, so I wrapped the cmdlet in a bit of extra code that lets you get Bits transfer information from a remote computer, and adds a couple of extra values like the transfer size in megabytes and gigabytes as well as a percent complete value. Run it while there’s an active transfer to monitor the progress.

Simply provide a computer name like so:

Get-BitsTransfers -ComputerName PC001
Function Get-BitsTransfers {

[CmdletBinding()]
Param
    (
    [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
    $ComputerName
    )

    Invoke-Command -ComputerName $ComputerName -ScriptBlock {
        $BitsTransfers = Get-BitsTransfer -AllUsers 
        Foreach ($BitsTransfer in $BitsTransfers)
        {
            [pscustomobject]@{
                DisplayName = $BitsTransfer.DisplayName
                JobState = $BitsTransfer.JobState
                OwnerAccount = $BitsTransfer.OwnerAccount
                FilesTotal = $BitsTransfer.FilesTotal
                FilesTransferred = $BitsTransfer.FilesTransferred
                BytesTotal = $BitsTransfer.BytesTotal
                MegaBytesTotal = [Math]::Round(($BitsTransfer.BytesTotal / 1MB),2)
                GigaBytesTotal = [Math]::Round(($BitsTransfer.BytesTotal/ 1GB),2)
                BytesTransferred = $BitsTransfer.BytesTransferred
                PercentComplete = [Math]::Round((100 * ($BitsTransfer.BytesTransferred / $BitsTransfer.BytesTotal)),2)
                CreationTime = $BitsTransfer.CreationTime
                TransferCompletionTime = $BitsTransfer.TransferCompletionTime

            }
        }
    } -HideComputerName

}

New Tool: ConfigMgr Client Notification

Today I whipped-up a very simple tool for ConfigMgr admins and support staff. It allows you to send client notifications (using the so-called fast channel), such as downloading the computer policy, collecting hardware inventory, checking compliance etc, to remote computers from your local workstation independently of the ConfigMgr console.

CNT

The tool connects to your ConfigMgr site server using a Cimsession and PSSession, so you need WsMan operational in your environment. You simply provide some computer name/s in the text box, enter your site server name, select which client notification you want to send and click GO. The tool will get the online status of the clients from the SMS Provider to give you an indication of which systems will receive the client notification. Then it will trigger the client notification on online systems from the site server.

The tool is coded in PowerShell / Xaml and uses the MahApps Metro libraries for WPF styling.

Download

Download the tool from here.

Installation

I decided not to package the tool this time but just to release the files as they are, so if you need to tweak something for it to work in your environment, such as a non-default WsMan port, you can do that. Download and extract the zip file, right-click the ‘ConfigMgr Client Notification Tool.ps1’ and run with PowerShell.

Requirements

– Dot Net 4.6.2 minimum

  • PowerShell 5 minimum

  • WSMan remote access to the ConfigMgr Site server on the default port

  • Appropriate RBAC permissions for performing client operations

  • A version of ConfigMgr that supports the client notifications

Feel free to leave any feedback.

Querying for Devices in Azure AD and Intune with PowerShell and Microsoft Graph

Recently I needed to get a list of devices in both Azure Active Directory and Intune and I found that using the online portals I could not filter devices by the parameters that I needed. So I turned to Microsoft Graph to get the data instead. You can use the Microsoft Graph Explorer to query via the Graph REST API, however, the query capabilities of the API are still somewhat limited. To find the data I needed, I had to query the Graph REST API using PowerShell, where I can take advantage of the greater filtering capabilities of PowerShell’s Where-Object.

To use the Graph API, you need to authenticate first. A cool guy named Dave Falkus has published a number of PowerShell scripts on GitHub that use the Graph API with Intune, and these contain some code to authenticate with the API. Rather than re-invent the wheel, we can use his functions to get the authentication token that we need.

First, we need the AzureRM or Azure AD module installed as we use the authentication libraries that are included with it.

Next, open one of the scripts that Dave has published on GitHub, for example here, and copy the function Get-AuthToken into your script.

The also copy the Authentication code region into your script, ie the section between the following:


#region Authentication
...
#endregion

If you run this code it’ll ask you for an account name to authenticate with from your Azure AD. Once authenticated, we have a token we can use with the Graph REST API saved as a globally-scoped variable $authToken.

Get Devices from Azure AD

To get devices from Azure AD, we can use the following function, which I take no credit for as I have simply modified a function written by Dave.


Function Get-AzureADDevices(){

[cmdletbinding()]

$graphApiVersion = "v1.0"
$Resource = "devices"
$QueryParams = ""

    try {

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)$QueryParams"
        Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get
    }

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

In the $graphAPIVersion parameter, you can use the current version of the API.

Now we can run the following code, which will use the API to return all devices in your Azure AD and save them to them a hash table which organizes the results by operating system version.


# Return the data
$ADDeviceResponse = Get-AzureADDevices
$ADDevices = $ADDeviceResponse.Value
$NextLink = $ADDeviceResponse.'@odata.nextLink'
# Need to loop the requests because only 100 results are returned each time
While ($NextLink -ne $null)
{
    $ADDeviceResponse = Invoke-RestMethod -Uri $NextLink -Headers $authToken -Method Get
    $NextLink = $ADDeviceResponse.'@odata.nextLink'
    $ADDevices += $ADDeviceResponse.Value
}

Write-Host "Found $($ADDevices.Count) devices in Azure AD" -ForegroundColor Yellow
$ADDevices.operatingSystem | group -NoElement

$DeviceTypes = $ADDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$AzureADDevices = @{}
Foreach ($DeviceType in $DeviceTypes)
{
    $AzureADDevices.$DeviceType = $ADDevices | where {$_.operatingSystem -eq "$DeviceType"} | Sort displayName
}

Write-host "Devices have been saved to a variable. Enter '`$AzureADDevices' to view."

It will tell you how many devices it found, and how many devices there are by operating system version / device type.

2018-10-22 16_06_14-Windows PowerShell ISE

We can now use the $AzureADDevices hash table to query the data as we wish.

For example, here I search for an iPhone that belongs to a particular user:


$AzureADDevices.Iphone | where {$_.displayName -match 'nik'}

Here I am looking for the count of Windows devices that are hybrid Azure AD joined, and display the detail in the GridView.


($AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'}).Count
$AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'} | Out-GridView

And here I’m looking for all MacOS devices that are not compliant with policy.


($AzureADDevices.MacOS | where {$_.isCompliant -ne "True"}) | Out-GridView

Get Devices from Intune

To get devices from Intune, we can take a similar approach. Again no credit for this function as its modified from Dave’s code.


Function Get-IntuneDevices(){

[cmdletbinding()]

# Defining Variables
$graphApiVersion = "v1.0"
$Resource = "deviceManagement/managedDevices"

try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"
    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

Running the following code will return all devices in Intune and save them to a hash table again organised by operating system.


$MDMDevices = Get-IntuneDevices

Write-Host "Found $($MDMDevices.Count) devices in Intune" -ForegroundColor Yellow
$MDMDevices.operatingSystem | group -NoElement

$IntuneDeviceTypes = $MDMDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$IntuneDevices = @{}
Foreach ($IntuneDeviceType in $IntuneDeviceTypes)
{
    $IntuneDevices.$IntuneDeviceType = $MDMDevices | where {$_.operatingSystem -eq "$IntuneDeviceType"} | Sort displayName
}

Write-host "Devices have been saved to a variable. Enter '`$IntuneDevices' to view."

Now we can query data using the $IntuneDevices variable.

Here I am querying for the count of compliant and non-compliant iOS devices.


$IntuneDevices.iOS | group complianceState -NoElement

Here I am querying for all non-compliant iOS devices, specifying the columns I want to see, sort the results and outputting into table format.


$IntuneDevices.iOS |
    where {$_.complianceState -eq "noncompliant"} |
    Select userDisplayName,deviceName,imei,managementState,complianceGracePeriodExpirationDateTime |
    Sort userDisplayName |
    ft

All Windows devices sorted by username:


$IntuneDevices.Windows | Select userDisplayName,deviceName | Sort userDisplayName

Windows devices managed by SCCM:


$IntuneDevices.Windows | where {$_.managementAgent -eq "ConfigurationManagerClientMdm"} | Out-GridView

Windows devices enrolled using Windows auto enrollment:


$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsAutoEnrollment"} | Out-GridView

Windows devices enrolled by SCCM co-management:


$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsCoManagement"} | Out-GridView

You can, of course, expand this into users and other resource types, not just devices. You just need the right URL construct for the data type you want to query.

Create a Custom Splash Screen for a Windows 10 In-Place Upgrade

A while back I wrote a blog with some scripts that can be used to improve the user experience in a Windows 10 in-place upgrade. The solution included a simple splash screen that runs at the beginning of the upgrade to block the screen to the user and discourage interaction with the computer during the online phase of the upgrade. Since then, I made some improvements to the screen and styled it to look more like the built-in Windows update experience in Windows 10. Using this splash screen not only discourages computer interaction during the upgrade, but also creates a consistent user experience throughout the upgrade process, for a user-initiated upgrade.

The updated screen contains an array of text sentences that you can customise as you wish. Here is an example of what it could look like:

The splash screen is not completely foolproof in that it is still possible to use certain key combinations, like ctrl-alt-del and alt-tab etc, but the mouse cursor is hidden and mouse buttons will do nothing. The intention is simply to discourage the user from using the computer during the online phase. If the computer is locked, it will display the splash screen again when unlocked. If you wish to block user interaction completely, you might consider a more hardcore approach like this or this.

To use the splash screen, download all the files in my GitHub repository here (including the bin directory). Create a standard package in ConfigMgr containing the files (no program needed) and distribute. Then add a Run PowerShell Script step in the beginning of your in-place upgrade task sequence that looks like the following (reference the package you created):

ts

Once the splash screen has been displayed, the task sequence will move on to the next step – the screen will not block the task sequence.

How does it work?

The Invoke-PSScriptAsUser.ps1 simple calls the Show-OSUpgradeBackground.ps1 and runs it in the context of the currently logged-on user so that the splash screen will be visible to the user (task sequences run in SYSTEM context so this is necessary).

The Show-OSUpgradeBackground.ps1 determines your active screens, creates a runspace for each that calls PowerShell.exe and runs the Create-FullScreenBackground.ps1 for each screen.

The Create-FullScreenBackground.ps1  does the main work of displaying the splash screen. It will hide the task bar, hide the mouse cursor and display a full screen window in the Windows 10 update style. I’ve used the excellent MahApps toolkit to create the progress ring. The text displayed in the screen can be defined by placing short sentences in the $TextArray variable. The dispatcher timer will cycle through each of the these every 10 seconds (or whatever value you set) ending with a final sentence “Windows 10 Upgrade in Progress” which will stay on the screen until the computer is restarted into the next phase of the upgrade.

You can test the splash screen before deploying it simply by running the Show-OSUpgradeBackground.ps1 script.

Remember to deselect the option Show task sequence progress in the task sequence deployment to avoid having the task sequence UI show up on top of the window.