Updating Lenovo ThinkPad/ThinkCentre BIOS as SCCM application

Updating BIOS is no easy task. You probably have several models with differing BIOS versions. Also, the update requires a restart making it difficult to deploy without interrupting work. In this case, user interaction is recommended if a user happens to be logged on during deployment. This is where PSADT comes in to play.

Goal of this guide is to help you create a repository of Lenovo BIOS updates and automate updates using a single application in SCCM. User interaction is made possible by PSADT.
This repository may also be used during OSD. I’ll show this in an upcoming post.

Disclaimer: This has been tested on L/T/X series ThinkPads for *30-*70 generation. For ThinkCentres we only have M900. So adjust and test accordingly.

Getting latest BIOS
An easy way to get the latest BIOS is to download it from Lenovo. Let’s take a ThinkPad L460 as an example. The URL for downloads is: https://pcsupport.lenovo.com/us/en/products/laptops-and-netbooks/thinkpad-l-series-laptops/thinkpad-l460/downloads. Here you can find the BIOS section. You want to download the BIOS update utility pack. For other models, you can easily modify the URL.

After download, extract the update (run the .exe to extract, but don’t run utility afterwards) to a folder with the model name, in this case a folder named L460. Do this for all your models.

Next, create a .txt file called biosversions.txt. Here, create tab delimited headers ‘Model’ and ‘Version’. Then enter all your models and the latest BIOS version you downloaded. As such:

Model Version
L530 2.67
L450 1.25
M900 7FA

Later on, we’ll use this text file in conjunction with our detection method. Here’s a template if you want: biosversions.txt

You may have noticed the differing versioning for ThinkCentre M900. For some reason Lenovo doesn’t use conventional version numbers for ThinkCentre, but instead hexadecimal(?). The complete version number for M900 is FWKT7FA. The FWKT characters are constant, but the three last characters change between versions, and the version is ascending in hexadecimal value if you watch the changelog from previous version.

Folder structure/placement
As mentioned, we’ll use PSADT for this setup.
So, copy all the folders for the models under the PSADT Files folder. Also copy biosversions.txt to the root of the PSADT folder. Finally, get a copy of 32-bit ServiceUI.exe from MDT and place it in the root folder (this allows for user interaction in “Whether user logged on or not” scenario).


Inside the files folder:

Disregard SCCM-UpdateBIOS.ps1 and ServiceUI64.exe until the next blog post…

Make the contents of the PSADT and subfolder readable and executable for Domain Computers on a server. In our case, we keep it on the SCCM server. We do this so the client doesn’t need to download all BIOS updates. The script will see to it that the client only copies the applicable update. If you want to perform BIOS updates in WinPE during OSD (wait for next post!), you should give read/execute permissions to the Everyone group. Additionally, for anonymous access (WinPE), you need some more tweaking, see this guide: https://vitoriodelage.wordpress.com/2016/04/07/creating-an-anonymous-smb-network-share/

Installation script
Here’s the code for the pre-install phase, i.e. insert the code under pre-install phase in Deploy-Application.ps1.
In a nutshell, check for AC power and show prompt if anyone is logged on.

Here’s for the installation phase (thanks for bug report from /u/davidnait over on reddit):

And lastly, post-install phase. Note that ThinkCentres require a shutdown.

Full Deploy-Application.ps1 here: https://www.dropbox.com/s/nfadi4q4mobembv/Deploy-Application.ps1

Create application in SCCM
Create application as you normally do. For deployment type, select Script Installer.
Leave content empty.
For installation program command, use ServiceUI.exe Deploy-Application.exe in conjunction with Installation start in containing the path to the location of the contents. Lastly, check to run in 32-bit.

Detection method
Next up, detection method. Use custom script of type PowerShell. Remember the temporary registry key from the script? Here we’ll check for it and remove it.
Make sure you have the correct network location for the biosversions.txt file.

Lastly, you may deploy the app. You may set it to run as whether user is logged on or not. This will allow automatic updating of BIOS if PC is on, but no user logged on. Wake-on-LAN is to recommend in this scenario.

Remember to test thoroughly before deploying to production.

PSADT interaction in an SCCM task sequence

