ConfigMgr Anxieties

dmvidpics-2017-02-20-at-13-46-18

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

Beer-level:

  • 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

Wine-level:

  • 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

Advice:

  • 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

Wine-Cooler-level:

  • 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

Bourbon-level:

  • 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

Advice:

  • 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

RedBull-level:

  • 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

FourLoco-level:

  • 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

Advice:

  • 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

KoolAid-level:

  • 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

Scotch-level:

  • 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.

Advice:

  • 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

SawDust-level:

  • 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

Cheerios-level:

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

Advice:

  • 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

Bush-level:

  • 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

Tree-level:

  • 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.

Advice:

  • 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

Water-level:

  • 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

Steam-level:

  • 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

Advice:

  • 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!

Advertisements

Funny, but Not Too Funny

Accicents_01

Warning: Long-winded, rambling nonsense, sprinkled with pathetic humor and pseudo-metaphorical fortune cookie wisdom…

I ran into an old friend recently, and being in our 50’s, we soon gravitated towards reminiscing of old times, old friends, and so on.  One of the topics we discussed was about a girl from our neighborhood, named Laurie.  She was about 5 years younger than the rest of us, so we thought of her always as a “kid”.  She had two sisters and a brother, and two parents who drank heavily.  I mean heav-il-eeee.  Like mom would be dancing with the light pole (waltz style, not stripper style) at 3am in their front yard.  Dad would throw things through the kitchen window into the front yard on occasion (appliances, kids, whatever).

Laurie died of a heroin overdose a few years ago.  It turned out her childhood was worse than we had known, mostly from her dad, but later on from her social circles.  I can’t imagine what she had experienced, or how she must have tried to cope with it.  As irrational as it is, I still think of her as a child, trying to ride the little bicycle with training wheels.  We lost touch with her as each of us fanned out on our own journeys, meeting up again only to learn just how badly her life had turned out.

Sadly, her story isn’t unique.  The more I travel, and the more people I meet, I’ve come to realize that it’s pervasive, and that “hope” is what seems to be a dying species.  Some seek it in religion, drugs, drinking, new social circles, thrill-seeking, or just sitting in a dark room and hoping it will pass.  And some just stop looking for it.

It doesn’t take a massive tragedy, a bad childhood, or even a bad environment, to wind up in a downward spiral.  It can be any sudden, unexpected, change to life’s patterns:  Family, friends, pets, jobs, housing, transportation, bills, medical issues, etc.  Just about anything.

Shit.  I fight back tears every time one of our goldfish dies, even while I’m eating a tuna sandwich.  Talk about having mixed feelings.

We build our lives on patterns, and our biological systems adapt to those patterns, chemically, structurally.  Patterns are both controlled by our behaviors, as they control our behaviors.  It’s tightly connected.  Ask any doctor, clergy, counselor, or EMT.

Me, personally, I’ve had a few challenges.  Cancer, loss of parents, siblings who no longer communicate, losses of very close friends, children growing up and moving away, job changes, job losses, financial collapse, homelessness, etc. etc.  There were times when thoughts of pulling the plug danced around my empty cranium, but I always thought about how that would impact others I care about.  I dug in, climbed back up and never stopped.  I’ve fallen down a few times since, so I don’t want to make it sound like you can only hit bottom once.  And the bottom I hit is nowhere near as far down as others.  I never let myself get comfortable, because I’ve seen the tables flipped over too many times.  I keep a MRI photo of my brain tumor near my desk, alongside my “rock star” employee award, from a company that laid me off, both as a idiot’s version of a memento mori.

Four things will defeat most kinds of depression or sadness:  Diet, Exercise, Humor and Pets.

Diet – Cut out the junk food.  Cut back, or eliminate, sugar.   Go Keto if you want, or just anything that works for you actually.  But measure everything before you start.  Weight, blood work, etc.  Know your stats, or you will never know what really works in the end.  And don’t watch the scales every day.  Focus on the plan and check back every few weeks.  Seeing 2-3 lbs drop after a week can be discouraging to some, but seeing 10-12 lbs drop after a few weeks is more motivating.

