Set Computer Name in SCCM Task Sequence by identifying the computer by its serial number (serviceTag) instead of SMBios or MAC
To mimic MDT behavior (and yes we are using Dell computers)
[edited 8. 7. 2021] - Added information that Administration Service supports connections through CMG ๐
In case you have Dell computers in your environment and you are using SCCM for OSD, you've probably noticed, that serial number isn't used for identifying devices during OSD, but instead, SMBIOS identifier is. This is a pity because Dell's computer serial number (service tag) doesn't change even in the case of hardware replacement.
As you probably know, it's all about setting OSDComputerName
Task Sequence variable during OSD.
Therefore I've created two separate solutions, that's purpose is to set OSDComputerName
variable based on computer Serial Number
- Dynamic, that leverages SCCM Administration Service (REST API) to get computer name
- Static, that uses Task Sequence step calling PowerShell code with a hardcoded list of computers serial numbers and corresponding names (but this content itself is dynamically generated via PowerShell function)
I personally combine these two solutions, to support CMG installation and on-premise installation at once.
What interesting stuff will we use?
- Getting information from SCCM REST API
- Modification of SCCM Task Sequence step using PowerShell
Table of Contents
- 1. Set OSDComputerName variable by getting information from SCCM Administration Service (REST API)
- 2. Set OSDComputerName variable using PowerShell script that is generated by another PowerShell script ๐
- Summary
1. Set OSDComputerName variable by getting information from SCCM Administration Service (REST API)
Requirements
- Administration Service has to be accessible from WinPE
- To make this work over internet (CMG) follow this post about enabling Administration Service over CMG. Otherwise Administration Service cannot be used outside of your on-premise environment
- SCCM User account with READ permission to needed device data stored in Administration Service
Create Administration Service READ account for device name retrieval
To be able to use Administrative Service safely, we have to grant some service account permission to read just device information
1. Create new Security Role in SCCM
Create new Security Role and grant this role only following permissions
2. Add Domain User to previously created Security Role
To add a user to created Security Role, right-click Administrative Users in SCCM console and choose the user you want to grant the permission
Add this user to the previously created Security Role.
3. Download & Import & Use Task Sequence "SET OSDCOMPUTERNAME BASED ON SERIAL NUMBER"
- Download my Task Sequence
- Import it to your list of Task Sequences in SCCM
- Use it as a step in your own OSD Task Sequence
Of course, you have to use in BEFORE step "Apply Operating System"
4. Modify the PowerShell code to match your environment
There are three variables, that have to be modified! Check region
CHANGE THIS TO MATCH YOUR ENVIRONMENT
Variables $u and $p contains credentials of the service account we granted access to the SCCM Administration Service before.
PS: to avoid placing account password in plaintext, you can use for example SCCM Collection Variable, to store it
PowerShell code used in this Task Sequence step
# getting hostname based on device serial number from SCCM REST API
#region CHANGE THIS TO MATCH YOUR ENVIRONMENT
$sccmServer = "nameOfYourSCCMServer"
# credentials for accessing REST API
$u = 'CONTOSO\accountWithPermToRESTApi'
$p = 'accountPassword'
#endregion CHANGE THIS TO MATCH YOUR ENVIRONMENT
$errorActionPreference = "Stop"
function Invoke-CMAdminServiceQuery {
<#
.SYNOPSIS
Function for retrieving information from SCCM Admin Service REST API.
Will connect to API and return results according to given query.
Supports local connection and also internet through CMG.
.DESCRIPTION
Function for retrieving information from SCCM Admin Service REST API.
Will connect to API and return results according to given query.
Supports local connection and also internet through CMG.
Use credentials with READ rights on queried source at least.
For best performance defined filter and select parameters.
.PARAMETER ServerFQDN
For intranet clients
The fully qualified domain name of the server hosting the AdminService
.PARAMETER Source
For specifying what information are we looking for. You can use TAB completion!
Accept string representing the source in format <source>/<wmiclass>.
SCCM Admin Service offers two base Source:
- wmi = for WMI classes (use it like wmi/<className>)
- examples:
- wmi/ = list all available classes
- wmi/SMS_R_System = get all systems (i.e. content of SMS_R_System WMI class)
- wmi/SMS_R_User = get all users
- v1.0 = for WMI classes, that were migrated to this new Source
- example v1.0/ = list all available classes
- example v1.0/Application = get all applications
.PARAMETER Filter
For filtering the returned results.
Accept string representing the filter statement.
Makes query significantly faster!
Examples:
- "name eq 'ni-20-ntb'"
- "startswith(Name,'Drivers -')"
Usable operators:
any, all, cast, ceiling, concat, contains, day, endswith, filter, floor, fractionalseconds, hour, indexof, isof, length, minute, month, round, second, startswith, substring, tolower, toupper, trim, year, date, time
https://docs.microsoft.com/en-us/graph/query-parameters
.PARAMETER Select
For filtering returned properties.
Accept list of properties you want to return.
Makes query significantly faster!
Examples:
- "MACAddresses", "Name"
.PARAMETER ExternalUrl
For internet clients
ExternalUrl of the AdminService you wish to connect to. You can find the ExternalUrl by directly querying your CM database.
Query: SELECT ProxyServerName,ExternalUrl FROM [dbo].[vProxy_Routings] WHERE [dbo].[vProxy_Routings].ExternalEndpointName = 'AdminService'
It should look like this: HTTPS://<YOURCMG>.<FQDN>/CCM_Proxy_ServerAuth/<RANDOM_NUMBER>/AdminService
.PARAMETER TenantId
For internet clients
Azure AD Tenant ID that is used for your CMG
.PARAMETER ClientId
For internet clients
Client ID of the application registration created to interact with the AdminService
.PARAMETER ApplicationIdUri
For internet clients
Application ID URI of the Configuration manager Server app created when creating your CMG.
The default value of 'https://ConfigMgrService' should be good for most people.
.PARAMETER BypassCertCheck
Enabling this option will allow PowerShell to accept any certificate when querying the AdminService.
If you do not enable this option, you need to make sure the certificate used by the AdminService is trusted by the device.
.EXAMPLE
Invoke-CMAdminServiceQuery -Source "wmi/SMS_R_SYSTEM" -Filter "name eq 'ni-20-ntb'" -Select MACAddresses
.EXAMPLE
Invoke-CMAdminServiceQuery -Source "wmi/SMS_R_SYSTEM" -Filter "startswith(Name,'AE-')" -Select Name, MACAddresses
.NOTES
!!!Credits goes to author of https://github.com/CharlesNRU/mdm-adminservice/blob/master/Invoke-GetPackageIDFromAdminService.ps1 (I just generalize it and made some improvements)
Lot of useful information https://www.asquaredozen.com/2019/02/12/the-system-center-configuration-manager-adminservice-guide
#>
[CmdletBinding()]
param(
[parameter(Mandatory = $false, HelpMessage = "Set the FQDN of the server hosting the ConfigMgr AdminService.", ParameterSetName = "Intranet")]
[ValidateNotNullOrEmpty()]
[string] $ServerFQDN = $_SCCMServer
,
[Parameter(Mandatory = $true)]
[ValidateScript( {
If ($_ -match "(^wmi/)|(^v1.0/)") {
$true
} else {
Throw "$_ is not a valid source (for example: wmi/SMS_Package or v1.0/whatever"
}
})]
[ArgumentCompleter( {
param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
$source = ($WordToComplete -split "/")[0]
$class = ($WordToComplete -split "/")[1]
Invoke-CMAdminServiceQuery -Source "$source/" | ? { $_.url -like "*$class*" } | select -exp url | % { "$source/$_" }
})]
[string] $Source
,
[string] $Filter
,
[string[]] $Select
,
[parameter(Mandatory = $true, HelpMessage = "Set the CMG ExternalUrl for the AdminService.", ParameterSetName = "Internet")]
[ValidateNotNullOrEmpty()]
[string] $ExternalUrl
,
[parameter(Mandatory = $true, HelpMessage = "Set your TenantID.", ParameterSetName = "Internet")]
[ValidateNotNullOrEmpty()]
[string] $TenantID
,
[parameter(Mandatory = $true, HelpMessage = "Set the ClientID of app registration to interact with the AdminService.", ParameterSetName = "Internet")]
[ValidateNotNullOrEmpty()]
[string] $ClientID
,
[parameter(Mandatory = $false, HelpMessage = "Specify URI here if using non-default Application ID URI for the configuration manager server app.", ParameterSetName = "Internet")]
[ValidateNotNullOrEmpty()]
[string] $ApplicationIdUri = 'https://ConfigMgrService'
,
[parameter(Mandatory = $false, HelpMessage = "Specify the credentials that will be used to query the AdminService.", ParameterSetName = "Intranet")]
[parameter(Mandatory = $true, HelpMessage = "Specify the credentials that will be used to query the AdminService.", ParameterSetName = "Internet")]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.PSCredential] $Credential
,
[parameter(Mandatory = $false, HelpMessage = "If set to True, PowerShell will bypass SSL certificate checks when contacting the AdminService.", ParameterSetName = "Intranet")]
[parameter(Mandatory = $false, HelpMessage = "If set to True, PowerShell will bypass SSL certificate checks when contacting the AdminService.", ParameterSetName = "Internet")]
[bool]$BypassCertCheck = $false
)
Begin {
#region functions
function Get-AdminServiceUri {
If ($ServerFQDN) {
Return "https://$($ServerFQDN)/AdminService"
}
If ($ExternalUrl) {
Return $ExternalUrl
}
}
function Import-MSALPSModule {
Write-Verbose "Checking if MSAL.PS module is available on the device."
$MSALModule = Get-Module -ListAvailable MSAL.PS
If ($MSALModule) {
Write-Verbose "Module is already available."
} Else {
#Setting PowerShell to use TLS 1.2 for PowerShell Gallery
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Verbose "MSAL.PS is not installed, checking for prerequisites before installing module."
Write-Verbose "Checking for NuGet package provider... "
If (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
Write-Verbose "NuGet package provider is not installed, installing NuGet..."
$NuGetVersion = Install-PackageProvider -Name NuGet -Force -ErrorAction Stop | Select-Object -ExpandProperty Version
Write-Verbose "NuGet package provider version $($NuGetVersion) installed."
}
Write-Verbose "Checking for PowerShellGet module version 2 or higher "
$PowerShellGetLatestVersion = Get-Module -ListAvailable -Name PowerShellGet | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version
If ((-not $PowerShellGetLatestVersion)) {
Write-Verbose "Could not find any version of PowerShellGet installed."
}
If (($PowerShellGetLatestVersion.Major -lt 2)) {
Write-Verbose "Current PowerShellGet version is $($PowerShellGetLatestVersion) and needs to be updated."
}
If ((-not $PowerShellGetLatestVersion) -or ($PowerShellGetLatestVersion.Major -lt 2)) {
Write-Verbose "Installing latest version of PowerShellGet..."
Install-Module -Name PowerShellGet -AllowClobber -Force
$InstalledVersion = Get-Module -ListAvailable -Name PowerShellGet | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version
Write-Verbose "PowerShellGet module version $($InstalledVersion) installed."
}
Write-Verbose "Installing MSAL.PS module..."
If ((-not $PowerShellGetLatestVersion) -or ($PowerShellGetLatestVersion.Major -lt 2)) {
Write-Verbose "Starting another powershell process to install the module..."
$result = Start-Process -FilePath powershell.exe -ArgumentList "Install-Module MSAL.PS -AcceptLicense -Force" -PassThru -Wait -NoNewWindow
If ($result.ExitCode -ne 0) {
Write-Verbose "Failed to install MSAL.PS module"
Throw "Failed to install MSAL.PS module"
}
} Else {
Install-Module MSAL.PS -AcceptLicense -Force
}
}
Write-Verbose "Importing MSAL.PS module..."
Import-Module MSAL.PS -Force
Write-Verbose "MSAL.PS module successfully imported."
}
#endregion functions
}
Process {
Try {
#region connect Admin Service
Write-Verbose "Processing credentials..."
switch ($PSCmdlet.ParameterSetName) {
"Intranet" {
If ($Credential) {
If ($Credential.GetNetworkCredential().password) {
Write-Verbose "Using provided credentials to query the AdminService."
$InvokeRestMethodCredential = @{
"Credential" = ($Credential)
}
} Else {
throw "Username provided without a password, please specify a password."
}
} Else {
Write-Verbose "No credentials provided, using current user credentials to query the AdminService."
$InvokeRestMethodCredential = @{
"UseDefaultCredentials" = $True
}
}
}
"Internet" {
Import-MSALPSModule
Write-Verbose "Getting access token to query the AdminService via CMG."
$Token = Get-MsalToken -TenantId $TenantID -ClientId $ClientID -UserCredential $Credential -Scopes ([String]::Concat($($ApplicationIdUri), '/user_impersonation')) -ErrorAction Stop
Write-Verbose "Successfully retrieved access token."
}
}
If ($BypassCertCheck) {
Write-Verbose "Bypassing certificate checks to query the AdminService."
#Source: https://til.intrepidintegration.com/powershell/ssl-cert-bypass.html
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3, [Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls12
}
#endregion connect Admin Service
#region make&execute query
$URI = (Get-AdminServiceUri) + "/" + $Source
$Body = @{}
if ($Filter) {
$Body."`$filter" = $Filter
}
if ($Select) {
$Body."`$select" = ($Select -join ",")
}
switch ($PSCmdlet.ParameterSetName) {
'Intranet' {
Invoke-RestMethod -Method Get -Uri $URI -Body $Body @InvokeRestMethodCredential | Select-Object -ExpandProperty value
}
'Internet' {
$authHeader = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer " + $token.AccessToken
'ExpiresOn' = $token.ExpiresOn
}
$Packages = Invoke-RestMethod -Method Get -Uri $URI -Headers $authHeader -Body $Body | Select-Object -ExpandProperty value
}
}
#endregion make&execute query
} Catch {
throw "Error: $($_.Exception.HResult)): $($_.Exception.Message)`n$($_.InvocationInfo.PositionMessage)"
}
}
}
if (Test-Connection $sccmServer -Quiet) {
[securestring]$credP = ConvertTo-SecureString $p -AsPlainText -Force
[pscredential]$cred = New-Object System.Management.Automation.PSCredential ($u, $credP)
$serviceTag = (Get-WmiObject -Class WIN32_BIOS).SerialNumber
$resourceId = Invoke-CMAdminServiceQuery -ServerFQDN $sccmServer -Credential $cred -Source "wmi/SMS_G_System_SYSTEM_ENCLOSURE" -Filter "SerialNumber eq '$serviceTag'" -Select ResourceID | select -exp ResourceID
$hostname = Invoke-CMAdminServiceQuery -ServerFQDN $sccmServer -Credential $cred -Source "wmi/SMS_R_SYSTEM" -Filter "ResourceId eq $resourceId" -Select Name | select -exp Name
# save the computername to TS variable
if ($hostname) { return $hostname }
}
The code will connect to SCCM Administration Service, search for name of the device identified by extracted serial number and return this name, so it will be saved into OSDComputerName variable.
2. Set OSDComputerName variable using PowerShell script that is generated by another PowerShell script ๐
This solution is suitable for installations without access to SCCM Administration Service. It hardcodes serial number and corresponding computer name directly into Task Sequence.
Requirements
- SCCM account, that has READ permission to needed device data stored in Administration Service plus can modify Task Sequence step Hardcoded OSDCOMPUTERNAME based on Serial Number
At first, I've tried to use Task Sequence Dynamic Variables to define the serial number and corresponding OSDComputerName. The problem was very slow responses in GUI when watching the content.
A solution to this problem could be to use a file containing serial numbers and names plus a PowerShell script that would set OSDComputerName based on this data. But I prefer a fileless solution if possible, so I decided to omit usage of any file and instead save necessary information directly to Task Sequence.
What this mean? I have created PowerShell function, which will modify the existing Task Sequence step that itself sets OSDComputerName. This function will generate the complete content of this step (data + logic) so the result will look like this.
PowerShell function for filling this Task Sequence step
You can download the function in my GitHub repo.
function Set-CMTSStep_ServiceTag2OSDComputerName {
<#
.SYNOPSIS
Function for setting Task Sequence Step, that sets OSDCOMPUTERNAME variable based on device serial number (service tag).
Serial tags and device names of all clients are received from SCCM REST API.
.DESCRIPTION
Function for setting Task Sequence Step, that sets OSDCOMPUTERNAME variable based on device serial number (service tag).
Serial tags and device names of all clients are received from SCCM REST API.
It will:
- connect to SCCM server,
- receive serial numbers and device names of all clients,
- generate PowerShell script content that will return device name, based on its serial number
- set PowerShell script content in given Task Sequence Step
.PARAMETER sccmServer
Name of the SCCM server.
.PARAMETER sccmSiteCode
SCCM site code.
.PARAMETER tsName
Name of Task Sequence you want to modify.
.PARAMETER tsStepName
Name of Task Sequence Step you want to modify.
.EXAMPLE
Set-CMTSStep_ServiceTag2OSDComputerName
Will:
- connect to SCCM server,
- receive serial numbers and device names of all clients,
- generate PowerShell script content that will return device name, based on its serial number
- set PowerShell script content in given Task Sequence Step
.NOTES
Inspired by https://www.deploymentshare.com/rename-your-task-sequence-steps-with-powershell/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $sccmServer = $_SCCMServer,
[Parameter(Mandatory = $true)]
[string] $sccmSiteCode = $_SCCMSiteCode,
[Parameter(Mandatory = $true)]
[string] $tsName = "SET OSDCOMPUTERNAME BASED ON SERIAL NUMBER",
[Parameter(Mandatory = $true)]
[string] $tsStepName = "Hardcoded OSDCOMPUTERNAME based on Serial Number"
)
if (!(Get-Command Invoke-CMAdminServiceQuery -ErrorAction SilentlyContinue)) {
throw "Required command Invoke-CMAdminServiceQuery is missing."
}
# cannot use Connect-SCCM because of deserialization error :(
$session = New-PSSession -ComputerName $sccmServer -ErrorAction Stop
# create SCCM PSDrive & import SCCM PS module
Invoke-Command -Session $session {
param ($sccmSiteCode)
if (!(Get-Module ConfigurationManager)) {
Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
}
if (!(Get-PSDrive -Name $sccmSiteCode -PSProvider CMSite -ErrorAction SilentlyContinue)) {
New-PSDrive -Name $sccmSiteCode -PSProvider CMSite -Root $env:COMPUTERNAME | Out-Null
}
Set-Location "$($sccmSiteCode):\"
} -ArgumentList $sccmSiteCode
# prepare this remote session for working with Task Sequence
Invoke-Command -Session $session {
param ($tsName, $tsStepName)
# get Task Sequence object
$taskSequence = (Get-CMTaskSequence -Name $tsName -Fast)
if (!$taskSequence) { throw "'$tsName' Task Sequence wasn't found" }
# check Task Sequence Lock status
$lockState = Get-CMObjectLockDetails -InputObject $taskSequence | select -ExpandProperty LockState
if ($lockState -eq 1) {
throw "Task Sequence $tsName is locked (probably someone has it open in SCCM console)"
}
# get Task Sequence Step
$tsSteps = (Get-CMTaskSequenceStep -InputObject $taskSequence)
$tsStep = $tsSteps | ? { $_.Name -eq $tsStepName }
if (!$tsStep) { throw "Step '$tsStepName' wasn't found in Task Sequence '$tsName'" }
} -ArgumentList $tsName, $tsStepName -ErrorAction Stop
# get serial number and device name from SCCM Admin Service (REST API)
$deviceSerialNumber = Invoke-CMAdminServiceQuery -Source "wmi/SMS_G_System_SYSTEM_ENCLOSURE" -Select SerialNumber, ResourceID
$deviceName = Invoke-CMAdminServiceQuery -Source "wmi/SMS_R_SYSTEM" -Select Name, ResourceID, DistinguishedName
if (!$deviceSerialNumber -or !$deviceName) { throw "Unable to receive information from SCCM Administration service" }
#region prepare TS Step PowerShell script content
$devicesArrayString = '$devices = @('
$deviceName | Sort-Object -Property Name | % {
$name = $_.Name
$resourceID = $_.ResourceID
$serial = $deviceSerialNumber | ? { $_.ResourceID -eq $resourceID } | select -ExpandProperty SerialNumber | select -First 1
if ($serial) {
$devicesArrayString += "`n[PSCustomObject]@{Name = '$name'; SerialNumber = '$serial'}"
} else {
Write-Warning "Skipped. $name device doesn't have record in SCCM database"
}
}
# close array
$devicesArrayString += "`n)"
$sourceScript = @"
`$errorActionPreference = "Stop"
# array of all devices name and serial numbers that exists in SCCM
$devicesArrayString
# this computer serial number
`$serialNumber = (Get-WmiObject -Class WIN32_BIOS).SerialNumber
`$computerName = `$devices | ? {`$_.SerialNumber -eq `$serialNumber} | Select -expandProperty Name
if (`$computerName) {
if (`$computerName.count -gt 1) { throw "For computer with serial `$serialNumber, there is more than one name (`$computerName)"}
else { return `$computerName }
}
"@
#endregion prepare TS Step PowerShell script content
# customize content of PowerShell script called in Task Sequence Step
Invoke-Command -Session $session {
param ($sourceScript, $tsName, $tsStepName)
Set-CMTSStepRunPowerShellScript -TaskSequenceName $tsName -StepName $tsStepName -OutputVariableName 'OSDComputerName' -SourceScript $sourceScript -ExecutionPolicy Bypass
} -ArgumentList $sourceScript, $tsName, $tsStepName
Remove-PSSession $session -ea SilentlyContinue
}
The function itself gets all necessary data from SCCM Administration Service and then modifies given Task Sequence step (therefore has to be run with correct permissions).
You can run this function on schedule, to have the most recent data whenever you need them.
Summary
As I said, you can use both solutions or just pick the right one for your environment. Getting information from SCCM Administration Service is great if you install OS on-premise. Hardcoded (but dynamically generated) solution can be used if you use CMG for you OSD and you don't want to enable internet access for Administration Service.
Task Sequence and PowerShell function can be downloaded from my GitHub repo.
Enjoy ๐