Wednesday, January 18, 2017

Powershell Script to Deploy Dell BIOS/Firmware Updates to Computers in the Wild using SCCM

Update 1/31/2017 - I've joined the world and opened a GitHub account. I've posted the XML and my Script there. Makes it much easier to share.

https://github.com/dptechjournal/Dell-Firmware-Updates

Update - 1/27/2017, Script has been modified to create a scheduled task instead of a registry entry in Windows 7, and I changed the -contains parameter to the more appropriate -match parameter.

Update 1/27/2017 - I am testing modifying the script in the Windows 7 section to add a scheduled task on startup, that does two things, one, re-enables bitlocker on startup, and two,  removes itself from scheduled tasks. As Jay pointed out in the comments, with the current script that adds the reg key to HKLM it only re-enables bitlocker when a user logs in, and it runs with the user's permissions. I rather not have bitlocked machines be unprotected between the firmware update and a user logging in. So moving it to Scheduled Tasks on startup seems the way to go. I will post the updated script when it is finished, along with the XML for the scheduled task.

BIOS (or Firmware) updates are rated “Recommended” or "Urgent" by Dell, which means you should probably be performing them regularly. I find they solve a lot of little issues that we see with hardware, and also solve issues with BitLocker. For instance, on the OptiPlex 9010, BitLocker protection would activate after plugging in an additional monitor to a desktop. Updating the BIOS version fixed the issue. There was a time when a BIOS update that went sideways could basically brick your computer, but those days appear to be past. I have successfully updated a few thousand Dell computer BIOS’s in the wild over the past year using the script below without losing a single one. I will say I stick to flashing newer hardware of the past 4 years. I do not try to update GX 280’s for instance. Here is a list of the Dell Models I have successfully been updating:

We use a two pronged approach to updating our BIOS’s. During task sequence imaging we use a package to update the BIOS’s, the package suppresses the reboot, allowing the Task Sequence to control the reboot at a later stage. There are many blog posts about how to accomplish this type of update during a task sequence so I will just focus on the script we use for deploying BIOS updates to computers in the wild. The SCCM application we use to deploy this script is set to only run when no user is logged in. We time the deployments with our monthly Windows updates, which initiate a 10 hour countdown to reboot after installation. This ensures that a majority of desktops and a portion of laptops get the BIOS updates about 10 hours after deployment. The monthly Windows updates are due after work hours, the computers install the updates, reboot, and now have no user logged in. They then install the BIOS update and reboot again. The rest of the computers get the BIOS updates as the application catches the computer without a logged in user. This is hit and miss, but eventually works it way to most of the other computers. Twice I have heard someone mention seeing the Bios flashing and reboot and asked about it, but most of the time the end user either wont be there or wont see it.

Now you may ask, why not just deploy the bios update and suppress the reboot? Well, we tried that, and found that if a computer installed a bios update and went to sleep before rebooting to finish the update, it would cause an error when Windows resumed from sleep. Therefore I created this script to try to handle any contingencies I could think of.

The script has gone through a number of versions before I felt it was ready to share. It performs the following functions, while logging steps to the computers Application Log, so we can check for certain error codes in SCOM.
  1. Checks if computer is a laptop, if it is, exits script if battery life is less than 30%.
  2. Gets the OS of the Computer.
    1. For Windows 7 – Uses Manage-bde.exe to suspend BitLocker if the C: drive is BitLocked. We found that computers that are not BitLocked but ran the -disable command, informed the user that BitLocker could not be resumed upon logon, even though its not Bitlocked to being with. So we only run the suspend command if its actually BitLocked. Also writes to the RunOnce registry key a command to re-enable BitLocker on next startup. Also adds a Scheduled Task to enable bitlocker on startup, and then remove itself. This was necessary because in rare cases, BitLocker did not resume on its own after using the “Manage-bde.exe –protectors –disable” command.
    2. Windows 8 and 10 – Uses PowerShell commands to check for and disable BitLocker on the volume with the Operating System.
  3. Checks if BitLocker was successfully suspended. If it was not, writes the error to the Application Log and exists. I have not seen this error happen yet.
  4. Updates the BIOS and forces a reboot of the machine, which re-enables Bitlocker
