ConfigMgr Anxieties


This is a sampling of the most common situations I run into with customer engagements.  I haven’t followed-up my older “what I learned this week” articles, so this is sort of catch-up.

The following is intended for an immature audience.  Mature people will likely fall asleep before completing this, and may require physical or electrical stimulation to regain consciousness.  I am not mature, so I may fall asleep before you do.

1 – Software and Update Deployments with Maintenance Windows and Overrides


  • You can deploy Applications and Packages to Device Collections and User Collections
  • The Deployment itself has properties (attributes) assigned to stipulate the behavior (schedule, target, restrictions, exceptions, etc.)
  • User and Device Collections can have Maintenance Windows assigned, which provide guard rails for drunken, passed-out, deployments
  • Deployments can be assigned to respect or ignore Maintenance Windows, like a politician at a press conference


  • Machines which end up in multiple collections with maintenance windows can easily become unpredictable
  • Machines that end up in multiple non-contiguous maintenance windows can become a reason to drink
  • People who place machines into multiple collections with unique maintenance windows may end up duct taped to a urinal at a bus station
  • It’s easy to forget Maintenance Windows, or how they overlap and pile up on devices which end up in multiple collections


  • Use maintenance windows sparingly
  • Plan all of your maintenance window needs before you apply the first one
  • Consider naming Collections to indicate maintenance window involvement (as well as the window itself: example = “Crappy Machines – MW – Fridays 5p-11p”)
  • Consider custom Device Client Settings for roaming devices, versus stationary devices

2 – Operating System Deployments and PXE Panic


  • You can target PXE deployments of OSD Task Sequences to Collections
  • You can restrict the Deployments to recognize only existing ConfigMgr Clients, or “unknown” Computers (never been on your network or AD or ConfigMgr), or allow anything (both known and unknown)
  • Targeting PXE OSD deployments to both known and unknown can be risky


  • It is actually not easy to accidentally image production machines via PXE using SCCM OSD
  • In order to “accidentally” re-image existing/known computers, you’d have to:
    • Target the wrong Collection(s) with a…
    • Zero-Touch (ZTI) task sequence, and…
    • Select the wrong availability, such as allowing ConfigMgr clients, and…
    • Set the Deployment purpose option to “Required”, and…
    • Not have a PXE password enabled, and…
    • Have the devices boot configuration set to boot from the Network first, and…
    • Not know what you’re doing
    • Not ask for help when you realize you don’t know what you’re doing
    • Getting ready to quit your job out of anger at your employer


  • Be careful with Deployment targeting
  • Clearly name your Task Sequences so your worst technicians can still understand which ones to select
  • Restrict access to Deploying Task Sequences (and anything else) if you work within a large team (tip: roles and scopes are your friend)
  • Consider using a central or regional workbench process, with a local PXE responder (DP, etc.) on the same subnet, isolated from the production network on a common switch.  The network team doesn’t need to be bothered
  • Use a controlled password on your PXE responder (DP, etc.)
  • Slip Xanax into the coffee pot near the network team (just kidding, don’t do that)

3 – Collection Limiting and Refresh Scheduling


  • Collections with Query Rules are “limited” to being a subset of a parent Collection, called (queue the dramatic music) a “limiting collection”
  • A limiting Collection restricts the scope from which the dependent Collection can see resources to consider as members.  If the limiting Collection doesn’t contain a resource, the dependent Collection won’t be able to add it as a member


  • A Query-Rule Collection will only refresh membership results as often as its limiting Collection.  In other words, if the limiting Collection refreshes every 7 days, the dependent Collection can be set to 1 day, but will only update when the limiting Collection hits its 7 day cycle
  • Query-rule Membership Collections with very short refresh cycles may impact site server performance, and may cause ripple effects which can further impact performance
  • Avoid using a lot of incremental updates for collections.
  • Be careful with Direct-rule Membership Collections and assigning refresh schedules


  • Pay attention to Limiting Collections and their membership rules and refresh schedules.
  • Consider tuning query rule collection refresh schedules during and after major changes to the environment only.  For example, during OS migrations/upgrades, reduce the interval to get faster reporting updates, but after the bulk of activity dies down, increase the interval back to reasonable values
  • If you’re not a DBA or DBD, go talk to one and ask for advice on query optimization techniques.  They may look at you like an Ebola patient when you show them WQL, but free doughnuts or Empanadas, and coffee should calm them down again

4 – Accounts and Security


  • ConfigMgr uses several security (user-type) accounts for various features.  Each account is used in a unique and different way, for a unique purpose.  Some are mandatory, and some are optional:
    • Site Installation and Initial Configuration (Windows, SQL Server, AD)
    • Client Push Installation (Workstations, Servers, Domain Controllers, Classified/Sensitive systems, etc.)
    • Network Access / Software Distribution and OSD
    • Domain-Join / OSD
    • Build and Capture / OSD
    • Remote Tools / Client Settings
    • Roles / Accounts / Administration
    • Discovery Methods / Administration
    • Cloud Services / Administration
  • Each account has unique access/privilege needs


  • Accounts which are used by multiple (unrelated) systems often add risk of unexpected interruption by other users (password changes, account lock-outs, etc.)
  • You can often control team access from application-level groups, or via AD security groups.  Trying to manage it both ways often leads to unwanted results.


  • Avoid using SCCM-related accounts for multiple purposes, even within SCCM
  • Aim for dedicated, purpose-driven accounts, exclusive to what they’re intended for
  • Grant each account only the bare minimum permissions it needs
  • Choose a consistent manner by which to manage team access to tools and features
  • NEVER share the use of a ConfigMgr service account with an unrelated system or process
  • NEVER use a person’s own login account for a service account
  • Avoid using a person’s own login account for building site systems
  • Document everything, and track changes always

