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 DeploymentResearch.com 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

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.


ConfigMgr Device Naming, Part 2: The Electric Boogaloo



So the previous blog post infuriated a few people, so I thought “I can do better than that. I can infuriate more people!” and came up with this:

I combined several PowerShell scripts, some oatmeal, hot water, chopped walnuts and a fresh cup of coffee into a single, mono-nuclear, monolithic uber-script that can be awoken (that’s a real word, I checked) from within a task sequence to rope the legs of the device, hold it down, and burn a new name on it.

You can view and laugh at it here.

Note: If you wish to ignore and infuriate people who strongly advise against using location names as part of device names, you will also want the associated “locations.txt” file to go with this steaming pile of electrons.  This is a poor man’s version of using an MDT CustomSettings.ini with DefaultGateway mappings, only poorer, and it hasn’t had anything to eat in a long, long time.

How to make this cat say “moo”…

  1. Drop the script files in a folder
    1. If you’re using location names, be sure to edit the locations.txt file to suit your environment.
  2. Create a new (or update an existing, your choice) Package in Configuration Manager.
  3. The Package contains source files, but has no Program entries, at least not for this script.
  4. Distribute that mess out into your unsuspecting environment.
  5. Add a task sequence step (Add > General > Run PowerShell Script)
    1. Insert somewhere above the “Apply Operating System” step
    2. Name: Set Computer Name
    3. Select the Package with the the script
    4. Script name: Set-ComputerName.ps1
    5. Parameters: (see notes)
    6. PowerShell execution policy: Bypass
  6. Click OK
  7. Deploy
  8. Run like hell

Figure 1 shows an example using the -Interactive parameter.  Figure 2 shows an example of using the fully-automatic, belt-fed, multi-rotational, liquid-cooled location-based naming option, which implies the -InfuriatesTheShitOutOfSomeAdmins parameter.

set-name1Figure 1 – Comes before Figure 2 🙂

set-name2Figure 2 – Comes right after Figure 1


  • The -Interactive [switch] parameter ignores all other parameters except -DefaultName.  This parameter displays a really fancy, super-complex, highly-sophisticated dialog form for entering the computer name.  It looks like this…
  • -DefaultName is a [string] parameter which is set to “” by default.  If a value is assigned, it overrides everything like a car crashing through a drug store front window at 50 mph.
    Example: -DefaultName “DOUCHEBAG”
  • -UseLocation is a [switch] parameter which references the default IP gateway, at the time of imaging, to determine the location code to add to the device name.  If no matching gateway is found, you can force a default using the -DefaultLocation parameter.  You can also use multiple location.txt files (with different filenames of course) by using the -LocationFile parameter.  This might be useful when you have a CAS environment and want to segregate different sub-groups of IP gateways for some crazy, glue-sniffing reason. The locations.txt file uses a strict format which took decades to refine:  GATEWAYADDRESS=FULLNAME,SHORTNAME
    Example: -UseLocation -DefaultLocation “LON” -LocationFile “en-gb.txt”
    Example: -UseLocation -DefaultLocation “CLT”
  • -UseHyphens is a [switch] parameter that will concatenate the sub-atomic naming particles into a cohesive pile of dung using 100% organic gluten-free hyphen characters.
    Example: (without) “D1234”
    Example: (with) “D-1234”
  • -SnMaxLen controls the maximum length of the BIOS serial number values to allow when concatenating the final name value.  In most cases, a serial number won’t cause a problem, but within some virtual environments, the serial number can be almost as long as a fillibuster speech on Capitol Hill.  The default is 8 characters.  The truncation is from the left, so this fetches the right-most characters.
    Example: -SnMaxLen 10
  • -Testing is a [switch] parameter for running the script outside of a task sequence for testing and validation only.  If you don’t use -Testing and not running within a task sequence session, it will throw some ugly red errors at you because it can’t invoke the Microsoft.SMS.TSEnvironment interface.  Can be combined with any or all other parameters.
    Example: -Testing
  • If you wish to change the “form factor” codes (“D”, “L”, etc.) you will need to edit the {switch} block code within function Get-FormFactorCode. Between lines 170 and 185 or so.