Exercise – do anything.  You don’t need to join a gym, but it’s not a bad idea if you can afford it.  I work out alone.  I don’t have an expensive gym, just resistance stuff.  You’d be amazed what that can do.  YouTube has tons of great workout examples that don’t require any equipment at all.

Humor – make yourself laugh.  Driving in your car?  Practice your impressions of other people saying ridiculous things.  Read the American Pledge of Allegiance as your absolute best Arnold Schwarzenegger.  Watch a few silly videos.  Works great every time.

Pets – get a dog, cat, horse, or anything you can hug and squeeze, and then smooch the living shit out of it at least twice a day.  Every minute you hug and squeeze them, it increases your levels of awesomeness.  That’s actually no joke.  If you have a dog, feed them something harmless, but which gives them gas for a while.  I guarantee it will make you forget anything else you had on your mind.  I’m a firm believer that personal issues are no match for a strong odor.

Other tips:

  • Take your worst (non-medical) problem right now, and pretend you’re explaining it to a group of 6 year old children in a remote village in South Sudan.  They haven’t eaten for days, most of their parents are dead, they only have brown water to drink and bathe in, and they’re surrounded by flies. See if you can make it more than one minute before realizing how silly your problems (often) seem.
  • Go sit in a Walmart on a Saturday for one hour.  Do not look at your phone even once.  Just watch the people.
  • Read Calvin and Hobbes, any Matt Groening books (The Big Book of Hell is good for starters), or Dave Barry books.
  • Tune out the “news”.  It’s not really “news” anyway.
  • Do something unexpectedly nice for a stranger.  Pay for someone’s meal behind you in a fast food drive-thru line.

But most importantly: be nice to each other.  Even that asshole working for the other team at work.  We don’t know what he/she is going through.

This.  This – is why I chose the path I’ve taken on social media.  It’s not a path for some of you, and that’s perfectly fine.  I’m not selling anything.  I’m not trying to win any awards.  There are more than enough experts, probably you included, to satisfy all our technical needs.  But so often, we spend too much time behind a screen and keyboard, and we end up feeling empty.  Lack of sleep.  Stress. Poor diet and no exercise, missing old friends, all of which can gradually gang up on us.

I don’t know how this ties back to Laurie, but I suppose it might be something about being at a low point in life, and not knowing where to turn.  Maybe not knowing if you should turn at all.  Most importantly, knowing when a situation is within your control to change it.

I’m here, doing my part to remind you that life is still out there, beyond the screen, and it’s still just as stupid and wonderful as you remembered it being, before you learned what a computer was for.  If you don’t need that, that’s even better.  So, you keep on being smart, and genius, and I’ll handle the stupid.  The world needs you.

Namaste

 

This message brought to you by a Tony Robbins imitator who drinks way too much coffee.

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.

Enjoy!

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”

[CmdletBinding()]
param (
  [parameter(Mandatory=$False)]
  [ValidateNotNullOrEmpty()]
  [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.

[CmdletBinding()]
param (
  [parameter(Mandatory=$False, HelpMessage="CSV input file path")]
    [string] $CsvFile = "",
  [parameter(Mandatory=$False, HelpMessage="Form Title")]
    [ValidateNotNullOrEmpty()]
    [string] $FormTitle = "Contoso - Provision Device",
  [parameter(Mandatory=$False, HelpMessage="Maximum UserName character length")]
    [ValidateRange(1,15)]
    [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 (
    [parameter(Mandatory=$True)]
    [ValidateNotNullOrEmpty()]
    [string] $TargetOU
  )
  $ComputerName = $env:COMPUTERNAME
  $ads=[adsi]''
  $adssearch = New-Object DirectoryServices.DirectorySearcher
  $adssearch.searchroot = $ads
  $adssearch.filter="(objectclass=computer)"
  $adc1 = $adssearch.findall() | Where-Object {$_.properties.item("cn") -like $ComputerName}
  $ComputerDN = $adc1.properties.item("distinguishedname")
  Write-Verbose "distinguishedName = $ComputerDN"
  $adc = [adsi]"LDAP://$ComputerDN"
  $targetOU="LDAP://$targetOU"
  Write-Verbose "target path = $targetOU"
  $adc.psbase.MoveTo($targetOU)
}

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

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

[xml]$XAML = @' 
<Window 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  Title="" 
  Height="200" Width="320" Topmost="True" WindowStyle="ToolWindow" 
  WindowStartupLocation="Manual" Top="200" Left="200" 
  FocusManager.FocusedElement="{Binding ElementName=ComputerName_text}"> 
  <Grid> 
    <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">
    </ComboBox>
    <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"/> 
  </Grid>
</Window> 
'@
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework') 

# 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)"
  $Combo_Loc.AddChild($LocDept)
}

$Form.Title = $FormTitle
$Text_User.Maxlength = $MaxUserNameLength
if (!($IgnoreCase)) {
  $Text_User.CharacterCasing = "Upper"
}
# add form handler for pressing Enter on UserName text box
$Text_User.add_KeyDown({
  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
    }
    $Form.Close() 
  }
})
# add form handler for clicking Continue button on exit
$Button_Continue.add_Click({
  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
  }
  $Form.Close() 
})
# 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."
  break
}