5 – Device Imaging, Naming and Provisioning


  • If you don’t provide an explicit name for each device during imaging, the default name will suck
  • Sucky device names make for a sucky day at work
  • You can assign a unique device name to a bare metal imaging process using a variety of means within ConfigMgr:
    • Collection Variables
    • UDI form input, or tools like UI++
    • Custom (script) form input
    • Semi-Automated using external systems / third-party tools or custom development
    • Completely Automated using internal data
  • You can spend hours, days, even weeks on the perfect naming process


  • There is no 200 level.  Don’t listen to that other guy


  • Strive for fully-automated, zero-input processes
  • Watch out for forgetting Collection Variables while trying to implement other options at the same time
  • Count the time you spend on device naming (if you’re not already fully-automated) and assign a dollar figure to that, tally it up weekly, monthly and annually.  Then see if it saves you more than that somewhere else, and by how much.  I bet it’s not saving you anything at all (dollar-wise)

6 – Guard Dogs and Anti-Malware Fences


  • Anti-Virus / Anti-Malware is intended to protect your computer from malicious code
  • Most AV/AM products are general in nature, rather than specifically-tuned for every other product you may have installed.
  • Some products step on common parts of a computer which cause AV/AM products to react as if it is malware.
  • System Center Configuration steps on a lot of those parts.
  • Most AV/AM products do not know what Configuration Manager is.
  • Many Windows 10 migration projects are derailed or delayed by AV/AM interference
  • The less consistent your AV/AM environment, the greater the interference risk and scale of impact to migration projects


  • One of the most common interference issues is having unsupported (outdated) AV/AM client versions installed on machines
  • Another common problem are rogue AV tools installed inconsistently throughout the environment
  • Most AV/AM products like McAfee, Symantec, Sophos, provide administrator consoles for managing their own environments.
  • Most AV/AM admin consoles are in front of dedicated AV admins
  • Most AV/AM admins have some sort of favorite food or drink.  Leverage that.


  • Make sure all AV/AM clients are on a consistent, and Windows 10 supported, version.  Refer to vendor docs like McAfee, Symantec and Sophos or whatever you use, for more details.
  • Grant exclusions for ConfigMgr client footprints, such as files/folders, registry paths, services, processes, etc.
  • Don’t forget the firewall exceptions
  • Don’t forget the exclusions for the ConfigMgr site servers too
  • Don’t forget food bribery works wonders on cross-team collaboration

7 – Failing to Keep Up


  • Many customers are still on ConfigMgr 2007, 2012, 2012 R2, as well as Windows Server 2003, 2008, 2008 R2, and SQL 2008, 2012
  • Many customers are still on Windows 7, Office 2010 or 2013
  • Many customers have moved up to ConfigMgr Current Branch, but are still behind on site updates/upgrades.
  • Many customers upgraded in-place but didn’t spend time on the underlying infrastructure (Windows Server OS, SQL Server, physical and virtual resource allocations, etc.)
  • Current levels are Windows Server 2016, SQL Server 2017, SCCM 1802, ADK 1803, MDT 8450, and Windows 10 1803


  • Many sites haven’t been diligent on keeping 3rd party apps updated consistently
  • Many customers haven’t kept their AV and disk encryption installs consistently up to date


  • Focus on bringing all of your platforms up to date
  • Consider a new stack and clean cut-over if possible
  • Double-check your licensing and resource constraints (VM hosts, storage, etc.)
  • Update or replace peripheral processes, like backups, monitoring, etc.
  • Consider third-party tools to augment things like patching, backups, automation, etc.

Did I miss anything?  Let me know in the comments below.

Thank you!


Deploying the ConfigMgr 1802 Admin Console with Recast Right-Click Tools

I’ll spare you a long back story, rationale and explanation behind this, and just jump right into the “how”.  I’m guessing you already know “why”, or just think this is a stupid waste of time.  Either way, life is good.

Deploy the Admin Console

1) Copy the ConsoleSetup folder from \\<servername>\SMS_<sitecode>\Tools over to your Sources location (e.g. “\\<fileserver>\sources$\apps\microsoft\ConsoleSetup”).

2) Create a new Application:

3) Add a Deployment Type…

4) Set the Content (Install/Uninstall) options…

Content Location: \\<servername>\sources\apps\ConsoleSetup
Installation Program: “ConsoleSetup.exe” /q DefaultSiteServerName=cm01.contoso.local TargetDir=”C:\ConfigMgrConsole
Uninstall Program: “ConsoleSetup.exe” /q /uninstall

Locate a reliable detection type.  I chose the Registry.  Keep in mind it is a 32-bit application…

5) Set the User Experience options…

6) Set Requirements if you wish…

7) Click Next on Dependencies, and continue to the finish.  Return to the Application properties form.

8) Click Next and continue until finish.

Note: You can bundle the latest console updates into the deployment, leveraging the same background process that AdminUI.ExtensionInstaller.exe uses: installing the most-recent .msp patch file(s).  But that can add more work for you keeping up with the .msp files, deployment types and detection methods.  Being lazy, I prefer just deploying the most-recent base version and let the user click “Yes” to the prompt to update it after the first launch.  Less work for you, and it annoys them, so it’s a win-win.

Deploy the Right-Click Tools