PSADT is a great toolkit. I use it for all our SCCM applications and packages. In fact, the toolkit is installed as a module on all systems through GPO. The cmdlets it provides are excellent (Get-InstalledApplication and
Invoke-HKCURegistrySettingsForAllUsers being personal favorites). PSADT is so great, there’ll be many posts in the future containing it.

Anyway, this post is about PSADT in a task sequence. This need was born out of the need to perform an organization wide Windows 7 to 10 upgrade using a SCCM TS. TS’s are out-of-the-box without any sort of interactivity, but with PSADT and ServiceUI.exe magic we can make it happen.
So, using the upgrade scenario as an example, what we want is:

1. Have the upgrade available to users in software center, so they can install it at their own leisure
2. After a certain date, make the deployment required.

Required task sequences are tricky. You can make people easily angry with that kind of stuff. So, a deferral option has to be in there. What else? Well, pre-flight checks in general would be. We want to check that user has power connected, as well as wired network connection, for starters.

PSADT has a deferral system. In a script, specifically in the Show-InstallationWelcome cmdlet, you can specify how many times a user is allowed to defer. After the user has deferred the allotted times, the prompt will be inescapable. Lovely!

So, on to the PSADT script. You may have the code in either the pre-installation or installation section of Deploy-Application.ps1. Doesn’t really matter since we’re just running PoSH code, not doing any installation.

Let’s start with pre-flight checks. Let’s assume first that AC is plugged, if the system has a battery, we’ll actually check it, by using WMI.