$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.

Loc,Dept,DeviceOU,ADGroup
BOS,HR,"OU=Workstations,OU=HR,OU=Boston,DC=Contoso,DC=local",
BOS,RD,"OU=Workstations,OU=Research,OU=Boston,DC=Contoso,DC=local",
MIA,HR,"OU=Workstations,OU=HR,OU=Miami,DC=Contoso,DC=local",
MIA,MK,"OU=Workstations,OU=Marketing,OU=Miami,DC=Contoso,DC=local",
SFO,FN,"OU=Workstations,OU=Finance,OU=SanFrancisco,DC=Contoso,DC=local",
SFO,HR,"OU=Workstations,OU=HR,OU=SanFrancisco,DC=Contoso,DC=local",
SFO,RD,"OU=Workstations,OU=Research,OU=SanFrancisco,DC=Contoso,DC=local",
TMP,HR,"OU=Workstations,OU=HR,OU=Tampa,DC=Contoso,DC=local",

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!”

[CmdletBinding()]
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 2.8.5.201 -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…

[CmdletBinding()]
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 
  $srv.Configuration.Alter()
}

Set CM Database Recovery Model to Simple

[CmdletBinding()]
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)

[CmdletBinding()]
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

[CmdletBinding()]
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

[CmdletBinding()]
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

[CmdletBinding()]
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."
  break
}
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

[CmdletBinding()]
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)

[CmdletBinding()]
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
  $AdObj.psbase.ObjectSecurity.AddAccessRule($ACE)
  $AdObj.psbase.commitchanges()
}
catch {
  Write-Error $_.Exception.Message
}

Import Windows 10 OS Image (added in 1.1)

[CmdletBinding()]
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."
  break
}
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)

[CmdletBinding()]
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."
  break
}
try {
  New-CMOperatingSystemInstaller -Name $OSName -Path $ImageSource -Description $OSName -ErrorAction Stop
}
catch {
  Write-Error $_.Exception.Message
}

Create a Console Folder (added in 1.1)

[CmdletBinding()]
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")]
  [ValidateSet('Application','BootImage','ConfigurationBaseline','ConfigurationItem','DeviceCollection','Driver','DriverPackage','OperatingSystemImage','OperatingSystemInstaller','Package','Query','SoftwareMetering','SoftwareUpdate','TaskSequence','UserCollection','UserStateMigration','VirtualHardDisk')]
  [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

[CmdletBinding()]
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."
  break
}
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
  }
}

 

Things that make me nervous

  1. Cops following me for a long time
  2. Doctor says “I think we need to do more tests”
  3. Dentist (looks at x-ray and just shakes head)
  4. Someone pointing a gun at my face
  5. Only one open restroom and it’s out of toilet tissue on taco lunch day
  6. Driving in Boston around the holidays
  7. A spider walking on my face
  8. A TSA person who stares at me without blinking while meticulously stretching latex gloves
  9. Something running around inside the refrigerator
  10. Seeing “Jason Sandys has replied to your post”