If you are wondering “why bother with RCT anymore?” that’s a fair question.  While the iterative console improvements are very nice, and the newer “Run Scripts” feature can do many of the same chores, there are still some nice features in RCT that make it nice to keep around.  If you don’t care about RCT, just skip this section and go to step 16.

If you haven’t downloaded the Right-Click Tools by now, go here, fill-out their registration form, wait for the email with the download link, and download the .msi package (e.g. Recast_RCT_Latest.msi)

9) Place the RCT installer .msi file into a new source location for importing into ConfigMgr.  Example “\\<server>\<share>\<folder>\Recast_RCT_Latest.msi”

10) Create a new Application for the Right Click Tools using the MSI package

Wait for the progress bar…

11) Modify the properties as you see fit…

Review the Summary and continue (or go back and drink more coffee)…

Modify the Deployment Type on the Right-Click Tools Application

12) Edit the Deployment Type

13) Select the Dependencies tab, and click Add

14) for the Dependency group name, enter “required”, then click Add

15) Select the ConfigMgr Console Application, and select the Console Setup Deployment Type and click OK

Click OK again, and confirm that the Automatic Installation option shows “Yes”.  If it doesn’t, you have failed, and you must slap yourself in the face.  Otherwise, continue on…

16) Distribute the Content to your DP servers

17) Deploy the Right-Click Tools so it will launch the dependent installation for the Console first.


A Windows 10 Imaging 2-Step Boogaloo

It’s a new dance, and it goes like this…

Step 1 – Left foot forward: Image the device with a generic name, unplug, place on a shelf

Step 2 – Right foot to the side: Fetch from shelf, run script to assign to a user, hand device to user, go back to surfing Twitter

What could possibly go wrong?

Caveat Stuff

This “procedure”, if you will, is predicated on a scenario where the devices are NOT going to retain the auto-generated name when going into production.  They will instead use a unique name based on whomever they are assigned to (e.g. SAMaccountName, etc.).  If you can, I strongly recommend NOT doing this, which would seem strange that I’m essentially negating all of the remainder of this stupid blog post and telling you to just follow step 1, sort of.  However, if you insist on using “JSMITH”, or some other ad hoc data entry value, for the device name, then by all means, drink up, snort up, shoot up, and continue reading.  Thank you!

Errata / Disclaimer / Legal Stuff

At no point in any time in inter-galactic history, for any purpose or interstellar war or planetary conflict, shall anything mentioned herein be provided with any semblance of a warranty, guarantee, or promise that it will be error-free or suitable for your needs.  Nor shall this brainless author assume any liability, or responsibility for any direct, indirect, or alleged damages or loss of productivity, possibly attributed to the direct or indirect use of any information provided herein, for any purpose, explicit or implied, notwithstanding hereinafter for any jurisdiction of human societal or governmental law, or any group of suits on a golf course, related therein.  Golf carts and Martinis are not included.

…and One More Thing

Many blog posts / articles tend to portray a tone of “this is how it’s done”.  This blog post is different for two reason: (a) It’s just ONE example of dealing with ONE common scenario, out of quadrillions of bazillions and kadrillions of possible scenarios, and (b) it’s likely to be the dumbest article you’ve read today.

Step 1 – Image and Stage Device

This step is all about imaging a new device (or wipe/reload an existing device) whereby it isn’t immediately assigned to some whiney complainer, oops, I mean user.  It goes on a shelf, gathering dust, while it awaits being assigned to someone.

  1. Create / Copy / Hallucinate a PowerShell script:
    > It derives a name using available data (ex. Serial number, MAC, etc.).
    > Save the script in a shared location to allow for making a Configuration Manager Package.
    > Refer to horrifically inept script example further below.
  2. Create a new Package in Configuration Manager
    > Note: if you already have a OSD-related package for bundling your script goodies, just toss it in with the rest and they’ll play like over-caffeinated kids in one of those gooey McDonald’s Playland ball pits.
    > Distribute or Update Distribution on the Package
  3. Add a step to your OSD Task Sequence
    > Insert just before “Apply Operating System”
    > Run PowerShell Script –> Choose the Package, and enter the script name and parameters/arguments, select “ByPass”
    > Note: If you want to assign a common OU just assign it in the Task Sequence “Apply Network Settings” step, or add your own “Join Domain or Workgroup” step.
  4. Deploy the Task Sequence
    > If you target “All Unknown Computers”, make sure the collection does not have the “OSDComputerName” Collection Variable attached

Step 2 – Provision and Assign to Hapless User

This step is all about getting up from your desk, grunting and complaining the entire way, maybe knocking over your cup of cold coffee, to shuffle slowly over to the dust-covered shelf, fetching a pre-imaged device, and doing some doodling on it so it can be handed to a bitchy customer, oops, again, I mean user.  Okay, in all seriousness, you may be lucky today, and the user is actually a cool person.  But you’re reading my blog, which means you’re probably not that lucky.

  1. Plug device into your network
  2. Find something to talk about while you wait for it to boot up
  3. Log in using your magical omniscient IT wizard power account
  4. Run a crappy half-baked PowerShell script which renames the device and moves it to a special AD Organizational Unit (OU) to suit the user’s department, etc.
  5. Wait for the reboot
  6. Unplug the device
  7. Throw at the user as hard as you can
  8. Go back to reading Facebook and Twitter
  9. Wait for Security to arrive and escort you out of the building

Horrifically Inept Script Examples

I told you they were going to be horrific and inept, but you didn’t think I was serious.

Script 1 – Assign a “Temporary” Device Name during OSD Task Sequence