We have Collections for each Hardware Model we have. I use those as  limiting collections for the Updating Collection that I will use to deploy the application script, with a query looking for older bio’s versions that need updating. I do this for each model I am updating. Here is an example of my Optiplex 3020 shrinking collection (models that are up to date get removed from the collection based a query), of which we have 575 of this model.  You can see, only 29 still have a BIOS that needs updating. (Most of them haven't been online, or in rare cases, the user is one who turns off the computer every night before leaving, so its never had time to install when no user is logged in. Eventually I will get them!)



Create an XML file named "sTask_Details.xml" to contain the Scheduled Task that will get imported when the script is ran. This will create 2 actions on the startup trigger. The first action will enable bitlocker, the 2nd action will remove the tasks from the task scheduler. Copy and pasting from the code below wont work, either recreate the task on a machine and export it as an XML, or better yet, grab it from my GitHub repository. https://github.com/dptechjournal/Dell-Firmware-Updates


  
    2017-01-30T08:47:18.2600256
    dave
  
  
    
      true
    
  
  
    
      S-1-5-18
      LeastPrivilege
    
  
  
    IgnoreNew
    false
    true
    true
    false
    false
    
      true
      false
    
    true
    true
    false
    false
    false
    false
    false
    P3D
    7
  
  
    
      Manage-bde.exe
      -protectors -enable c:
    
    
      cmd
      /c schtasks /delete /f /tn "Bitlock"
    
  


Here is the script itself, replace "Latitude_E7x70_1.12.3.exe" on the last line with the BIOS Update executable you want to run. If you have a Firmware Password, put it in the $args, replace /p=password with your firmware password. Yes this means that your firmware password is in your SCCM cache. But what can you do huh? The script is also at my GitHub repository https://github.com/dptechjournal/Dell-Firmware-Updates
<# 
 .NOTES
 ==========================================================================
  Created on:    3/25/2016 12:44 PM
  Created by:    David Pearson http://www.dptechjournal.net
 ===========================================================================
 .DESCRIPTION
  Installs BIOS Update's for Dell Computers.
  1. Determines the OS of the computer
  2. Suspends Bitlocker if needed
  3. Updates BIOS
  4. Writes Log to Application Log
 Function Get-Laptop from https://blogs.technet.microsoft.com/heyscriptingguy/2010/05/15/hey-scripting-guy-weekend-scripter-how-can-i-use-wmi-to-detect-laptops/
#>
Function Get-Laptop
{
 Param (
  [string]$computer = “localhost”
 )
 $isLaptop = $false
 if (Get-WmiObject -Class win32_systemenclosure -ComputerName $computer |
 Where-Object {
  $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 `
  -or $_.chassistypes -eq 14
 })
 { $isLaptop = $true }
 if (Get-WmiObject -Class win32_battery -ComputerName $computer)
 { $isLaptop = $true }
 $isLaptop
} # end function Get-Laptop
$currentDirectory = split-path -parent $MyInvocation.MyCommand.Definition

# Setup Logging
$ErrorActionPreference = "SilentlyContinue"
if (!(Get-Eventlog -LogName "Application" -Source "ConfigMgr Team"))
{
 New-Eventlog -LogName "Application" -Source "ConfigMgr Team" | Out-Null
}
$ErrorActionPreference = "Continue"


Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 1 -Message "ConfigMgr detected No User Logged In, Starting BIOS Upgrade Script."

# Check if Laptop, exit if Battery life is less than 30
If (get-Laptop)
{
 Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 3 -Message "Laptop detected, checking battery life."
 if ((Get-WmiObject win32_battery).estimatedChargeRemaining -le 30)
 {
  Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 4 -Message "Battery below 30%, canceling BIOS Upgrade...this time."
  exit
 }
}

# Get Operating System version and performs the appropriate actions to bitlocker
[int]$computerOS = (Get-WmiObject -namespace Root\cimv2 -Query "SELECT BuildNumber FROM win32_operatingSystem").buildnumber


if ($computerOS -ge 8000)
{
 $drive = Get-BitLockerVolume | where { $_.ProtectionStatus -eq "On" -and $_.VolumeType -eq "OperatingSystem" }
    if ($drive)
 {
     Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 2 -Message "Attempting to Suspend Bitlocker on drive $drive."
     Suspend-BitLocker -Mountpoint $drive -RebootCount 1
     if (Get-BitLockerVolume -MountPoint $drive | where ProtectionStatus -eq "On")
     {
      #Bitlocker Suspend Failed, Exit Script
      Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 13 -Message "Failed to Suspend Bitlocker on drive $drive , Exiting."
      exit
     }
    }
}
else
{
 $drive = manage-bde.exe -status c:
 if ($drive -match "    Protection Status:    Protection On")
 {
  Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 2 -Message "Attempting to Suspend Bitlocker on drive C: ."
  manage-bde.exe -protectors -disable c:
  $verifydrive = manage-bde.exe -status c:
  if ($verifydrive -match "    Protection Status:    Protection On")
  {
   #Bitlocker Suspend Failed, Exit Script
   Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 13 -Message "Failed to Suspend Bitlocker on drive C: , Exiting."
   exit
  }
  # Create a Scheduled Task to resume Bitlocker on startup, then remove
  cmd /c schtasks /create /f /tn "Bitlock" /XML $currentDirectory\sTask_Details.xml
 }
 
}

#Install BIOS Update
Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 7 -Message "Configmgr starting BIOS Update and rebooting. For more information, examine update.log in $currentDirectory"

$args = "/s /r /f /p=password /l=$currentDirectory\Update.log"
$install = Start-Process Latitude_E7x70_1.12.3.exe -ArgumentList $args -WorkingDirectory $currentDirectory -Wait


Name the script "Install_Dell_Bios_upgrade.ps1" and put it in your application source with the Bios Update executable and your XML file that holds the Scheduled Tasks details.

Here are the screenshots of how I setup my Application in ConfigMgr. The installation program install line is
powershell.exe -ExecutionPolicy Unrestricted -File "Install_Dell_Bios_upgrade.ps1"



My detection method checks the BIOS version in the registry, this briefly makes the application report that it failed to install as this does not update until after the reboot, but I am only concerned with the BIOS not upgrading if its already at the correct version.
I should mention that the parameters in the install command WILL NOT install the Firmware update if a laptop is not plugged into power. If you are feeling adventurous, you can add the switch to force the update, usually its /f . I am debating adding this, as I will rely on the estimated battery life part of the script to ensure there is enough juice left for the update to happen. That would go a long way towards getting the update to laptops in a more timely manner.