$ACplugged = $true
If (Get-WmiObject Win32_Battery) { 
    $ACplugged = Get-WmiObject -Class BatteryStatus -NameSpace Root\Wmi | Select-Object -ExpandProperty PowerOnline

Network connections. Let’s check for a wired connection as well as wireless. In our case, we don’t want to allow upgrades over VPN. Depending on VPN, you may need to modify so the variable suits your environment. Included WMI query checks for Palo Alto GlobalProtect.

$wiredNIC = Get-WmiObject -Class Win32_NetworkAdapter -NameSpace root\CIMV2 | Where-Object {$_.AdapterType -match "Ethernet" -and $_.NetConnectionStatus -eq 2 -and $_.Name -notmatch "Wireless" -and $_.Name -notmatch "PANGP"}

$WiFiNIC = Get-WmiObject -Class Win32_NetworkAdapter -NameSpace root\CIMV2 | Where-Object {$_.AdapterType -match "Ethernet" -and $_.NetConnectionStatus -eq 2 -and $_.Name -match "Wireless"}

$VPN = Get-WmiObject -Class Win32_NetworkAdapter -NameSpace root\CIMV2 | Where-Object {$_.Name -match "PANGP" -and $_.NetConnectionStatus -eq 2}

Next, let’s invoke TS environment to the $TSEnv variable.

$TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment

Here we set TS error dialog timeout to one second. Doesn’t look cool if the user defers and they still have an error dialog for 20 minutes…

$TSEnv.Value("SMSTSErrorDialogTimeout") = "1"

For clear visibility of our PSADT prompt, let’s close the TS progress UI, but only if a user is logged on. $usersLoggedOn is a PSADT built-in variable.

If ($usersLoggedOn) { (New-Object -COMObject Microsoft.SMS.TSProgressUI).CloseProgressDialog() }

Now, if a user is on VPN, we’ll inform the user to come to the office for the upgrade. Then, we exit with code 60012 (PSADT deferral code).

If ($VPN) {

    $deferInstallation = Show-InstallationPrompt -Message 'Unfortunately, Windows 10 upgrade is not supported over VPN. Please do it on premises.' -ButtonMiddleText 'OK' -Icon 'Exclamation' -PersistPrompt

    Exit-Script -ExitCode 60012

If AC power is not available, let’s inform that the user has to plug it in for the upgrade to continue. There’s also an option to defer to run the upgrade at another time.
To accomplish this, I believe it’s best to use a While loop.

While (!$ACplugged) {

    If (!$usersLoggedOn) { Exit-Script -ExitCode 60012 }
    ElseIf ($usersLoggedOn) {

        $deferInstallation = Show-InstallationPrompt -Title "Windows 10 upgrade" -Message "AC power is disconnected! Please plug in your PC and select continue, if you want to run the upgrade now. You may also defer installation to another time." -ButtonLeftText "Continue" -ButtonRightText "Defer" -Icon 'Exclamation' -PersistPrompt
        If ($deferInstallation -eq "Defer") { Exit-Script -ExitCode 60012 } 
        ElseIf ($deferInstallation -eq "Continue") {

            Start-Sleep -Seconds 4
            $ACplugged = Get-WmiObject -Class BatteryStatus -NameSpace Root\Wmi | Select-Object -ExpandProperty PowerOnline

Let’s do the same thing for Ethernet.

While (!$wiredNIC) { 

    $deferInstallation = Show-InstallationPrompt -Title "Windows 10 upgrade" -Message "Wired network is disconnected! Please plug in your PC and select continue, if you want to run the upgrade now. You may also defer installation to another time." -ButtonLeftText "Continue" -ButtonRightText "Defer" -Icon 'Exclamation' -PersistPrompt
	If ($deferInstallation -eq "Defer") { Exit-Script -ExitCode 60012 } 
	ElseIf ($deferInstallation -eq "Continue") {

        Start-Sleep -Seconds 5
        $wiredNIC = Get-WmiObject -Class Win32_NetworkAdapter -NameSpace root\CIMV2 | Where-Object {$_.AdapterType -match "Ethernet" -and $_.NetConnectionStatus -eq 2 -and $_.Name -notmatch "Wireless" -and $_.Name -notmatch "PANGP"}

Lastly, if both Ethernet and Wifi is enabled, let’s disconnect from the Wifi network.

If ($WiFiNIC) {

    netsh wlan disconnect
    Start-Sleep -Seconds 2

If all checks out, let’s inform the user of the pending upgrade, and allow deferral. Let’s also prompt for the closing of applications. We can do this with the Show-InstallationWelcome cmdlet.
If you want custom text in this cmdlet, you need to edit AppDeployToolkitConfig.xml inside the respective language tag, as well as set -CustomText to true in the command. Like this:

Show-InstallationWelcome -CloseApps 'iexplore=Internet Explorer,chrome=Chrome,firefox=Firefox,winword=Word,excel=Excel,outlook=Outlook,powerpnt=PowerPoint' -AllowDefer -DeferTimes 10 -PersistPrompt -CustomText $true

And that’s pretty much the script. Of course, consider this a template and modify to your needs. Please comment if you have some handy additions!

Now, to include this in a task sequence, you’ll want to include it as the first step. For user interaction you need to provide ServiceUI.exe in the package (Get it from MDT).
Finally, the command line will be:

ServiceUI.exe Deploy-Application.exe

Interaction will look like this:

That’s all for now!

SCCM OSD with multiple disks

OSD with systems that have multiple disks can be troublesome. Out of the box, SCCM TS steps don’t offer much in regards to this. You can make Format Drive steps for each disk, but there is no immediate way to identify which disk is which!
Even going into BIOS to check which disk is on which channel won’t help. At each boot, Windows may interpret the disk channels differently: https://support.microsoft.com/en-us/help/937251/disk-drive-numbers-may-not-correspond-to-the-sata-channel-numbers-when

BUT, all hope is not lost. SCCM has a TS variable named OSDDiskIndex. The docs (https://technet.microsoft.com/en-us/library/hh273365.aspx) describe it as “Specifies the physical disk number to be partitioned.”. Hm, specify it how? Well, basically you find the number ID of the disk you want to specify for formatting, and you give that number to the OSDDiskIndex variable. How do we do this? You guessed it, with Powershell of course!

Since Server 2012/Windows 8, there exists the cmdlet Get-PhysicalDisk. You’d think it’d be a part of Powershell 3.0 for Windows 7/2008 R2, but for some reason the storage modules were not included in that (https://blogs.technet.microsoft.com/heyscriptingguy/2012/10/26/use-powershell-to-create-a-bootable-usb-drive/). I don’t know if it’s included in PS 4.0 and higher. But don’t fret, I will show how to accomplish the same thing with a WMI query.


Get-PhysicalDisk is a nice cmdlet. It gives all the relevant information you could wish for from a disk. For OSD, you probably want to differentiate disks on two criteria, SSD/HDD and size.
Getting disk type is easy. The MediaType property of a disk object from the cmdlet describes if the disk is an SSD or HDD. Size is decribed in the… size property! What else do we want? Oh, right the most important one, the ID! This is in DeviceID property.
Output example:

So, in a two disk scenario, where one disk is SSD and you want to select it, the command is as simple as

Get-PhysicalDisk | Where-Object MediaType -eq 'SSD' | Select-Object -ExpandProperty DeviceID

This will give us the ID of the disk that is an SSD.
Defining on size basis is easy as well (300GB):

Get-PhysicalDisk | Where-Object {$_.Size/1GB -gt 300} | Select-Object -ExpandProperty DeviceID

For combination of size and SSD:

Get-PhysicalDisk | Where-Object {$_.MediaType -eq 'SSD' -and  $_.Size/1GB -gt 300} | Select-Object -ExpandProperty DeviceID


This is all fine and dandy if you’re on at least Win8/2012/PE4.0 (or possibly Powershell 4.0 or higher on Win7/2008), but what if you’re not? Good old WMI it is then! Here’s the last example in WMI:

Get-WmiObject Win32_DiskDrive | Where {$_.Model -match "SSD" -and $_.Size/1GB -gt 300} | Select-Object -ExpandProperty Index

Basically, we use Win32_DiskDrive WMI class to get disk info. Sadly, there is no definite property in WMI that will tell if disk is SSD or not… But in my experience, all SSDs have “SSD” in the model name, so we’ll have to make do with that. Lastly, the ID of the disk is in the Index property.

So now we should know how to find the disk we want to partition. Next, let’s pass this information to the SCCM TS variable OSDDiskIndex, so the TS partition step will select the disk we want!

I find using Run Command Line TS step, with powershell.exe with the Command argument is the easiest way to run one-liners.

In the one-liner, we simply give the OSDDiskIndex variable the ID of the disk.

PowerShell -ExecutionPolicy Bypass -Command " (New-Object -COMObject Microsoft.SMS.TSEnvironment).Value('OSDDiskIndex') = Get-PhysicalDisk | Where-Object {$_.MediaType -eq 'SSD' -and  $_.Size/1GB -gt 300} | Select-Object -ExpandProperty DeviceID "

Of course, if you want to use WMI, replace the second portion of the command argument with the WMI equivalent.

When you run this, that disk will now be selected for later partitioning step.

Capturing SCCM TS logs – The PowerShell way

Ever had a task sequence fail? Wouldn’t it be nice to view the logs from a central location? Then this is the tutorial for you.

There are several posts out there how to capture logs, but they’re all a bit cumbersome for my tastes… involving several additional steps in your task sequences and relying on CMD commands (yuck!).

Here’s how to do it all in a single PowerShell script.


As you probably know, when a task sequence fails it stops and usually show you this a countdown like this:

So, instead of just stopping when a failure occurs, how do we force the TS to proceed to copy logs instead? Answer lies in taking advantage of the “Continue on error” TS step property:

By nesting the TS under one group, checking its “Continue on error” property and lastly setting the capture log step last in the TS, we make sure logs will always be copied, regardless of outcome.

Now for the script.

The script

For the impatient and experienced, you can skip further down for the full script. Verbose description follows:

First we need to initialize the task sequence environment to an object stored in a variable called $TSEnv.

$TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment

Next, let’s store the computer name in the variable $TSComputerName

$TSComputerName = $TSEnv.Value("OSDComputerName")

If OSDComputerName doesn’t exist, we will use _SMSTSMachineName instead.

If (!$TSComputerName) { $TSComputerName = $TSEnv.Value("_SMSTSMachineName") }

Let’s set the TS logpath to a variable.

$TSLogPath = $TSEnv.Value("_SMSTSLogPath")

Now for more involved stuff. For a good organization of your TS logs, it’s preferable to sort them by Success and Failure. How do we determine if a TS was either? The self-explanatory boolean _SMSTSLastActionSucceeded variable tells us! If it’s false, the last step was a failure, if true, all’s ok.

So, our IF statement, in case of TS failure:

If ($TSEnv.Value("_SMSTSLastActionSucceeded") -eq "false")

For better visibility of our later custom error message, it’s best close the TS progress bar. We do this by invoking the TSProgressUI object and calling its CloseProgressDialog method.

(New-Object -COMObject Microsoft.SMS.TSProgressUI).CloseProgressDialog()

At this point, let’s set the central log location we want to write the logs to. We’ll use the computer name as the last folder.

$LogLocation = "\\YourServer\ConfigMgr\Logs\Tasksequence\Failure\$TSComputerName"

Let’s remove old logs for this model, recursively, forcefully and without error (in case previous logs don’t exist).

Remove-Item $LogLocation -Recurse -Force -ErrorAction SilentlyContinue

Let’s then copy the logs!

Copy-Item $TSLogPath $LogLocation -Recurse

Finally, let’s display an error message using WshShell popup method! This method has some possibilities. In this example, we display a popup that will say that an error occurred, log location and prompt the user if the want to shut down the system or continue (and exit the TS). You can read about the syntax more here: https://msdn.microsoft.com/en-us/library/x83z1d9f(v=vs.84).aspx

Start by creating a WshShell object and setting it to the $WshShell variable.

$WshShell = New-Object -ComObject WScript.Shell

Create popup. Insert your text as in the example.

$PopUp = $WshShell.Popup("An error occurred! Check the logs on the server in this location: `n\Logs\Failure\$TSComputerName.`n`nTurn off the computer?",0,"Error occured",0x4 + 0x10 + 0x1000)

It will look like this.

A useful pop-up for once.

Again, I recommend you read the documentation if you want do make further customization to the prompt.

For the next step, we process the input to the prompt. The docs state, an answer of Yes equals Return value of 6, 7 for No. In this case, we only need to do something if the user chose to shut down the system.
Since shutdown.exe is not provided in WinPE, we have to use the command Wpeutil ShutDown instead. As for full OS, we use shutdown.exe, but we use it through the SMSTSPostAction variable instead. SMTSPostAction is basically an optional command line that is run after a TS ends, after TS environment is closed. In this case it’s preferable to shut down that way, ensuring that the TS environment does not end abruptly, possibly causing the client to be stuck in provisioning mode (a hassle!).

If ($PopUP -eq "6") {

    If ($TSEnv.Value("_SMSTSInWinPE") -eq "true") { wpeutil shutdown }
    Else { $TSEnv.Value("SMSTSPostAction") = "shutdown /s /t 0 /f" }

That’s it for the failure scenario! Next, let’s get dressed for success. Basically we just copy the logs, pretty much like in the failure scenario.
Also, if you want to set a SMSTSPostAction, you can do it here. In this case we have a restart.

Else {

    $LogLocation = "\\ville02\ConfigMgr\Logs\Tasksequence\Success\$TSComputerName"
    Remove-Item $LogLocation -Recurse -Force -ErrorAction SilentlyContinue
    Copy-Item $TSLogPath $LogLocation -Recurse
    $TSEnv.Value("SMSTSPostAction") = "shutdown /r /t 5 /f"

Full script

Apply script in TS

Now it’s time to put the script in the TS. Due to SCCM task sequences being executed in Session 0 and user console session being in Session 1, we need some assistance to move execution of the script to the user session to show the prompt.
Enter ServiceUI.exe. ServiceUI.exe is an executable found in MDT (Microsoft Deployment Toolkit) and its purpose is to break out processes from Session 0 into Session 1. You can find the executable in an installed MDT instance. Insert ServiceUI.exe in the same folder as the script.

Anonymous access to network share

For the script to work in both full OS and PE, you need to have the log folders to allow Anonymous write access. To avoid packages, you can also insert the script into a folder where Anonymous has read access.
Here’s a fine guide written by Vitorio Delage how to accomplish this: https://vitoriodelage.wordpress.com/2016/04/07/creating-an-anonymous-smb-network-share/

Script in TS

Finally, insert a “Run command line” step as the last one in the TS. The command line should be:

ServiceUI64.exe -process:TSProgressUI.exe %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File SCCM-CopyLogs.ps1

In the “Start in:” field, add the location of the script. Also check to Disable 64-bit redirection, no need for that.

That’s it!