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
  }
}

 

Advertisements

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

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

20161120_160743

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.

<#
.DESCRIPTION
.PARAMETER SourcePath
Why didn't they use PARAMETRE, hmm? So American. Anyhow, SourcePath is wherever the Windows 10 media is located.
.PARAMETER OutputPath
If not "" then it will dump a file named <computer>-<result>.txt in that location.
.NOTES
I was only half awake when I wrote this. Use at your own risk.
#>
[CmdletBinding()]
param (
    [parameter(Mandatory=$True, HelpMessage="Path to setup.exe")]
    [ValidateNotNullOrEmpty()]
    [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.

Cheers!

Dr. Skatterbrainz Answers Reader Mail

Warning: The following text may contain adult-ish offensive language which may cause unwanted side effects.  Read at your own risk.

“I was just wondering what you think about going into IT consulting for someone who’s never done it before, but who’s been working in IT direct for about ten years?” – Brad

It’s not for everyone, but some really enjoy it.  It also depends on whether you’re working from home/remote or on-site mostly.  If you’re used to being in a room full of people and a bustling office environment, then switch to being alone all the time, even with online communication tools, it’s sometimes lonely.  If you have pets or someone at home to talk to it helps, otherwise you should get outside frequently and mix with other people.  Coffee shop, park, etc.  It also requires you to impose your own control over scheduling, sleep, eating, exercise, etc.

If you prefer keeping hands-on with things after you build them, it might be tough letting go of each project and moving on to the next.  If you like having a steady office environment, that too may be a tough adjustment.  If you don’t like traveling a lot, or meeting strangers and getting used to strange places, accents, rules, customs, and so on, it may be a tough adjustment.

Other things to consider are how well you adjust to working alone, or with different teams from one day/week/month to the next, as opposed to being with the same group of people for months/years.

I would suggest that if you’re really curious/interested in consulting to give it a try.  You will be exposed to more variety and more ideas than you typically get with a steady office role.  But, no matter how it turns out, it will still be more experience, and more experience is good no matter what (as long as it doesn’t kill you or leave you brain-damaged). And you may get to rack up lots of flyer miles and hotel rewards points.

“Why do CIOs so often turn down requests from their own IT staff to improve tools and processes?” – Jim

Because most technical people suck at communicating things in terms of money.  Remember that old book “Women are from Venus. Men are from Mars”?.  CxO’s like numbers and charts.  The more colors and spiffiness (I made that term up) the better.  The situations where I’ve seen (or done by myself) a proposal laid out in terms of what it will provide in terms of the following, it got a positive result:

  • Cost savings
  • New revenue (not always welcome, unless it’s in a core competency)
  • Added capability (e.g. competitive advantage)

Any idea you have to improve things needs to be distilled down to what “improve” really means.  Improves what?  How?  For whom?  Keep walking that question back until it comes to a dollar figure.  And one other aspect I find that helps is to focus more on the repeat financial benefit, rather than a one-time benefit.  A simple one-time cost-savings doesn’t usually get them excited enough to set down the Martini and whip out the credit card, but a pay-off that keeps getting better every quarter/year is hard to ignore.  In the end, if you have to fluff the numbers to make it work, you need to ask yourself if you really have the best idea.  If it really makes sense you shouldn’t need to oversell it, but make sure you present it in the language the suit-clad folks really love to hear.

“Some of my siblings and cousins have a condescending view of the IT profession.  They’re all lawyers and marketing people, but somehow think IT is like dishwashing.  What’s the best thing I can do for that?” – Charles

Hi Charles.  I can completely sympathize.  I have a few of those people in my family as well.  You can either hold a grudge, or let it go. I prefer to let it go.  Time is your most valuable asset.  Don’t waste it.  The time you would spend on debating them could be better used on learning new skills or finding more projects to grow your experience.  Changing someone’s mind about things is almost impossible without proper firearms and pharmaceuticals.

“I’ve seen you pick on Microsoft Access a few times.  What do you hate about it?” – Chris

I don’t hate Access itself.  It’s a great product, especially for small scale needs.  But it’s not built for large-scale, shared use, and it couples the application (forms, reports, logic) with the data (tables, views, etc.) which doesn’t scale or lend itself to flexible maintenance.  It’s also very dependent on the version of Office installed, so upgrading the rest of Office then becomes hostage to it.

The other issue is that shared-use problems often lead to proliferation of multiple, standalone copies throughout the enterprise.  Maintaining consistency and centralized reporting becomes increasingly difficult.

Then when IT wants (or needs) to roll out a new version of Office, it turns into “Now hold on a minute!  That’ll break our precious Access DB ‘application’!”   The longer the Access app remains in production, the more intrinsic it becomes to business operations, making it more sensitive to disruption.  And the longer is remains in production, the more likely staff will have quit/retired/died/joined a cult, whatever, and now nobody is left who knows how to maintain or modify the code.

This often leads to the following discussion playbook scenarios, complete with the eye-rolling, mumbling, and drooling package…

Version 1

IT: “You guys in Finance need to upgrade this Access thing of yours so it works with 2016!”

Finance guys: “The guy who wrote it left months/years ago and we don’t have anyone who can update it”

IT: “Not our problem. Make it happen.”

Finance guys: “Then fuck you.”

Version 2

IT: “You guys in Finance need to upgrade this Access thing of yours so it works with 2016!”

Finance guys: “Okay, but since this is YOUR requirement, then IT should pay for all that work.”

IT: “No!”

Finance guys: “Then fuck you.”

Version 3

IT: “You guys in Finance need to upgrade this Access thing of yours so it works with 2016!”

Finance guys: “Okay, but we don’t have anyone who can do it due to schedules and other work.  Can IT do it for us?”

IT: “No!”

Finance guys: “Then fuck you.”

Version 4

IT: “You guys in Finance need to…”

Finance guys: “Just fuck you.”

Then we break for lunch, listen to IT complain about how <insert department name here> are a bunch of a-holes to work with.  We come back to the office, fighting off the carb-coma sleep monster, and repeat the same discussions again.  Someone will suggest the usual workarounds…

  • “Let’s install the Access runtime for 2007 or 2010 along with Access 2016!”
    • “Adding more complexity to our environment is not the answer.”
  • “Move it to Citrix or RDS!”
    • Citrix/RDS guy: “No!  I need funding to buy more hardware.”
  • “Let’s App-V or ThinApp it!”
    • “Carl – Do YOU know how to sequence that?”
      • “Ummm no.  But I hear it’s really cool.”
    • “You thought Justin Bieber was cool.”
      • “What’s wrong with Justin?!”
    • (continues on until some smacks the table to break it up)
  • “Let’s rent an unmarked van and kidnap that old guy that wrote this!”
    • “I have $53 on me.  Can we rent one that cheap?”
      • “I have duct tape, maybe a gift card too, hold on…”

In the end, the most expensive, least efficient and most painful “solution” will be chosen and everyone will be unhappy.  After a few months they will have left for other jobs and a new staff will be looking at it, wash rinse and repeat.  Or, in some cases, they hire someone to rewrite the app using modern tools that support shared use, are easy to maintain and even move to the cloud.

“I’d like to use Chocolatey at my company, but management won’t allow it and won’t pay for the business version.  Can I still leverage pieces of it somehow?” – Larry

There are several things you can “leverage” without using the public repository, or buying the business license features.

  • Set up an internal repository.  If there’s no objection to internal sourcing of packages.
  • Crack open Chocolatey packages for the silent installation and configuration syntax to use elsewhere.  Scripts, SCCM, etc.
  • Apply for a new job elsewhere.

Option 1 – Setting up an internal repo…

  1. Read this – https://chocolatey.org/docs/how-to-host-feed
  2. Test, test, and test some more
  3. Pilot deployment
  4. Production domination and ultimate anihilation

Option 2 – Cracking open a warm one…

  1. Locate the desired package (e.g. Microsoft Teams desktop app)
  2. Click on the “Package source” link along the left (opens the Github repo)
  3. Inspect the .nuspec file for some general details
  4. Inspect the xxxinstall.ps1 file for the code and tasty stuff
  5. Copy / adapt when you can into whatever else you’re using

Notes:

  • If you intend to “keep up” with the latest releases of a given package, you may need to repeat the above steps, or monitor the vendor source location(s) to react as they post new versions.
  • If you want to expand on this, you can post your own packages with internal modification requirements (icacls, registry hacks, etc.) as needed, or adapt them into your own deployment scripts or task sequences.

Cheers!

Send more questions via Twitter DM.  If you follow me, and your account doesn’t smell like a bot, or a weird cats-for-kids black market thing, then I will usually follow you back.

ConfigMgr Device Naming, Part 2: The Electric Boogaloo

 

1f3be4a21f5823b4516b458ba2f89a87--drink-coffee-coffee-cafe

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

Notes:

  • 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…
    set-name3
  • -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 OSD Device Naming

For whatever reasons, this still seems to be an interesting and ongoing topic of discussion/debate/argument/angst/drinking, etc.  I’m talking about assigning names to computers while they’re being imaged or refreshed.  There are quite a few common scenarios, and an unknown variety of less-common scenarios, I’m sure.  Some embrace this and some despise it.  The views are all over the place it seems.  But regardless, as a consultant, I’m very often tasked with advising (okay: consulting) customers on options for either fully or partially automating the process.

Personally, I approach all automation scenarios with the same view: Explain why you need it.  Very often, automation is simply taking over from a poorly designed manual process.  As an old colleague of mine once said (and I repeat ad nauseum) “If you automate a broken process, you can only get an automated broken process“.  It’s true.  If the customer is receptive to a discussion about alternatives, it always produces a more positive outcome.  So far, at least.

Anyhow, I wanted to lay out some common scenarios and examples for handling them.  These are based on the use of either Microsoft System Center Configuration Manager (aka ConfigMgr, aka SCCM), or Microsoft Deployment Toolkit (aka MDT).  I am by no means implying these are the ONLY way to accomplish each scenario.  They are provided simply as ONE way out of many, but which I’ve found offer the best benefit for the least effort and risk.  You are free to roll your eyes and call me a dumbass, that’s fine, because I plug my ears anyway.

Manual: Stick shift (The Exploding Space Shuttle Method)

This method requires a human to manually enter the desired name to assign to the device.  It’s the easiest/quickest method to implement.

For ConfigMgr environments, the quickest implementation is by assigning “OSDComputerName” Collection Variable to a device collection.  For bare metal (e.g. new) machines, that is often the “Unknown Computers” collection, but for refresh/reimage scenarios it’s whatever device collection is being targeted by the relevant task sequence.

Pro – easy, fast, cheap

Con – prone to human error

Fully-Automatic: Serial Number

Since Windows 10 still imposes a limit of 15 characters, with restrictions on certain alphanumeric characters, this method often includes some checking/filtering to insure that the process doesn’t swallow a grenade and make a mess.  This is less common today with physical machines, but often occurs with virtualized environments due to differing serial number formats.  This example uses the BIOS serial number as the machine name.  If the serial number is longer than 15 characters, only the last 15 characters are used.  So “CNU1234567891234” would become “NU1234567891234”.

# Set-ComputerNameAutoSN.ps1
$snmax = 15
$csn = (Get-WmiObject -Class Win32_SystemEnclosure).SerialNumber
if ($csn.Count -gt 1) { $csn = $csn[0] }
if ($csn.Length -gt $snmax) {
  $csn = $csn.Substring($csn.Length - $snmax)
}
try {
  $tsenv = New-Object -ComObject Microsoft.SMS.TSEnvironment
  $tsenv.Value("OSDComputerName") = $csn
  Write-Output 0
}
catch {
  Write-Output "not running in a task sequence environment"
}

Note: You might be wondering why the “if ($csn.Count -gt 1)…” stuff.  That’s in case the device is sitting in a dock or connected via an e-dock or dongle, which often causes WMI queries to return an array instead of a single integer for things like ChassisTypes and so on.  Checking the count of values assigned to the variable allows for retrieving just the first element in the array.

Full-Automatic: Form-Factor + Serial Number

This is a variation on the preceding example, whereby a prefix (or sometimes a suffix) is added to the name to identify a general form, such as “D” for desktop, and “L” for laptop, and so on.  This example would take a ChassisType value of 3 and the last 10 characters (arbitrary, not required) of the Serial Number of “CNU1234567891234” and concatenate these into “D4567891234”

# Set-ComputerNameAutoFormSN.ps1
$snmax = 10
$csn = (Get-WmiObject -Class Win32_SystemEnclosure).SerialNumber
$cff = (Get-WmiObject -Class Win32_SystemEnclosure).ChassisTypes
if ($csn.Count -gt 1) { $csn = $csn[0] }
if ($cff.Count -gt 1) { $cff = $cff[0] }
if ($csn.Length -gt $snmax) {
  $csn = $csn.Substring($csn.Length - $snmax)
}
switch ($cff) {
   3 { $ff = 'D'; break }
   4 { $ff = 'D'; break }
   5 { $ff = 'D'; break }
   6 { $ff = 'D'; break }
   7 { $ff = 'D'; break }
   8 { $ff = 'L'; break }
   9 { $ff = 'L'; break }
  10 { $ff = 'L'; break }
  11 { $ff = 'L'; break }
  14 { $ff = 'L'; break }
  default { $ff = 'X'; break }
}
$newname = $ff+'-'+$csn
try {
  $tsenv = New-Object -ComObject Microsoft.SMS.TSEnvironment
  $tsenv.Value("OSDComputerName") = $newname
  Write-Output 0
}
catch {
  Write-Output "you forgot to flush the toilet"
}

Semi-Automatic: Location Code + Form Factor + Serial Number

This one is based on a scenario where all devices are imaged at a single location, and shipped out to other locations, whereby the customer wishes to place the AD computer account into an associated location-based Organizational Unit (OU) during the imaging process.  This can be done using User-Driven Interface (UDI) forms, or by script.  I prefer script because I’m more comfortable with it and there are far fewer moving parts to contend with.  That said, there are several ways to do this:

Option 1 – SCCM Collection Variable

A collection variable can be assigned to the target collection in order to prompt for a location identifier.  This could be a number, a name, or an abbreviation, it doesn’t really matter since it’s just a textbox form input.

Option 2 – MDT / SCCM Task Sequence Script

Some flavors of this involve copying the ServiceUI.exe component from the MDT installation into the script package, so that ServiceUI.exe can be invoked to suppress the task sequence progress UI and allow custom script GUI forms to display properly.  I did it this way for a long time, until I ran across this really nice script from John Warnken *here*.  After some (minor) tweaking it can easily be adapted to almost any need.  The example below prompts the technician for a 3-character Location Code and then concatenates the device name using [Location]+[FormFactor]+[SerialNumber] up to 15 characters, so the [SerialNumber] value is truncated to the last 10 characters (max).

<#
Set-ComputerNameFormLocSN.ps1
.SYNOPSIS 
Displays a gui prompt for a computername usable in a SCCM OSD Task Sequence. 
 
.PARAMETER testing 
The is a switch parameter that is used to test the script outside of a SCCM Task Sequence. 
If this -testing is used the script will not load the OSD objects that are only present while a task sequence is running. 
instead it will use write-output to display the selection. 
 
.EXAMPLE 
powershell -executionpolicy Bypass -file .\Set-ComputerNameLocationPrompt.ps1 
 
.EXAMPLE 
powershell -file .\Set-ComputerNameLocationPrompt.ps1 -testing 
 
.NOTES 
This is a very simple version of a OSD prompt for a computername. You can add extra validation to the computer name, for example a regular expression test 
to ensure it meets standard form used in your environment. Addtional form object can be added to other options that you may want to set 
task sequence variables for. Also as a simple example, I just added the xaml for the wpf form as a variable in the script. You have the option of storing it in 
a external file if your form gets complex.

.Author 
Jonathan Warnken - jon.warnken (at) gmail (dot) com 
https://gallery.technet.microsoft.com/scriptcenter/Prompt-for-a-computername-6f99fa67
.Hallucinator
Skatterbrainz - @skatterbrainzz
#> 
[CmdletBinding()] 
param( 
  [parameter(Mandatory=$False)]
    [switch] $Testing
)
$snmax = 10
if(!$Testing){
  # this section provides the gluten-free, high-protein alternative to ServiceUI.exe
  $tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment  
  $tsui  = New-Object -COMObject Microsoft.SMS.TSProgressUI  
  $OSDComputername = $tsenv.Value("OSDComputername")
  $tsui.CloseProgressDialog() 
}

# query machine serial number and chassis type from WMI
$csn = Get-WmiObject -Class Win32_SystemEnclosure | Select-Object -ExpandProperty SerialNumber
$cff = Get-WmiObject -Class Win32_SystemEnclosure | Select-Object -ExpandProperty ChassisTypes

# check if return values are an array and if true, then only use the first element
if ($csn.Count -gt 1) { $csn = $csn[0] }
if ($cff.Count -gt 1) { $cff = $cff[0] }

# if serial number is longer than 10 chars, get last 10 chars only
if ($csn.Length -gt $snmax) {
  $csn = $csn.Substring($csn.Length - $snmax)
}
# derive form factor code from chassis type code
switch ($cff) {
   3 { $ff = 'D'; break }
   4 { $ff = 'D'; break }
   5 { $ff = 'D'; break }
   6 { $ff = 'D'; break }
   7 { $ff = 'D'; break }
   8 { $ff = 'L'; break }
   9 { $ff = 'L'; break }
  10 { $ff = 'L'; break }
  11 { $ff = 'L'; break }
  14 { $ff = 'L'; break }
  default { $ff = 'X'; break }
}
[xml]$XAML = @' 
<Window 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  Title="SCCM OSD Computername" Height="154" Width="425" Topmost="True" WindowStyle="ToolWindow"> 
  <Grid> 
    <Label Name="Computername_label" Content="Location:" HorizontalAlignment="Left" Height="27" Margin="0,10,0,0" VerticalAlignment="Top" Width="241"/> 
    <TextBox Name="LocationCode_text" HorizontalAlignment="Left" Height="27" Margin="146,10,0,0" TextWrapping="Wrap" Text=" " VerticalAlignment="Top" Width="220"/> 
    <Button Name="Continue_button" Content="Continue" HorizontalAlignment="Left" Margin="201,62,0,0" VerticalAlignment="Top" Width="75"/> 
  </Grid> 
</Window> 
'@ 
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework') 
#Read XAML 
$reader=(New-Object System.Xml.XmlNodeReader $xaml) 
$Form=[Windows.Markup.XamlReader]::Load( $reader )

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

# set the default value for the form textbox
$LocationCode_text.Text = 'STL'

# assign event handler to the "Continue" button
$Continue_button.add_Click({ 
  $Script:LocationCode = $LocationCode_text.Text.ToString()
  $Form.Close() 
})
# display the form for user input
$Form.ShowDialog() | Out-Null

# after form is closed, concatenate computer name from input values and other variables
$computername = $Script:LocationCode+$ff+$csn

if (!$Testing) {
  $tsenv.Value("OSDComputername") = $computername 
  Write-Output " OSDComputername set to $($tsenv.value("OSDComputername"))" 
}
else { 
  Write-Output " OSDComputername would be set to $computername" 
}

Pro – works with PXE and offline media

Con – some scripting and testing required

Fully Automatic: Location Code + Form Factor + Serial Number

This method relies on something available from the environment upon which to determine the “location”.  For MDT this is often using the CustomSettings.ini with the IP Gateway address.  Using a logical if/then you can construct a logic mapping of IP gateway = Location or location Code.

Option 1 – MDT Deployment Share Configuration

This one uses a CustomSettings.ini which reads the IPv4 Gateway address and the Serial Number to concatenate the resultant device name.  For a better example of this and a much, much better explanation by the one-and-only Mikael Nystrom, please read this first.

[Settings]
Priority=Init, ByDesktop, ByLaptop, DefaultGateway, Default 
Properties=ComputerLocationName, ComputerTypeName, ComputerSerialNumber

[Init]
ComputerSerialNumber=#Right("%SerialNumber%",10)#

[ByLaptop]
SubSection=Laptop-%IsLaptop%
ComputerTypeName=L

[ByDesktop]
SubSection=Desktop-%IsDesktop%
ComputerTypeName=D

[DefaultGateway]
192.168.1.1=Norfolk
192.168.2.1=Chicago
192.168.3.1=NewYork

[Norfolk]
ComputerLocationName=NOR

[Chicago]
ComputerLocationName=CHI

[NewYork]
ComputerLocationName=NYC

[Default]
OSInstall=Y
ComputerLocationName=XXX
ComputerTypeName=X
OSDComputerName=%ComputerLocationName%%ComputerTypeName%%ComputerSerialNumber%
SkipCapture=YES
SkipAdminPassword=YES
SkipProductKey=YES
SkipComputerBackup=YES
SkipBitLocker=YES

Using this sample INI configuration, a laptop device with Serial Number “CNU1234567891234” connected to IP gateway 192.168.3.1 would end up with an assigned name of “NYCL4567891234”.  If the DefaultGateway is not defined in the list, it will default to location code “XXX”.  If the form factor cannot determine Desktop or Laptop it will default to “X”.  So, using the same serial number value it would end up as “XXXX4567891234”.

Option 2 – SCCM Task Sequence

This example adapts the semi-automatic example above, but replaces the GUI prompt with a check for the IP gateway.

# Set-ComputerNameAutoLocIPFormFactorSN.ps1
$csn = (Get-WmiObject -Class Win32_SystemEnclosure).SerialNumber
$cff = (Get-WmiObject -Class Win32_SystemEnclosure).ChassisTypes
if ($csn.Count -gt 1) { $csn = $csn[0] }
if ($cff.Count -gt 1) { $cff = $cff[0] }
if ($csn.Length -gt 15) {
  $csn = $csn.Substring($csn.Length - 15)
}
$gwa = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where {$_.IPEnabled -eq $True}).DefaultIPGateway
switch ($gwa) {
  '192.168.1.1' {
    $location = 'Norfolk'
    $loccode = 'NOR'
    break
  }
  '192.168.2.1' {
    $location = 'Chicago'
    $loccode = 'CHI'
    break
  }
  default {
    $location = 'NewYork'
    $loccode = 'NYC'
    break
  }
}
switch ($cff) {
   3 { $ff = 'D'; break }
   4 { $ff = 'D'; break }
   5 { $ff = 'D'; break }
   6 { $ff = 'D'; break }
   7 { $ff = 'D'; break }
   8 { $ff = 'L'; break }
   9 { $ff = 'L'; break }
  10 { $ff = 'L'; break }
  11 { $ff = 'L'; break }
  14 { $ff = 'L'; break }
  default { $ff = 'X'; break }
}
$newName = $loccode+$ff+$csn
try {
  $tsenv = New-Object -ComObject Microsoft.SMS.TSEnvironment
  $tsenv.Value("OSDComputerName") = $newName
  Write-Output 0
}
catch {
  Write-Output "not running in a task sequence environment"
}

Option 3 – SCCM Task Sequence + Collection Variable

This example came from a project where the customer had created a device collection for each branch location, which had a Collection Variable “DeviceLoc” assigned, and having deployed a refresh task sequence to each.  So in this scenario, they wanted to merge the script with the collection variable to derive the new name.  Is this ideal?  Not for most people, but due to time constraints it had to work.

$csn = (Get-WmiObject -Class Win32_SystemEnclosure).SerialNumber
$cff = (Get-WmiObject -Class Win32_SystemEnclosure).ChassisTypes
if ($csn.Count -gt 1) { $csn = $csn[0] }
if ($cff.Count -gt 1) { $cff = $cff[0] }
if ($csn.Length -gt 15) {
  $csn = $csn.Substring($csn.Length - 15)
}
$gwa = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where {$_.IPEnabled -eq $True}).DefaultIPGateway
switch ($gwa) {
  '192.168.1.1' {
    $location = 'Norfolk'
    $loccode = 'NOR'
    break
  }
  '192.168.2.1' {
    $location = 'Chicago'
    $loccode = 'CHI'
    break
  }
  default {
    $location = 'NewYork'
    $loccode = 'NYC'
    break
  }
}
switch ($cff) {
   3 { $ff = 'D'; break }
   4 { $ff = 'D'; break }
   5 { $ff = 'D'; break }
   6 { $ff = 'D'; break }
   7 { $ff = 'D'; break }
   8 { $ff = 'L'; break }
   9 { $ff = 'L'; break }
  10 { $ff = 'L'; break }
  11 { $ff = 'L'; break }
  14 { $ff = 'L'; break }
  default { $ff = 'X'; $ousub = ''; break }
}
try {
  $tsenv = New-Object -ComObject Microsoft.SMS.TSEnvironment
  $loc = $tsenv.Value("DeviceLoc")
  if ([string]::IsNullOrEmpty($loc)) {
    $newName = 'CRP'+$ff+$csn
  }
  else {
    $newName = $loc+$ff+$csn
  }
  $tsenv.Value("OSDComputerName") = $newName
  Write-Output 0
}
catch {
  Write-Output "not running in a task sequence environment"
}

Conclusion / Summary

This is only a small sample of all the variations and scenarios many of us encounter.  Other variants include setting the Organizational Unit (OU) path, adding the device to domain security groups, and so on.  I would like to thank John Warnken for the script code he posted on TechNet, which makes it so much easier to deal with the progress bar UI when using custom forms.

I’d also like to say that you shouldn’t look at these and think you need to immediately put them into use.  Test labs are one thing.  But production requires serious review and planning to determine what really makes the most sense.  For example, a lot of customers I work with have persistently used a location code for device names, but when they really assess the business environment, they find many devices roam to different locations, or don’t really stay in any “official” location (home, hotel, on the road, etc.)  And when pressed to explain how the location code provides real value, many don’t have a rationale besides, “it’s how we’ve always done it”.  So think it through.  Ask if you really need to do something like this and if so, why.  Then work on the how aspects.

Do you have other/better examples you’d like to share?  Post a comment below.  Thank you for reading!