So, for those of you that find this useful: glad I could help in some small way.  For those who are even more angry: Decaf is on sale at Trader Joe’s this week.  Stock up.


ConfigMgr – 2 Minute Microwave Style

Genesis – I posted a tweet about someone I know getting stressed at learning Configuration Manager in order to manage 50 Windows devices.  All desktops.  The background is basically that his company had planned on 1000 devices running Windows.  But the end-users, who wield more purchasing power, opted to buy mostly Macbooks.  So the total Windows device count was capped at 50, BUT…. they already approved the purchase of ConfigMgr.  It’s worth noting that the end-users also purchases JAMF (formerly Casper) and set it up in their own secret underground lab, complete with a diabolical German scientist in a white lab coat.  Ok.  That last part isn’t really true, but the JAMF part is true.

So, the “discussion” slid into “okay mr. smarty-pants skatter-turd-brainz, what would you want in a ‘perfect’ ConfigMgr world to address such a scenario?” (again, I’m paraphrasing a bit here)

MC DJam, aka DJammer, aka David the Master ConfigMaster Meister of ConfigMgr, popped some thermal verbals in front of the house and the room went Helen Keller (that means quiet and dark, but please don’t be offended, just stay with me I promise this will make sense soon…)

Yes, I’ve had a few beers.  Full disclosure.  I had to switch to water and allow time for the electric shock paddles to bring my puny brain back online.  That was followed by a brief gasp,”oh shit?! what have I started now?”  Then some breathing exercises and knuckle crackings and now, back to the program…

So, Ryan Ephgrave (aka @EphingPosh) stepped in and dropped some mic bombs of his own.

And just like having kids, the whole thing got out ahead of me way too quick.

So, I agree with Ryan, who also added a few other suggestions like IIS logs, Chocolatey package deployments (dammit – I was hoping to beat him to that one).

So the main thing about this was that this person (no names) is entirely new to ConfigMgr.  Never seen it before, and only gets to spend a small portion of their daily/weekly time with it, due to concurrent job functions.  This is becoming more and more common everywhere I go, and I’ve blogged ad nauseum about it many times (e.g. “role compression”)

What do most small shop admins complain about?

  1. Inventory reporting
  2. Remote management tools
  3. Deploy applications
  4. Deploy updates
  5. Imaging
  6. Customizable / Extendable

These are the top (6) regardless of being ConfigMgr, LANdesk, Kace, Altiris, Solarwinds, or any other product.  All of them seem to handle most of the first 4 pretty well, with varying levels of learning and effort.  But Imaging is entirely more flexible and capable with ConfigMgr (or MDT) than any of the others I’ve seen (Acronis, Ghost, etc. etc. etc.)

ConfigMgr does an outstanding job of all 6 (even though I might bitch about number 6 in private sometimes, it is improving).  ConfigMgr is also old as dirt and battle-tested.  It scales to very large demands, and has a strong community base to back it up in all kinds of ways.  In some respects it reminds me of the years I spent with AutoCAD and Autodesk communities and the ecosystems that developed around that, but that’s another story for another time.

