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)

Subscribe to my newsletter and never miss my upcoming articles

[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)

Requirements

  • Administration Service has to be accessible from WinPE
  • 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 image.png

1. Create new Security Role in SCCM

Create new Security Role and grant this role only following permissions image.png

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

image.png

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 image.png

    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.

image.png

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. image.png


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. image.png

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 👍

Interested in reading more such articles from Ondrej Sebela?

Support the author by donating an amount of your choice.

 
Share this