Save this mess to a file named “Set-DeviceName.ps1”

param (
  [string] $Prefix = "TMP"
$SerialNum = Get-WmiObject -Class Win32_SystemEnclosure | Select-Object -ExpandProperty SerialNumber
$NewName = "$Prefix-$SerialNum"
# in case you're imaging a VM with a stupid-long serial number...
if ($NewName.Length -gt 15) {
  $SerialNum = $SerialNum.Substring(0,15-($Prefix.Length+1))
$NewName = "$Prefix-$SerialNum"
try {
  Write-Verbose "new device name = $NewName"
  $tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
  $tsenv.Value("OSDComputerName") = $NewName
  Write-Verbose "OSDComputerName = $NewName"
catch {
  Write-Verbose "not running in a task sequence environment"
  Write-Host "new device name = $NewName"

Script 2 – Provision Device for Assigned User

Note: The following chunk of PowerShell code might look impressive, but that’s because I didn’t create all of it.  I just modified original examples shared by John Warnken and Stephen Owen.  Save this mess to a file named “Assign-UserDevice.ps1”.  This script relies on the “Locations.csv” file to provide the list of locations and department codes for the popup form.

param (
  [parameter(Mandatory=$False, HelpMessage="CSV input file path")]
    [string] $CsvFile = "",
  [parameter(Mandatory=$False, HelpMessage="Form Title")]
    [string] $FormTitle = "Contoso - Provision Device",
  [parameter(Mandatory=$False, HelpMessage="Maximum UserName character length")]
    [int] $MaxUserNameLength = 11,
  [parameter(Mandatory=$False, HelpMessage="Force Upper Case username")]
    [switch] $IgnoreCase,
  [parameter(Mandatory=$False, HelpMessage="Keep existing OU location")]
    [switch] $KeepOuLocation,
  [parameter(Mandatory=$False, HelpMessage="Apply Changes")]
    [switch] $Apply,
  [parameter(Mandatory=$False, HelpMessage="Do not force a restart")]
    [switch] $NoRestart

$ScriptPath = Split-Path -Parent $PSCommandPath
if ($CsvFile -eq "") {
  $CsvFile = Join-Path -Path $ScriptPath -ChildPath "Locations.csv"

function Move-ComputerOU {
  param (
    [string] $TargetOU
  $ComputerName = $env:COMPUTERNAME
  $adssearch = New-Object DirectoryServices.DirectorySearcher
  $adssearch.searchroot = $ads
  $adc1 = $adssearch.findall() | Where-Object {$"cn") -like $ComputerName}
  $ComputerDN = $"distinguishedname")
  Write-Verbose "distinguishedName = $ComputerDN"
  $adc = [adsi]"LDAP://$ComputerDN"
  Write-Verbose "target path = $targetOU"

if ($MaxUserNameLength -gt 9) {
  Write-Warning "UserName portion cannot be longer than 9 characters when the prefix is 6 characters long"

if (!(Test-Path $CsvFile)) {
  Write-Warning "CSV Input file not found: $CsvFile"
$LocData = Import-Csv -Path $CsvFile

[xml]$XAML = @' 
  Height="200" Width="320" Topmost="True" WindowStyle="ToolWindow" 
  WindowStartupLocation="Manual" Top="200" Left="200" 
  FocusManager.FocusedElement="{Binding ElementName=ComputerName_text}"> 
    <Label Name="Label_Warn" Content="" HorizontalAlignment="Left" Foreground="#ff0000" Height="27" Margin="15,0,0,0" VerticalAlignment="Top" Width="300" />
    <Label Name="Label_Loc" Content="Loc+Dept" Foreground="#000000" HorizontalAlignment="Left" Height="27" Margin="15,20,0,0" VerticalAlignment="Top" /> 
    <Label Name="Label_Dlm" Content="-" Foreground="#000000" HorizontalAlignment="Left" Height="27" Margin="125,50,0,0" VerticalAlignment="Top" />
    <Label Name="Label_Num" Content="UserName" Foreground="#000000" HorizontalAlignment="Left" Height="27" Margin="150,20,0,0" VerticalAlignment="Top" />
    <ComboBox Name="Combo_Loc" Margin="20,50,0,0" Height="27" Width="90" HorizontalAlignment="Left" VerticalAlignment="Top" VerticalContentAlignment="Center">
    <TextBox Name="Text_User" Margin="150,50,0,0" Height="27" Width="90" HorizontalAlignment="Left" VerticalAlignment="Top" VerticalContentAlignment="Center" Text="" MaxLength="20" CharacterCasing="Lower" />
    <Button Name="Button_Continue" Content="Continue" Margin="90,100,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="27" Width="100"/> 

# Read XAML string and convert into a form object
$reader = (New-Object System.Xml.XmlNodeReader $xaml) 
$Form = [Windows.Markup.XamlReader]::Load( $reader ) 

# Add Form objects as script variables 
$xaml.SelectNodes("//*[@Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)} 

foreach ($loc in $LocData) {
  $LocDept = "$($loc.Loc)$($loc.Dept)"

$Form.Title = $FormTitle
$Text_User.Maxlength = $MaxUserNameLength
if (!($IgnoreCase)) {
  $Text_User.CharacterCasing = "Upper"
# add form handler for pressing Enter on UserName text box
  if ($args[1].key -eq 'Return') {
    Write-Verbose "action -> user pressed Enter on username textbox"
    $Location = $Combo_Loc.SelectedValue
    $UserName = $Text_User.Text.ToString()
    Write-Verbose "selection -> $Location"
    Write-Verbose "username -> $UserName"
    if (!([string]::IsNullOrEmpty($Location))) {
      $Script:LocIndex = $Combo_Loc.SelectedIndex
      $Script:NewName = $Location+'-'+$UserName
      $Script:Ready = $True
# add form handler for clicking Continue button on exit
  Write-Verbose "action -> pressed Continue button"
  $Location = $Combo_Loc.SelectedValue
  $UserName = $Text_User.Text.ToString()
  Write-Verbose "selection -> $Location"
  Write-Verbose "username -> $UserName"
  if (!([string]::IsNullOrEmpty($Location))) {
    $Script:LocIndex = $Combo_Loc.SelectedIndex
    $Script:NewName = $Location+'-'+$UserName
    $Script:Ready = $True
# display the form for the user to interact with

$Form.ShowDialog() | Out-Null

if (!($Script:Ready)) {
  Write-Warning "No selection or entry. Nothing to do."

$RowSet = $LocData[$Script:LocIndex]
$OuPath = $RowSet.DeviceOU

if ($Apply) {
  Write-Host "New Name...: $NewName" -ForegroundColor Green
  if (-not ($KeepOuLocation)) {
    Write-Host "OU Path....: $OuPath" -ForegroundColor Green
    Move-ComputerOU -TargetOU $OuPath
  Write-Verbose "renaming computer to $NewName"
  Rename-Computer -NewName $NewName -Force
  if (!($NoRestart)) {
    Restart-Computer -Force
else {
  Write-Host "Test Mode (No changes were applied)" -ForegroundColor Cyan
  Write-Host "New Name...: $NewName" -ForegroundColor Cyan
  if (-not ($KeepOuLocation)) {
    Write-Host "OU Path....: $OuPath" -ForegroundColor Cyan

Locations.csv File for Assign-UserDevice.ps1

Note: “Loc” can be a building, campus, city, or whatever.  The ADGroup column is for future/optional/possible/potential use for adding the computer to an AD security group as well.


Cheesy Examples

Example: Assign-UserDevice.ps1 -MaxUserNameLength 9 -Verbose

Summary and Conclusion

As you may have surmised by now, everything you’ve read above is completely stupid and useless. You’re shaking your head in disbelief that you skipped some other opportunity to read this, and you should have chosen otherwise, even if that other opportunity was a prostate exam.  You are now dumber for having read this.

You’re welcome.

Miscellaneous SCCM Configuration Stuff using PowerShell with Fries and a Coke

Rather than trying to build some Frankenstein stack of horrors, I decided to piecemeal this instead. What I mean is that in the past I would approach everything like I did back in my app-dev life, and try to make everything an API stack. But more often, for my needs anyway, I don’t need a giant roll-around tool case with built-in workbench. I just need a toolbox with a select group of tools to fit my project tasks.  This makes it easier to cherry-pick useful portions and ignore, or laugh at the rest, as you see fit.  Anyhow, hopefully some of it is useful to others.

  • Version 1.0 – 06/05/2018 – initial post
  • Version 1.1 – 06/08/2018 – added more crappy examples to bore you to death

Purpose:  Why not?

Intent: Automate some or all of the tasks with installing Configuration Manager on a modern Windows platform using PowerShell.

Caveats: You might have better alternatives to each of these snippets.  That’s cool.

Assumptions:  Most examples are intended for processing on the primary site server or CAS, rather than from a remote workstation.  However, considering the author, they can easily be improved upon.

Disclaimer: Provided “as-is” without warranties, test before using in production, blah blah blah.

Example Code Snippets

Set SQL Server Memory Allocation

Note:  Neither dbatools or sqlps provide a direct means for configuring minimum allocated memory for SQL Server instances.  For the the max-only example, I’m using dbatools for simplicity.  For the min and max example, I’m using SMO, because SMO contains “MO”, and “MO” is used for phrases like “mo money!” and “mo coffee!”

param (
  [parameter(Mandatory=$False, HelpMessage="SQL Host Name")]
  [string] $SqlInstance = "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)",
  [parameter(Mandatory=$False, HelpMessage="Mo Memory. Mo Memory!")]
  [int32] $MaxMemMB = 25600
# following line is optional unless you've already finished off that bottle of wine
Install-PackageProvider -Name NuGet -MinimumVersion -Force
Install-Module dbatools -AllowClobber -SkipPublisherCheck -Force
Import-Module dbatools
Set-DbaMaxMemory -SqlInstance $SqlInstance -MaxMB $MaxMemMB

Using SMO, because it has “mo” in the name…

param (
  [parameter(Mandatory=$False, HelpMessage="SQL Host Name")]
  [string] $SqlInstance = "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)",
  [parameter(Mandatory=$False, HelpMessage="Mo Memory. Mo Memory!")]
  [int32] $MaxMemMB = 25600
[reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
$srv = New-Object Microsoft.SQLServer.Management.Smo.Server($SQLInstanceName)
if ($srv.status) {
  $srv.Configuration.MaxServerMemory.ConfigValue = $MaxMemMB
  $srv.Configuration.MinServerMemory.ConfigValue = 8192 

Set CM Database Recovery Model to Simple

param (
  [parameter(Mandatory=$False, HelpMessage="Server Name")]
  [string] $SqlInstance = "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)",
  [parameter(Mandatory=$False, HelpMessage="Site Code")]
  [string] $SiteCode = "P01"
Import-Module dbatools
Set-DbaDbRecoveryModel -SqlInstance $SqlInstance -Database "CM_$SiteCode" -RecoveryModel SIMPLE

Set CM Database Service Principal Name (SPN)

param (
  [parameter(Mandatory=$False, HelpMessage="SQL Host Name")]
  [string] $SqlInstance = "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)",
  [parameter(Mandatory=$False, HelpMessage="SQL Instance Name")]
  [string] $InstanceName = "MSSQLSvc",
  [parameter(Mandatory=$False, HelpMessage="SQL Server Account")]
  [string] $SqlAccount = "$($env:USERDOMAIN)\cm-sql"
$SpnShort = $SqlInstance.split('.')[0]
if ((Test-DbaSpn -ComputerName $SqlInstance).InstanceServiceAccount[0] -ne $SqlAccount) {
  $Spn1 = "$InstanceName/$SpnShort:1433"
  $Spn2 = "$InstanceName/$SqlInstance:1433"
  try {
    Set-DbaSpn -SPN $Spn1 -ServiceAccount $SqlAccount -Credential (Get-Credential)
    Set-DbaSpn -SPN $Spn2 -ServiceAccount $SqlAccount -Credential (Get-Credential)
  catch {
    Write-Error $_.Exception.Message
else {
  Write-Warning "SPN is already configured.  Go back to sleep"

Add CM SQL Service Account to “Log on as a Service” Rights

param (
  [parameter(Mandatory=$False, HelpMessage="Service Account Name")]
  [string] $AccountName = "$($env:USERDOMAIN)\cm-sql"
Install-Module carbon -SkipPublisherCheck -AllowClobber -Force
if ((Get-Privilege -Identity $AccountName) -ne SeServiceLogonRight) {
  try {
    Grant-Privilege -Identity $AccountName -Privilege SeServiceLogonRight
  catch {
    Write-Error $_.Exception.Message
else {
  Write-Warning "Already granted service logon rights. Continue drinking"

Set WSUS IIS Application Pool properties

param (
  [parameter(Mandatory=$False, HelpMessage="Queue Length")]
  [int32] $QueueLength = 2000,
  [parameter(Mandatory=$False, HelpMessage="Private Memory Limit")]
  [int32] $PrivateMemoryLimit = 7372800
Import-Module WebAdministration -DisableNameChecking
try {
  Set-ItemProperty IIS:\AppPool\WsusPool -Name queueLength -Value $QueueLength
  Set-ItemProperty IIS:\AppPool\WsusPool -Name recycling.periodicRestart.privateMemory -Value $PrivateMemoryLimit
catch {
  Write-Error $_.Exception.Message

Move WSUS SQL Database Files

param (
    [parameter(Mandatory=$False, HelpMessage="New Database Files Path")]
    [string] $NewFolderPath = "G:\Database"
$ServerName = $env:COMPUTERNAME
$DatabaseName = "SUSDB"
$ServiceName = "WsusService"
$AppPool = "WsusPool"

if (!(Test-Path $NewFolderPath)) { mkdir $NewFolderPath -Force }
if (!(Test-Path $NewFolderPath)) {
  Write-Error "Your request died a horrible flaming death."
Import-Module WebAdministration
Write-Verbose "stopping WSUS application pool"
Stop-WebAppPool -Name $AppPool
Write-Verbose "stopping WSUS service"
Get-Service -Name $ServiceName | Stop-Service

Import-Module SQLPS -DisableNameChecking
$ServerSource = New-Object "Microsoft.SqlServer.Management.Smo.Server" $ServerName

Write-Verbose "detaching WSUS SUSDB database"
$Db = $ServerSource.Databases | Where-Object {$_.Name -eq $DatabaseName}
$CurrentPath = $Db.PrimaryFilePath
$ServerSource.DetachDatabase($DatabaseName, $True, $True)
$files = Get-ChildItem -Path $CurrentPath -Filter "$DatabaseName*.??f"
Write-Verbose "moving database files to $NewFolderPath"
$files | Move-Item -Destination $NewFolderPath
$files = (Get-ChildItem -Path $NewFolderPath -Filter "$DatabaseName*.??f") | Select-Object -ExpandProperty FullName
Write-Verbose "attaching database files"
# hard-coded 'sa' as the DB owner because I'm lazy AF
$ServerSource.AttachDatabase($DatabaseName, $files, 'sa')

Write-Verbose "starting WSUS service"
Get-Service -Name $ServiceName | Start-Service

Write-Verbose "starting WSUS app pool"
Start-WebAppPool -Name $AppPool

Write-Host "WSUS database files have been moved to $NewFolderPath"

Create System Management AD Container

param (
  [parameter(Mandatory=$False, HelpMessage="Domain Suffix")]
  [string] $DomainSuffix = "DC=contoso,DC=local"
if (!(Get-Module -ListAvailable | Where-Object {$_.Name -eq 'ActiveDirectory'})) {
  Install-WindowsFeature RSAT-AD-Tools -IncludeAllSubFeature -IncludeManagementTools
Import-Module ServerManager
Import-Module ActiveDirectory

if (!(Get-ADObject -Identity 'CN=System Management,CN=System,'+$DomainSuffix)) {
  New-ADObject -Name 'System Management' -Path 'CN=System,'+$DomainSuffix -Type container |
    Set-ADObject -ProtectedFromAccidentalDeletion:$True -Confirm:$False

Grant Permissions on System Management Container (added in 1.1)

param (
  [parameter(Mandatory=$False, HelpMessage="Your Domain Suffix")]
  [string] $DomainSuffix = "DC=contoso,DC=local",
  [parameter(Mandatory=$False, HelpMessage="Site Server Name")]
  [string] $SiteServer = "CM01"
$AdObj = [ADSI]("LDAP://CN=System Management,CN=System,$DomainSuffix")
try {
  $computer = Get-ADComputer $SiteServer
  $sid = [System.Security.Principal.SecurityIdentifier] $computer.SID
  $identity = [System.Security.Principal.IdentityReference] $SID
  $privs = [System.DirectoryServices.ActiveDirectoryRights] "GenericAll"
  $type = [System.Security.AccessControl.AccessControlType] "Allow"
  $inheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "All"
  $ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $identity, $privs, $type, $inheritanceType
catch {
  Write-Error $_.Exception.Message

Import Windows 10 OS Image (added in 1.1)

param (
  [parameter(Mandatory=$False, HelpMessage="OS Source Root Location")]
  [string] $ImageSource = "\\foo\sources\osimages\w10-1803",
  [parameter(Mandatory=$False, HelpMessage="Name to Assign")]
  [string] $OSName = "Windows 10 x64 1803"
$Source = "$ImageSource\sources\install.wim"
if (!(Test-Path $Source)) {
  Write-Error "Boom!  And just like that your code ate itself."
try {
  New-CMOperatingSystemImage -Name $OSName -Path $Source -Description $OSName -ErrorAction Stop
catch {
  Write-Error $_.Exception.Message

Import Windows 10 OS Upgrade Package (added in 1.1)

param (
 [parameter(Mandatory=$False, HelpMessage="OS Source Root Location")]
 [string] $ImageSource = "\\foo\sources\osimages\w10-1803",
 [parameter(Mandatory=$False, HelpMessage="Name to Assign")]
 [string] $OSName = "Windows 10 x64 1803"
if (!(Test-Path $ImageSource)) {
  Write-Error "I bet Jimmy deleted your source folder. You know what to do next."
try {
  New-CMOperatingSystemInstaller -Name $OSName -Path $ImageSource -Description $OSName -ErrorAction Stop
catch {
  Write-Error $_.Exception.Message

Create a Console Folder (added in 1.1)

param (
  [parameter(Mandatory=$False, HelpMessage="Site Code")]
  [string] $SiteCode = "P01",
  [parameter(Mandatory=$False, HelpMessage="Folder Name")]
  [string] $FolderName = "Windows Client",
  [parameter(Mandatory=$False, HelpMessage="Parent Folder")]
  [string] $ParentFolder = "OperatingSystemImage"
Set-Location "$($SiteCode):"
try {
  New-Item -Path "$SiteCode`:\$ParentFolder" -Name $FolderName -ErrorAction Stop
catch {
  Write-Error $_.Exception.Message

Move a Console Item into a Custom Folder (added in 1.1)

$OsImage = "Windows 10 x64 1803"
$Folder = "\OperatingSystemImage\Windows Client"
try {
  Get-CMOperatingSystemImage -Name $OsImage |
    Move-CMObject -FolderPath $Folder
catch {
  Write-Error $_.Exception.Message

Semi-Bonus: Create a Device Collection for each OS in AD

param (
  [parameter(Mandatory=$False, HelpMessage="Site Code")]
  [string] $SiteCode = "P01"
Import-Module ActiveDirectory
$osnames = Get-ADComputer -Filter * -Properties "operatingSystem" | Select-Object -ExpandProperty operatingSystem -Unique
$key = "HKLM:\SOFTWARE\Microsoft\SMS\Setup"
$val = "UI Installation Directory"
$uiPath = (Get-Item -Path $key).GetValue($val)
$modulePath = "$uiPath\bin\ConfigurationManager.psd1"
if (!(Test-Path $modulePath)) {
  Write-Error "Sudden implosion of planetary system.  The end. Roll the credits and dont forget to drop your 3D glasses in the barrel outside."
Import-Module $modulePath
Set-Location "$($SiteCode):"
foreach ($os in $osnames) {
  $collname = "Devices - $os"
  try {
    $sched = New-CMSchedule -DurationInterval Days -DurationCount 7 -RecurCount 1 -RecurInterval 7
    New-CMCollection -Name $collname -CollectionType Device -LimitingCollectionName "All Systems" -RefreshType Both -RefreshSchedule $sched -ErrorAction SilentlyContinue
    $query = 'select distinct SMS_R_System.ResourceId, SMS_R_System.ResourceType, SMS_R_System.Name, SMS_R_System.SMSUniqueIdentifier, SMS_R_System.ResourceDomainORWorkgroup, SMS_R_System.Client from SMS_R_System where SMS_R_System.OperatingSystemNameandVersion="'+$os+'"'
    Add-CMDeviceCollectionQueryMembershipRule -CollectionName $collname -RuleName "1" -QueryExpression $query
    Write-Host "collection created: $collname"
  catch {
    Write-Error $_.Exception.Message


The Most Basic of Basics

Some of the less-common issues I’ve run across in the past few months on the road.  They all tie back to being careful to read the documentation and following the guidelines properly.  These are all 100% true.  This is a stream-of-semi-conciousness post, so it’s going to ramble a bit.

  • Client Push Installation Not Working
    • Client Push Installation Accounts weren’t configured to use the appropriate accounts or the accounts didn’t have permissions
    • AntiVirus was waiting around the corner to stab the client installation files in the neck and steal it’s lunch (exclusions)
  • Clients Not Functioning
    • Site Boundaries were missing
    • DNS is fucked worse than a construction site toilet
    • DHCP is waiting outside the construction site toilet after having eaten 15 burritos and a Coke
    • Network team hates the AD team who hates the SCCM team
    • AD team refuses to hear any complaints about their “perfect” DNS environment
    • Boundary Group systems weren’t configured properly
  • OSD deployments not working
    • Didn’t visit or any other pertinent web sites / blogs / conferences / book stores / random homeless people wearing a “OSD is cool AF” t-shirt / etc.
    • Didn’t watch any YouTube videos of Johan or Mikael, or anyone else that does this every day
    • Didn’t install the correct ADK version
  • PXE not working
    • PXE wasn’t actually installed
    • DHCP options AND IP Helpers were in conflict (should only use IP helpers)
    • Forgot to distribute boot images where needed
    • Forgot to come to work awake
  • Backups Not Working
    • Backup targets were moved without telling anyone
    • GPO settings were stopping the backup service
    • Backup target was out of space
  • SQL database connectivity issues
    • DBA team moved the database without telling the SCCM admins
    • SCCM admins urinated on DBA cars in the parking lot
    • Unsupported SQL configuration (with regards to SCCM)
  • Slow console and slow inventory and status processing
    • Database was more fragmented than the teeth of an old English town drunk
    • DBA’s never heard of Ola Hallengren or Steve Thompson
    • DBA’s heard more than they wanted from me about Ola Hallengren and Steve Thompson
  • Apps and Updates not deploying
    • Maintenance windows were set to impossible periods of availability
    • Collections were not properly aligned with maintenance windows
    • Collections were not named to clarify the use (and configuration) of maintenance windows
  • Couldn’t use “Run Scripts” feature
    • Forgot to check the “pre-release” features option (earlier builds)
    • Forgot to check the ‘Run Scripts’ feature / enable it (earlier builds)
    • Forgot to uncheck the “Script authors require additional script approver” option
    • Forgot to approve the scripts
    • Forgot to actually import any scripts (they thought it made its own with hot water and a spoon)
    • Forgot to upgrade site from 1702
  • Software Center not working / Apps not showing up
    • Using a GPO to place a shortcut to Software Center but referring to the wrong SCClient.exe
    • Nothing was actually deployed to the device or the user trying to use it
  • Software Updates not working
    • WSUS is hosed
    • WSUS is still hosed
    • IIS is hosed too, but over-hosed by WSUS at the moment
    • Never learned to read logs like WCM.log, WSUSCtrl.log, wsyncmgr.log, SUPSetup.log
    • Never bothers looking at any logs
    • Forgot to actually make groups and packages and deploy them to anything
    • Still using GPO settings that conflict with ConfigMgr policies
  • Slow Deployments
    • Poor boundaries and boundary groups
    • Poor poor poor boundary groups
    • Sad little boundary groups, all neglected and lonely
    • BranchCache? WTF is that?
    • Our Network guys are throttling my shit all the time and don’t need to?! What?
  • Too many admins logging into the site server all day / every day
    • No local console installations
    • No understanding of RBA configurations (where needed)
  • More coffee, back to work

This Week in Nerdy Stuff

While I’m still sorting out some personal issues and trying to grasp how it is so many American-English words that begin with “con” or “pro” or “in” don’t have corollary words that make any damned sense whatsoever, and still lamenting walking away from a trip to MMS/MOA this year to start a new job, well, here’s what flew past my dust-covered radar this week, and might have missed your smartphone as well.

A Hammer to Turn Screws


Script: Check-Readiness.ps1

Purpose: Keeps me busy and away from drinking too much coffee.  Okay, seriously, it’s just another flavor of “check for Windows 10 upgrade readiness using PowerShell”. It can be used within SCCM or while standing naked in a WalMart, your choice.

Why didn't they use PARAMETRE, hmm? So American. Anyhow, SourcePath is wherever the Windows 10 media is located.
If not "" then it will dump a file named <computer>-<result>.txt in that location.
I was only half awake when I wrote this. Use at your own risk.
param (
    [parameter(Mandatory=$True, HelpMessage="Path to setup.exe")]
    [string] $SourcePath,
    [parameter(Mandatory=$False, HelpMessage="Path to dump output data")]
    [string] $OutputPath = ""
$setup = Join-Path -Path $SourcePath -ChildPath "setup.exe"
Write-Verbose "setup source is $setup"
if (!(Test-Path $setup)) {Write-Error "$setup not found"; break}
Write-Verbose "starting assessment"
$p = Start-Process -FilePath $setup -Wait -ArgumentList "/Auto Upgrade /Quiet /NoReboot /Compat ScanOnly" -PassThru
Write-Verbose "exit code: $($p.ExitCode)"
switch ($p.ExitCode) {
    -1047526896 { $result = "UpgradeReady"; break } 
    -1047526912 { $result = "SysReqtsFault"; break } 
    -1047526904 { $result = "CompatIssues"; break } 
    -1047526898 { $result = "InsuffDiskSpace"; break } 
    default { $result = "InvalidProductSku"; break }
if ($OutputPath -ne "") { 
    $filename = "$($env:COMPUTERNAME)-$result.txt"
    $filepath = Join-Path -Path $OutputPath -ChildPath $filename 
    $result | Out-File -FilePath $filepath -Append -NoClobber -Encoding Default
Write-Output $result

Shove it out as a Package, or make it a Script object you can spray all over helpless devices via Run Script.  Or run it directly (just add hot water and PSRemoting), or print it out, hang it on the wall, and laugh hysterically at it.  No matter what, it beats whatever your next staff meeting has to offer.