The challenge tends to come from just a few areas:

  1. Cost and Licensing – ConfigMgr is still aimed at medium-to-large scale customers.  The EA folks with Software Assurance, are most often interested and courted into buying it.  Some would disagree, but I set my beer mug down and calmly say “Walk into any major corporate IT office and ask who knows about ConfigMgr.  Then walk into a dentist office, car dealership, or small school system and ask that same question.”  I bet you get a different response.
  2. Complexity – ConfigMgr makes no bones about what it aims to do.  The product sprung from years of “Microsoft never lets me do what I want to manage my devices” (say that with a nasally whiny tone for optimum effect).  Microsoft responded “Here you go bitch.  A million miles of rope to hang yourself.  Enjoy!”  It’s an adjustable wrench filled with adjustable wrenches, because it was designed to be the go-to toolset for almost any environment.  And it’s still evolving today (faster than ever by the way)
  3. Administration – Anyone who’s worked with ConfigMgr knows it’s not really a “part-time” job.  But that’s okay.  It’s part of the “complexity” side-effect.  And rarely are two environments identical enough to make it cookie cutter.  That’s okay too.  Microsoft didn’t try to shoehorn us into “one way”, but said “here’s fifty ways, you choose“.  The more devices you manage with it, the more time and staff it often demands in order to do it justice.  I know plenty of environments that have scaled to the point of having dedicated staff for parts of it like App deployments, Patch Management, Imaging and even Reporting.

None of these are noted with the intention of being negative.  They are realities.  It’s like saying an NHRA dragster is loud and fast.  It’s supposed to be.

Now, add those three areas up and it makes that small office budget person lose control of their bowels and start munching bottles of Xanax.  So they start searching Google for “deploy apps to small office computers” or “patching small office computers cheap as hell” and things like that.

So, ConfigMgr already does the top 6 functions pretty darn well.  So what could be done to spin off a new sitcom version of this hit TV show for the younger generation?

  1. Simpler – It needs to be stupid-simple to install/deploy and manage.  This reaches into the UI as well.  Let’s face it, as much as I love the product, the console needs a makeover.  Simplify age-old cumbersome tasks like making queries and Collections, ADRs and so on.
  2. Lightweight – Less on-prem infrastructure requirements: DPs, MPs, SUPs, RPs, etc.  Move that into cloud roles if possible.
  3. Integrate/Refactor – Move anything which is mature (and I mean really mature) in Intune, out of ConfigMgr.  Get rid of Packages AND Applications, make a hybrid out of both.  Consider splitting some features off as premium add-ons or extensions, like Compliance Rules (or move that to Intune), OSD, Custom Reporting, Endpoint Protection, Metering, etc.
  4. Cheaper – Offer a per-node pricing model that scales down as well as up.  Users should be able to get onboard within the cost range of Office 365 models, or lower.

Basically, this sounds like Intune 3.0, which I’ve also blabbered about like some Kevin Kelly wanna-be futurist guy, but without the real ability to predict anything.

Some of the other responses on Twitter focused on ways to streamline the current “enterprise” realm, with things like automating many of the (currently) manual tasks involved with installation and initial configuration (SQL, AD, service accounts, IIS, WSUS, dependencies, etc. etc.), all of which are extremely valid points.  I’m still trying to focus on this “small shop” challenge though.

It’s really easy to stare at the ConfigMgr console and start extrapolating “what would the most basic features I could live with really come down to?” and end up picking the entire feature set in the end.  But pragmatically, it’s built to go 500 mph and slow down to push a baby stroller.  That’s a lot of range for a small shop to deal with, and they really shouldn’t.  That would be like complaining that the Gravedigger 4×4 monster truck makes for a terrible family vehicle, but it’s not supposed to be that.  And ConfigMgr really isn’t supposed to be the go-to solution for a group of 10-20 machines on a small budget.  Intune COULD be, but it’s still not there yet.  And even it is already wandering off the mud trail of simplicity.  It needs to be designed with a different mindset, but borrowing from the engine parts under the ConfigMgr hood.

Maybe, like how App-V was boiled down and strained into a bowl of Windows 10 component insertions for Office 365 enablement, and dayam that was a weird string of nouns and verbs, they could do something similar with a baked-in “device management client” in a future build of Windows 10.  Why not?  Why have to deploy anything?  They have the target product AND the management tool under the same umbrella (sort of, but I heard someone unnamed recently moved from the MDT world into the Windows 10 dev world, so I’m not that far off).

Does any of this make sense?  Let me know.


Windows 10 Soup Sandwiches

Version 2.1 (I lost Version 1.0 somehow, from the old blog, but it was focused on Windows 7, 8.1 anyway)

An ad hoc collection of wine-infused recipes to help smoosh Windows 10 like a ball of clay, or like a soggy sandwich.

Disable Windows Firewall (MDT, SCCM)

Disable Windows Firewall (GPO)

Disable Windows Defender (GPO)

Deploy .NET Framework 3.5 with Feature on Demand (GPO)

Enable Controlled Folder Access (GPO)

Create Shortcuts on Desktop, Start Menu (GPO)

Disable IPv6 (GPO)

Configure, Start, Stop Windows Services (GPO)

Block and Disable Cortana (GPO)

Set default Web Browser (GPO)

Waste Time Customizing the Start Menu and Taskbar (GPO, Script, MDT, SCCM)

Configure OEM Support info and Company Logo on Support page (GPO)

Block Windows Store and Store Apps (GPO)

Remove Store Apps during Imaging (MDT, SCCM, etc.)

Remove OneDrive from File Explorer (GPO)

Show File Extensions in File Explorer (GPO)

Show Hidden Folders and Files in File Explorer (GPO)

Show File Explorer Menu Bar (GPO)

Expand to Current Folder in File Explorer (GPO)

Customize and Push BGInfo to Desktops (script)

Customize and Push BGInfo to Desktops (GPO)

Destroy and Annihilate SMBv1 by any means necessary

TLS Configuration Guidelines (hotfix, registry, GPO)

Create and Configure a Group Policy Central Store

Updates 2.1

Add Domain User to Local Administrators Group (GPO)

Add Domain Users to Remote Desktop Users on Servers (GPO)

Modify Registry Key Permissions on Domain Computers (GPO)

Create Scheduled Tasks using Group Policy (script, GPO)

Configure PowerShell Settings using Group Policy (GPO)

Prompt for Computer Name / OSD Variable in Task Sequence (script)

Mass Upgrade Windows 10 using PowerShell (script)

Replicate MDT Boot Images to multiple WDS/PXE servers (script)

Set Google Chrome as Default Browser (GPO)

More to be added (version will be updated too)

ConfigMgr Script Deployments


The following caffeine-induced mess was the result of a quick demo session conducted with a customer about the use of the new “Scripts” feature in Configuration Manager 1710+.  There are other examples floating about the Internet which are equally good, if not better, but just finished unpacking, doing laundry, walking the dog, and needed something to do.

What is it?

The new “Scripts” feature allows you to perform “real-time” execution of PowerShell scripts against a Device Collection or individual members of a Device Collection.  It is worth noting that you cannot deploy to individual Devices from within the Devices node of the console, it only works from within, and beneath, the Device Collections node.  The script is executed on the client remotely, so the shell context is local to the remote client.  This means if you instruct the code to look at C:, it will be looking for C: on the remote device(s).

What Can You Use This For?

The answer to this question depends on your intentions and personality.  If you’re an eager workaholic, the sky is the limit.  If you’re a diabolical evil bastard with malicious thoughts, the sky is also the limit.  Is this potentially dangerous?  Yes.  But EVERYTHING in life is potentially dangerous, even brushing your teeth and going for a walk.  So weigh your risks and proceed accordingly.  I’ve provided a few examples below to illustrate some possible use cases.  Read the disclaimer before attempting to use any of them.

Preliminary Stuff

The first thing you need is to have Configuration Manager 1710 or later.  The second thing you need is to check the box to “Consent to use Pre-Release features” (Administration / Site Configuration / Sites / Hierarchy Settings / General tab).  The third thing you need (for testing anyway) is to un-check the box right below it that says “Do not allow script authors to approve their own scripts”.  If you do not un-check that option, you will be able to create script items, but you won’t be able to deploy them.

The next step is to enable the pre-release feature “Create and run scripts”:  Administration / Updates and Servicing / Features.  Right-click “Create and run scripts” and select “Turn on”. Once you’ve enabled the feature, the first time at least, you may need to close and re-open the console.  This is not always the case it seems, but I have seen this most of the time.

The Process

Create the Script

Once everything is enabled and ready to go, you should be ready to destroy, I mean, ready to begin.

  1. Select “Software Library”
  2. Select “Scripts”
  3. Select “Create Script” on the ribbon menu at top-left (or right-click and choose “Create Script”)
  4. Provide a Name
  5. Import or Paste the script code (only PowerShell is supported as of now)
  6. Tip: Make sure your script code returns an exit code of some sort to indicate success/fail to ConfigMgr (example: Write-Output 0)
  7. Click Next, Next, and Close

Approve the Script

  1. Right-click on the Script item
  2. Select Approve/Deny
  3. Click Next (I still don’t know why, but you have to, at least for now)
  4. Choose “Approve” or “Deny” and enter an “Approver comment”
    NOTE: Many organizations have procedures that require documenting approval authorization directly on the change items involved with a given change.  And to change that would require changing the way you manage change, which would require change management to effect the change and change the way you’re changing things.
  5. Click Next, Next and Close

Deploy the Script

  1. Select Assets and Compliance
  2. Select Device Collections
  3. Navigate to an appropriate device collection
    1. To deploy the script to all members, right-click on the Collection and select “Run Script”
    2. To deploy the script to individual members, select ‘Show Members’, right-click on each member (resource) and select “Run Script”
  4. Choose the approved Script from the library listbox, and click Next
  5. Click Next again (safety switch, good idea!)
  6. Watch the green bar thing slide across the progress banner a few times
  7. When it’s done, review the pretty Bar Chart.

    Select the “Bar Chart” drop-down to change reports to “Pie Chart” or “Data Table” display.
  8. Change the “Script Output” selection to “Script Exit Code” to view results by exit code values.


You can include parameter inputs within a script by including the param(…) block at the very top.  As soon as you type in param ( and then enter a variable name, like $MyParam, you should notice the ‘Script Parameters’ node appear in the left-hand panel below “Script”.  Remember to close the parentheses on param ().  This adds a new set of options that you’ll see when you click Next in the Create Script form.

This allows you to make scripts more flexible at runtime, so you can provide specific inputs as needed, rather than making a bunch of duplicate scripts with only minor variations between them.


So, here are just a few basic examples for using this feature.  You can obviously apply more brain juice to this and concoct way-more amazing awesomeness than this stuff, but here’s a taste.  These are provided “as-is” without any warranty or guarantee of fitness or function for any purposes whatsoever.  The author assumes no liability or responsibility for use,  or derivative use, of any kind in any environment on any planet in any universe for any reason whatsoever, notwithstanding, hereinafter, forthwith, batteries not included, actual results may vary, void where prohibited or taxed, past results do not indicate future performance.

Collect Client Log Files

# Collect-ClientLogs.ps1
# Modify $TargetPath to suit your needs
$SourcePath = 'C:\Windows\CCM\Logs'
$TargetPath = '\\CM01.contoso.com\ClientLogs$\'+$($env:COMPUTERNAME)
if (!(Test-Path $TargetPath)) { mkdir $TargetPath }
robocopy $SourcePath $TargetPath *.log /R:2 /W:2 /XO /MT:16
if (Test-Path $TargetPath) {
  Write-Output 0
else {
  Write-Output -1

Refresh Group Policy

# Refresh-GroupPolicy.ps1
Write-Output 0

Modify Folder Permissions

# Set-FolderPermissions.ps1
param (
  [string] $FolderPath
if (Test-Path $FolderPath) {
  ICACLS "$FolderPath" /grant 'USERS:(OI)(CI)(M)' /T /C /Q
  Write-Output 0
else {
  Write-Output -1


If you haven’t looked into this feature yet, I strongly recommend you give it a try IN A TESTING ENVIRONMENT.

How’s my driving?

Did I miss anything?  Did you find any bugs?  Let me know!

Thank you for reading!