Deploying HP BIOS Updates – a real world example

Not so long ago HP published a customer advisory listing a number of their models that need to be on the latest BIOS release to be upgraded to Windows 10 2004. Since we were getting ready to rollout 20H2 we encountered some affected models in piloting, which prompted me to find that advisory and then get the BIOS updated on affected devices.

To be honest, until now we’ve never pushed out BIOS updates to anyone, but to get these devices updated to 20H2 we now had no choice. In this post I’m just going to share how we did that. For us, good user experience is critical but finding the balance between keeping devices secure and up-to-date without being too disruptive to the user can be a challenge!

First off was to create a script that could update the BIOS on any supported HP workstation, without needing to package and distribute BIOS update content. I know other equally handsome community members have shared some great scripts and solutions for doing BIOS updates, but I decided to create my own in this instance to meet our particular requirements and afford a bit more control over the update process. I considered using the HP Client Management Script Library which seems purpose-built for this kind of task and is a great resource, but I preferred not to have the dependency of an external PowerShell module and its requirements.

I published a version of this script in Github here. The script does the following:

  • Creates a working directory under ProgramData
  • Disables the IE first run wizard which causes a problem for the Invoke-WebRequest cmdlet when running in a context where IE hasn’t been initialised
  • Checks the HP Image Assistant (HPIA) web page for the latest version and downloads it
  • Extracts and runs the HPIA to identify the latest applicable BIOS update (if any)
  • If a BIOS update is available, downloads and extracts the softpaq
  • Checks if a BIOS password has been set, if so creates an encrypted password file as required by the firmware update utility
  • Runs the firmware update utility to stage the update
  • Everything is logged to the working directory and the logs are uploaded to Azure blob storage upon completion, or if something fails, so we can review them without requiring remote access to the user’s computer

It all runs silently without any user interaction and it can be run on any HP model that the HPIA supports.

In Production however, we used a slightly modified version of this script. Since there was the possibility that there could be unknown BIOS passwords in use out there, we decided not to try to flash the BIOS using an encrypted password file, but instead try to remove the BIOS password altogether (temporarily!) When the BIOS update is staged it simply copies the password file to the staging volume – it doesn’t check whether the password is correct or not. If it is not correct, the user would then be asked for the correct password when the BIOS is flashed and that is not cool! Removing the password meant that the user could never be unexpectedly prompted for the password in the event that the provided password file was incorrect. Of course, to remove the password you also have to know the password, so we tried the ones we knew and if they worked, great, if they didn’t, the script would simply exit out as a failsafe.

We have a compliance baseline deployed with MEMCM that sets the BIOS password on any managed workstation that does not have one set, so after the machine rebooted, the BIOS flashed and machine starts up again, before long the CB would run and set the password again.

Doing this also meant that we needed to ensure the computer was restarted asap after the update was staged – and for another reason as well – Bitlocker encryption is suspended until the update is applied and the machine restarted.

Because we didn’t want to force the update on users and force a restart on them, we decided to package the script as an application in MEMCM. This meant a couple of things:

  • We could put a nice corporate logo on the app and make it available for install in the Software Center
  • We could handle the return codes with custom actions. In this case, we are expecting the PowerShell script to exit successfully with code 0, and when it does we’ve set that code to be a soft reboot so that MEMCM restart notifications are then displayed to the user.

As a detection method, the following PowerShell code was used. This simply detects if the last write time of the log file has changed within the last hour, if it has, it’s installed. Longer than an hour and it’s available for install again if needed.

$Log = Get-ChildItem C:\ProgramData\Contoso\HP_BIOS_Update -Recurse -Include HP_BIOS_Update.log -ErrorAction SilentlyContinue | 
    where {([DateTime]::Now - $_.LastWriteTime).TotalHours -le 1}
If ($Log)
{
    Write-Host "Installed"
}

We then deployed the application with an available deployment. We communicated with the users directly to inform them a BIOS update needed to be installed on their device in order to remain secure and up-to-date, and directed them to the Software Center to install it themselves.

We also prepared collections in SCCM using query-based membership rules to identify the machines that were affected by the HP advisory, and an SQL query to find the same information and pull the full user name and email address from inventoried data.

The script does contain the BIOS password in clear text which, or course, may not meet your security requirements, although for us this password is not really that critical – it’s just there to help prevent the user from making unauthorized changes in the BIOS. In our Production script though, we simply converted these to base64 before adding them to the script to at least provide some masking. But for greater security you could consider storing the password in Azure key vault and fetch it at run time with a web request, for example.

If you wish to use the script in your own environment, you’ll need to change the location of the working directory as you desire. Additionally if you wish to upload the log files to an Azure storage container, you’ll need to have or create a container and add the URL and the SAS token string to the script, or else just comment out the Upload-LogFilesToAzure function where it’s used. I’m a big fan of sending log files to Azure storage especially during this season where many are working from home and may not be corporate connected. You can just use Azure Storage Explorer to download the log files which will open up in CMTrace if that’s your default log viewer.

Hope this is helpful to someone! The PS script is below.

#####################
## HP BIOS UPDATER ##
#####################
# Params
$HPIAWebUrl = "https://ftp.hp.com/pub/caps-softpaq/cmit/HPIA.html" # Static web page of the HP Image Assistant
$BIOSPassword = "MyPassword"
$script:ContainerURL = "https://mystorageaccount.blob.core.windows.net/mycontainer" # URL of your Azure blob storage container
$script:FolderPath = "HP_BIOS_Updates" # the subfolder to put logs into in the storage container
$script:SASToken = "mysastoken" # the SAS token string for the container (with write permission)
$ProgressPreference = 'SilentlyContinue' # to speed up web requests
################################
## Create Directory Structure ##
################################
$RootFolder = $env:ProgramData
$ParentFolderName = "Contoso"
$ChildFolderName = "HP_BIOS_Update"
$ChildFolderName2 = Get-Date Format "yyyy-MMM-dd_HH.mm.ss"
$script:WorkingDirectory = "$RootFolder\$ParentFolderName\$ChildFolderName\$ChildFolderName2"
try
{
[void][System.IO.Directory]::CreateDirectory($WorkingDirectory)
}
catch
{
throw
}
# Function write to a log file in ccmtrace format
Function script:Write-Log {
param (
[Parameter(Mandatory = $true)]
[string]$Message,
[Parameter()]
[ValidateSet(1, 2, 3)] # 1-Info, 2-Warning, 3-Error
[int]$LogLevel = 1,
[Parameter(Mandatory = $true)]
[string]$Component,
[Parameter(Mandatory = $false)]
[object]$Exception
)
$LogFile = "$WorkingDirectory\HP_BIOS_Update.log"
If ($Exception)
{
[String]$Message = "$Message" + "$Exception"
}
$TimeGenerated = "$(Get-Date Format HH:mm:ss).$((Get-Date).Millisecond)+000"
$Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">'
$LineFormat = $Message, $TimeGenerated, (Get-Date Format MMddyyyy), $Component, $LogLevel
$Line = $Line -f $LineFormat
# Write to log
Add-Content Value $Line Path $LogFile ErrorAction SilentlyContinue
}
# Function to upload log file to Azure Blob storage
Function Upload-LogFilesToAzure {
$Date = Get-date Format "yyyy-MM-dd_HH.mm.ss"
$HpFirmwareUpdRecLog = Get-ChildItem Path $WorkingDirectory Include HpFirmwareUpdRec.log Recurse ErrorAction SilentlyContinue
$HPBIOSUPDRECLog = Get-ChildItem Path $WorkingDirectory Include HPBIOSUPDREC64.log Recurse ErrorAction SilentlyContinue
If ($HpFirmwareUpdRecLog)
{
$File = $HpFirmwareUpdRecLog
}
ElseIf ($HPBIOSUPDRECLog)
{
$File = $HPBIOSUPDRECLog
}
Else{}
If ($File)
{
$Body = Get-Content $($File.FullName) Raw ErrorAction SilentlyContinue
If ($Body)
{
$URI = "$ContainerURL/$FolderPath/$($Env:COMPUTERNAME)`_$Date`_$($File.Name)$SASToken"
$Headers = @{
'x-ms-content-length' = $($File.Length)
'x-ms-blob-type' = 'BlockBlob'
}
Invoke-WebRequest Uri $URI Method PUT Headers $Headers Body $Body ErrorAction SilentlyContinue
}
}
$File2 = Get-Item $WorkingDirectory\HP_BIOS_Update.log ErrorAction SilentlyContinue
$Body2 = Get-Content $($File2.FullName) Raw ErrorAction SilentlyContinue
If ($Body2)
{
$URI2 = "$ContainerURL/$FolderPath/$($Env:COMPUTERNAME)`_$Date`_$($File2.Name)$SASToken"
$Headers2 = @{
'x-ms-content-length' = $($File2.Length)
'x-ms-blob-type' = 'BlockBlob'
}
Invoke-WebRequest Uri $URI2 Method PUT Headers $Headers2 Body $Body2 ErrorAction SilentlyContinue
}
}
Write-Log Message "#######################" Component "Preparation"
Write-Log Message "## Starting BIOS update run ##" Component "Preparation"
Write-Log Message "#######################" Component "Preparation"
#################################
## Disable IE First Run Wizard ##
#################################
# This prevents an error running Invoke-WebRequest when IE has not yet been run in the current context
Write-Log Message "Disabling IE first run wizard" Component "Preparation"
$null = New-Item Path "HKLM:\SOFTWARE\Policies\Microsoft" Name "Internet Explorer" Force
$null = New-Item Path "HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer" Name "Main" Force
$null = New-ItemProperty Path "HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer\Main" Name "DisableFirstRunCustomize" PropertyType DWORD Value 1 Force
##########################
## Get latest HPIA Info ##
##########################
Write-Log Message "Finding info for latest version of HP Image Assistant (HPIA)" Component "DownloadHPIA"
try
{
$HTML = Invoke-WebRequest Uri $HPIAWebUrl ErrorAction Stop
}
catch
{
Write-Log Message "Failed to download the HPIA web page. $($_.Exception.Message)" Component "DownloadHPIA" LogLevel 3
UploadLogFilesToAzure
throw
}
$HPIASoftPaqNumber = ($HTML.Links | Where {$_.href -match "hp-hpia-"}).outerText
$HPIADownloadURL = ($HTML.Links | Where {$_.href -match "hp-hpia-"}).href
$HPIAFileName = $HPIADownloadURL.Split('/')[-1]
Write-Log Message "SoftPaq number is $HPIASoftPaqNumber" Component "DownloadHPIA"
Write-Log Message "Download URL is $HPIADownloadURL" Component "DownloadHPIA"
###################
## Download HPIA ##
###################
Write-Log Message "Downloading the HPIA" Component "DownloadHPIA"
try
{
$ExistingBitsJob = Get-BitsTransfer Name "$HPIAFileName" AllUsers ErrorAction SilentlyContinue
If ($ExistingBitsJob)
{
Write-Log Message "An existing BITS tranfer was found. Cleaning it up." Component "DownloadHPIA" LogLevel 2
Remove-BitsTransfer BitsJob $ExistingBitsJob
}
$BitsJob = Start-BitsTransfer Source $HPIADownloadURL Destination $WorkingDirectory\$HPIAFileName Asynchronous DisplayName "$HPIAFileName" Description "HPIA download" RetryInterval 60 ErrorAction Stop
do {
Start-Sleep Seconds 5
$Progress = [Math]::Round((100 * ($BitsJob.BytesTransferred / $BitsJob.BytesTotal)),2)
Write-Log Message "Downloaded $Progress`%" Component "DownloadHPIA"
} until ($BitsJob.JobState -in ("Transferred","Error"))
If ($BitsJob.JobState -eq "Error")
{
Write-Log Message "BITS tranfer failed: $($BitsJob.ErrorDescription)" Component "DownloadHPIA" LogLevel 3
UploadLogFilesToAzure
throw
}
Write-Log Message "Download is finished" Component "DownloadHPIA"
Complete-BitsTransfer BitsJob $BitsJob
Write-Log Message "BITS transfer is complete" Component "DownloadHPIA"
}
catch
{
Write-Log Message "Failed to start a BITS transfer for the HPIA: $($_.Exception.Message)" Component "DownloadHPIA" LogLevel 3
UploadLogFilesToAzure
throw
}
##################
## Extract HPIA ##
##################
Write-Log Message "Extracting the HPIA" Component "Analyze"
try
{
$Process = Start-Process FilePath $WorkingDirectory\$HPIAFileName WorkingDirectory $WorkingDirectory ArgumentList "/s /f .\HPIA\ /e" NoNewWindow PassThru Wait ErrorAction Stop
Start-Sleep Seconds 5
If (Test-Path $WorkingDirectory\HPIA\HPImageAssistant.exe)
{
Write-Log Message "Extraction complete" Component "Analyze"
}
Else
{
Write-Log Message "HPImageAssistant not found!" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to extract the HPIA: $($_.Exception.Message)" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
##############################################
## Analyze available BIOS updates with HPIA ##
##############################################
Write-Log Message "Analyzing system for available BIOS updates" Component "Analyze"
try
{
$Process = Start-Process FilePath $WorkingDirectory\HPIA\HPImageAssistant.exe WorkingDirectory $WorkingDirectory ArgumentList "/Operation:Analyze /Category:BIOS /Selection:All /Action:List /Silent /ReportFolder:$WorkingDirectory\Report" NoNewWindow PassThru Wait ErrorAction Stop
If ($Process.ExitCode -eq 0)
{
Write-Log Message "Analysis complete" Component "Analyze"
}
elseif ($Process.ExitCode -eq 256)
{
Write-Log Message "The analysis returned no recommendation. No BIOS update is available at this time" Component "Analyze" LogLevel 2
UploadLogFilesToAzure
Exit 0
}
elseif ($Process.ExitCode -eq 4096)
{
Write-Log Message "This platform is not supported!" Component "Analyze" LogLevel 2
UploadLogFilesToAzure
throw
}
Else
{
Write-Log Message "Process exited with code $($Process.ExitCode). Expecting 0." Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to start the HPImageAssistant.exe: $($_.Exception.Message)" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
# Read the XML report
Write-Log Message "Reading xml report" Component "Analyze"
try
{
$XMLFile = Get-ChildItem Path "$WorkingDirectory\Report" Recurse Include *.xml ErrorAction Stop
If ($XMLFile)
{
Write-Log Message "Report located at $($XMLFile.FullName)" Component "Analyze"
try
{
[xml]$XML = Get-Content Path $XMLFile.FullName ErrorAction Stop
$Recommendation = $xml.HPIA.Recommendations.BIOS.Recommendation
If ($Recommendation)
{
$CurrentBIOSVersion = $Recommendation.TargetVersion
$ReferenceBIOSVersion = $Recommendation.ReferenceVersion
$DownloadURL = "https://" + $Recommendation.Solution.Softpaq.Url
$SoftpaqFileName = $DownloadURL.Split('/')[-1]
Write-Log Message "Current BIOS version is $CurrentBIOSVersion" Component "Analyze"
Write-Log Message "Recommended BIOS version is $ReferenceBIOSVersion" Component "Analyze"
Write-Log Message "Softpaq download URL is $DownloadURL" Component "Analyze"
}
Else
{
Write-Log Message "Failed to find a BIOS recommendation in the XML report" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to parse the XML file: $($_.Exception.Message)" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
}
Else
{
Write-Log Message "Failed to find an XML report." Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to find an XML report: $($_.Exception.Message)" Component "Analyze" LogLevel 3
UploadLogFilesToAzure
throw
}
###############################
## Download the BIOS softpaq ##
###############################
Write-Log Message "Downloading the Softpaq" Component "DownloadBIOSUpdate"
try
{
$ExistingBitsJob = Get-BitsTransfer Name "$SoftpaqFileName" AllUsers ErrorAction SilentlyContinue
If ($ExistingBitsJob)
{
Write-Log Message "An existing BITS tranfer was found. Cleaning it up." Component "DownloadBIOSUpdate" LogLevel 2
Remove-BitsTransfer BitsJob $ExistingBitsJob
}
$BitsJob = Start-BitsTransfer Source $DownloadURL Destination $WorkingDirectory\$SoftpaqFileName Asynchronous DisplayName "$SoftpaqFileName" Description "BIOS update download" RetryInterval 60 ErrorAction Stop
do {
Start-Sleep Seconds 5
$Progress = [Math]::Round((100 * ($BitsJob.BytesTransferred / $BitsJob.BytesTotal)),2)
Write-Log Message "Downloaded $Progress`%" Component "DownloadBIOSUpdate"
} until ($BitsJob.JobState -in ("Transferred","Error"))
If ($BitsJob.JobState -eq "Error")
{
Write-Log Message "BITS tranfer failed: $($BitsJob.ErrorDescription)" Component "DownloadBIOSUpdate" LogLevel 3
UploadLogFilesToAzure
throw
}
Write-Log Message "Download is finished" Component "DownloadBIOSUpdate"
Complete-BitsTransfer BitsJob $BitsJob
Write-Log Message "BITS transfer is complete" Component "DownloadBIOSUpdate"
}
catch
{
Write-Log Message "Failed to start a BITS transfer for the BIOS update: $($_.Exception.Message)" Component "DownloadBIOSUpdate" LogLevel 3
UploadLogFilesToAzure
throw
}
#########################
## Extract BIOS Update ##
#########################
Write-Log Message "Extracting the BIOS Update" Component "ExtractBIOSUpdate"
$BIOSUpdateDirectoryName = $SoftpaqFileName.Split('.')[0]
try
{
$Process = Start-Process FilePath $WorkingDirectory\$SoftpaqFileName WorkingDirectory $WorkingDirectory ArgumentList "/s /f .\$BIOSUpdateDirectoryName\ /e" NoNewWindow PassThru Wait ErrorAction Stop
Start-Sleep Seconds 5
$HpFirmwareUpdRec = Get-ChildItem Path $WorkingDirectory Include HpFirmwareUpdRec.exe Recurse ErrorAction SilentlyContinue
$HPBIOSUPDREC = Get-ChildItem Path $WorkingDirectory Include HPBIOSUPDREC.exe Recurse ErrorAction SilentlyContinue
If ($HpFirmwareUpdRec)
{
$BIOSExecutable = $HpFirmwareUpdRec
}
ElseIf ($HPBIOSUPDREC)
{
$BIOSExecutable = $HPBIOSUPDREC
}
Else
{
Write-Log Message "BIOS update executable not found!" Component "ExtractBIOSUpdate" LogLevel 3
UploadLogFilesToAzure
throw
}
Write-Log Message "Extraction complete" Component "ExtractBIOSUpdate"
}
catch
{
Write-Log Message "Failed to extract the softpaq: $($_.Exception.Message)" Component "ExtractBIOSUpdate" LogLevel 3
UploadLogFilesToAzure
throw
}
#############################
## Check for BIOS password ##
#############################
try
{
$SetupPwd = (Get-CimInstance Namespace ROOT\HP\InstrumentedBIOS ClassName HP_BIOSPassword Filter "Name='Setup Password'" ErrorAction Stop).IsSet
If ($SetupPwd -eq 1)
{
Write-Log Message "The BIOS has a password set" Component "BIOSPassword"
$BIOSPasswordSet = $true
}
Else
{
Write-Log Message "No password has been set on the BIOS" Component "BIOSPassword"
}
}
catch
{
Write-Log Message "Unable to determine if a BIOS password has been set: $($_.Exception.Message)" Component "BIOSPassword" LogLevel 3
UploadLogFilesToAzure
throw
}
##########################
## Create password file ##
##########################
If ($BIOSPasswordSet)
{
Write-Log Message "Creating an encrypted password file" Component "BIOSPassword"
$HpqPswd = Get-ChildItem Path $WorkingDirectory Include HpqPswd.exe Recurse ErrorAction SilentlyContinue
If ($HpqPswd)
{
try
{
$Process = Start-Process FilePath $HpqPswd.FullName WorkingDirectory $WorkingDirectory ArgumentList "-p""$BIOSPassword"" -f.\password.bin -s" NoNewWindow PassThru Wait ErrorAction Stop
Start-Sleep Seconds 5
If (Test-Path $WorkingDirectory\password.bin)
{
Write-Log Message "File successfully created" Component "BIOSPassword"
}
Else
{
Write-Log Message "Encrypted password file could not be found!" Component "BIOSPassword" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to create an encrypted password file: $($_.Exception.Message)" Component "BIOSPassword" LogLevel 3
UploadLogFilesToAzure
throw
}
}
else
{
Write-Log Message "Failed to locate HP password encryption utility!" Component "BIOSPassword" LogLevel 3
UploadLogFilesToAzure
throw
}
}
###########################
## Stage the BIOS update ##
###########################
Write-Log Message "Staging BIOS firmware update" Component "BIOSFlash"
try
{
If ($BIOSPasswordSet)
{
$Process = Start-Process FilePath "$WorkingDirectory\$BIOSUpdateDirectoryName\$BIOSExecutable" WorkingDirectory $WorkingDirectory ArgumentList "-s -p.\password.bin -f.\$BIOSUpdateDirectoryName -r -b" NoNewWindow PassThru Wait ErrorAction Stop
}
Else
{
$Process = Start-Process FilePath "$WorkingDirectory\$BIOSUpdateDirectoryName\$BIOSExecutable" WorkingDirectory $WorkingDirectory ArgumentList "-s -f.\$BIOSUpdateDirectoryName -r -b" NoNewWindow PassThru Wait ErrorAction Stop
}
If ($Process.ExitCode -eq 3010)
{
Write-Log Message "The update has been staged. The BIOS will be updated on restart" Component "BIOSFlash"
}
Else
{
Write-Log Message "An unexpected exit code was returned: $($Process.ExitCode)" Component "BIOSFlash" LogLevel 3
UploadLogFilesToAzure
throw
}
}
catch
{
Write-Log Message "Failed to stage BIOS update: $($_.Exception.Message)" Component "BIOSFlash" LogLevel 3
UploadLogFilesToAzure
throw
}
Write-Log Message "This BIOS update run is complete. Have a nice day!" Component "Completion"
UploadLogFilesToAzure

Windows 10 Upgrades – Dealing with Safeguard ID 25178825 (Conexant ISST Driver)

I saw a tweet recently from Madhu Sanke where he had deployed updated Conexant ISST drivers to his environment to release devices from the Safeguard ID 25178825 which at the time of writing still prevents devices trying to upgrade to 2004 or 20H2.

You can read more about the issue here, but around September/October 2020 timeframe, newer drivers became available that are not affected by this Safeguard and updating the drivers will release a device from this hold.

Madhu took the approach of downloading the drivers from the Microsoft Update Catalog and packaging them with a script wrapper for the install. In researching this myself, I found that there are more than one driver available and different models will take different drivers, so I decided to write a little C# program that will update a device using Windows Update directly instead.

The program simply connects to Windows Update online, checks if a newer driver version is available for the Conexant ISST audio driver (listed under ‘Conexant – MEDIA – ‘ in the MS update catalog), and downloads and installs it.

The executable can then be deployed as is using a product like Microsoft Endpoint Configuration Manager.

For environments where software updates are deployed with Configuration Manager / WSUS, the program will check if registry keys have been set preventing access to Windows Update online and temporarily open them. It will restore the previous settings after updating.

The program also logs to the Temp directory.

On my HP laptops, the driver in question is this guy:

This one has been updated and is no longer affected by the Safeguard.

You can download the C# executable from my GitHub repo, or you can clone the solution in Visual Studio and compile your own executable. Just remember to run the program in SYSTEM context or with administrative privilege on the client as it’s installing a driver.

A reboot is usually required after installation and the driver can display a small one-time toast notification like this:

If your devices are managed by Windows Update for Business, you may see a notification from there as well, depending on your configuration of course.

After deploying the driver update to affected devices, expect to see them being released from the Safeguard after telemetry has run. You can use my PowerBI reports to help with reporting on Safeguards in your environment.

Thanks again to Madhu for the inspiration!

Downloading the Latest Dell Driver Packs with PowerShell

It was a regular Tuesday morning and I hadn’t yet had my ‘PowerShell fix’ for the week, so when I realised I needed to download a new driver pack from Dell for my ConfigMgr OS deployments, I could hear a faint voice calling out to me: ‘Dude, I can make your life easier! Work smarter, not harder!

Of course, that’s only ever partially true, because with PowerShell you must work harder today in order to work smarter tomorrow, but in the interest of long-term benefit I proceeded to fire up the ISE.

Suddenly, a thought arose from my subconscious: ‘Wait just a second. Don’t re-invent the wheel.  Aren’t there already some good solutions out there for this?

Well yes, that’s true,‘ my internal musings continued. ‘Most notably, we have a very cool tool by Maurice Daly – the Driver Automation Tool. With this we can just click buttons and go get coffee while the tool does all the hard work. It’ll even import the driver packs into SCCM. I like that!

Yes that is awesome.‘ I responded to myself, ‘Problem is, I still need a PowerShell fix. So maybe I can find a different way of downloading driver packs. What do you suggest?

Well, we have the Dell Driver Pack Catalog. Dell even provide examples of how to use that with PowerShell to find the URLs you need to download the relevant cab files.

Yes, this is cool too. But I think there is still another way. Doesn’t Dell’s TechCenter wiki contain the download URLs for the most recent driver packs?

Yes, it does. But you want to use PowerShell, right?

Correct.’

So what are you thinking?

Web-scraping.

Ah, you bad boy! Let’s do it!

Dell maintains a wiki page containing links to the latest driver packs which can be found here:

http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment

You simply find the model and operating system version you want and click the link, which leads you to another wiki page containing a download URL.

Rolling up the sleeves, I whipped up some code that will scrape these web pages to find the download URL for the current driver pack version and download it using a BITS transfer. You can then import or add the driver pack into ConfigMgr for OSD using your favourite method (which is PowerShell, right?!)

The resulting script is quite simple to use and works reliably in my testing, although it takes a few seconds to filter the HTML in order to find the appropriate download URL.

You can download a driver pack for a single model, for example:


Download-LatestDellDriverPack -Model "Latitude E7470" -OperatingSystem 'Windows 10' -DownloadDirectory C:\DriverPacks -Verbose

Just provide the model name, operating system version and a location to save the downloaded file to. Support is provided for verbose output.

You can also pass a list of models to the script and it will download each one in turn, for example:


"M4800","Optiplex 9020","E6420","E5250" | Download-LatestDellDriverPack -OperatingSystem 'Windows 7' -DownloadDirectory C:\DriverPacks -Verbose

drivers

The script in action

The script will work for any driver pack with an operating system Windows 7 or higher (are you really deploying anything older than that?!), and there is no proxy support currently.

Here’s the full script:

[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true,
Position=0)]
[String[]]$Model,
[Parameter(Mandatory=$true,
Position=1)]
[ValidateSet("Windows 10","Windows 8.1","Windows 8","Windows 7")]
$OperatingSystem,
[Parameter(Mandatory=$true,
Position=2)]
[String]$DownloadDirectory
)
Begin
{
function Get-LatestDellDriverPackURL {
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[String]$Model,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
[ValidateSet("Windows 10","Windows 8.1","Windows 8","Windows 7")]
$OperatingSystem
)
# Remove the "E" prefix character from Latitude models due to some dodgy Dell URLs…
If ($Model.ToCharArray()[0] -eq "E" -and $Model -notmatch "Embedded")
{
$Model = $Model.Replace("E","")
}
# Find the specific wiki page for the model from the main wiki page
$URI = "http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment"
Try
{
$HTML = Invoke-WebRequest Uri $URI ErrorAction Stop
If ($OperatingSystem -eq "Windows 8")
{
# Filter out Windows 8.1 from the results if it's just Windows 8
$Href = $HTML.AllElements | Where {$_.innerText -match ("$Model" + " W") -and $_.innerText -match $OperatingSystem -and $_.innerText -notmatch "8.1" -and $_.innerText -match "Driver" -and $_.tagName -eq "A"} | Select ExpandProperty href
}
Else
{
$Href = $HTML.AllElements | Where {$_.innerText -match ("$Model" + " W") -and $_.innerText -match $OperatingSystem -and $_.innerText -match "Driver" -and $_.tagName -eq "A"} | Select ExpandProperty href
}
}
Catch
{
$_
Return
}
If (!$Href)
{
Write-Error "No Wiki page found for $Model and $OperatingSystem."
Return
}
# Find the download URL from the model
$URI = "http://en.community.dell.com/$Href"
Try
{
$HTML = Invoke-WebRequest Uri $URI ErrorAction Stop
$CabDownloadLink = $HTML.AllElements | Where {$_.innerHTML -match "Download Now" -and $_.tagName -eq "A"} | Select ExpandProperty href
Return $CabDownloadLink
}
Catch
{
$_
Return
}
Write-Error "No download URL found for $Model."
}
# Start a timer
$Stopwatch = New-Object System.Diagnostics.Stopwatch
$Stopwatch.Start()
}
Process
{
Foreach ($System in $Model)
{
Write-Verbose "Finding the download URL for '$System' and '$OperatingSystem'"
Try
{
# Get the download URL
$URL = Get-LatestDellDriverPackURL Model $System OperatingSystem $OperatingSystem ErrorAction Stop
}
Catch
{
$Stopwatch.Stop()
$_
Return
}
Write-Verbose "Initiating download of URL '$URL' with BITS"
Try
{
# Begin the download
Start-BitsTransfer Source $URL Destination $DownloadDirectory
}
Catch
{
$Stopwatch.Stop()
$_
Return
}
}
}
End
{
$Stopwatch.Stop()
Write-Verbose "Completed in $($Stopwatch.Elapsed.Minutes) minutes and $($Stopwatch.Elapsed.Seconds) seconds"
}

Get the Total Size of All Application Content Files in an SCCM Console Folder

Today I was installing a new ConfigMgr distribution point on a remote site and wanted to start distributing packages to it.  But since the network bandwidth is only 4Mbps to that site, rather than distribute all the required packages in one go I wanted to distribute groups of packages out of working hours when the network is more free and no-one is affected. Yes you can use bandwidth throttling on the distribution point, but I prefer to schedule my distributions out of hours so I don’t need to.

But to find out what time-frame is required to distribute my packages, I need to get an idea of how long a package distribution will take.  To do this, I use a script I published in an earlier blog (actually an updated version I haven’t published yet!) that will allow me to schedule and monitor a package distribution in SCCM, and will send me an email when the distribution is complete telling me how big the package is and how long the distribution took.  This will enable me to get a rough calculation of the amount of time needed to distribute packages, assuming I know the volume of data I need to distribute.

CaptureSo based on the time taken by this boot image, I can distribute 300MB in 20 minutes, so just less than 1GB per hour.  Of course, several factors can affect that time, but it gives a rough guide which is useful enough.

But now how do I find out the volume of data I need to distribute?  Well, lets start with the Application content packages.  I like to organise my Applications into subfolders in the SCCM console, and I have some scripts that will distribute all the packages in the console folder I choose.

Capture

Let’s choose the ‘Non-Default Apps’ folder.  This contains Applications that are not part of our default deployment image.  How do I find out the total size of all the content files in each application in that folder?

Ok, well let’s use some PowerShell 🙂  First, I need to get the ID of that console folder.

$server = "sccmsrv-01"
$SiteCode = "ABC"
$FolderName = "Non-Default Apps" # Applications\Non-Default Apps

# Get FolderID
Write-Host "Getting FolderID of Console Folder '$FolderName'"
$FolderID = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerNode where Name='$FolderName'" | Select ContainerNodeID
$FolderID = $FolderID.ContainerNodeID
Write-host "  $FolderID"

I have entered my SCCM site server name, site code, and the name of the console folder as variables so the script is portable.  Then I query the WMI on the site server to get the ContainerNodeID of the console folder.

Capture

Now I can query for all Applications in that folder using the FolderID, and get the unique ID, or ‘InstanceKey’ of each one, which equates to the CI Unique ID value for the Application in the SCCM console.  You can add that value in the console from the available fields.

# Get InstanceKey of Folder Members
Write-Host "Getting Members of Folder"
$FolderMembers = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerItem where ContainerNodeID='$FolderID'" | Select InstanceKey
$FolderMembers = $FolderMembers.InstanceKey
write-host "  Found $($FolderMembers.Count) applications"

Capture

Cool, it’s found 21 Applications in that folder.  Now we need to translate the Unique ID of each Application to its friendly name.  There’s more than one way to do that, but I chose to use the SMS_ObjectName class:

# Get Application name of each Folder member
write-host "Getting Application Names"
$NameList = @()
foreach ($foldermember in $foldermembers)
    {
        $Name = Get-wmiobject -Namespace "ROOT\SMS\Site_$SiteCode" -Query "select Name from SMS_ObjectName where ObjectKey='$foldermember'" | Select -ExpandProperty Name
        $NameList += $Name
    }
$namelist = $NameList | sort

Now that we have the list of Applications we need to find out the size of the content files in the deployment types for each Application.  The slightly annoying thing is that this information is serialized into xml format and stored in a property called SDMPackageXML, so we need to deserialize that property.

First we will import a .Net assembly and add an accelerator to perform deserialization.

# import assemblies
[System.Reflection.Assembly]::LoadFrom(“C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\Microsoft.ConfigurationManagement.ApplicationManagement.dll”) | Out-Null

# Creating Type Accelerators
$accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
$accelerators::Add('SccmSerializer',[type]'Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer')

Now we can loop through each Application, deserialize the SDMPackageXML property, loop through each deployment type that might be present, and retrieve the size of each file in the contents of each deployment type, adding the size values together as we go and storing them in a variable.

# Deserialize each SDMPackageXML property, and get the file size of each file in the contents for each deployment type
$totalsize = 0
foreach ($name in $namelist)
    {
        write-host "  Deserializing $name"
        $app = [wmi](gwmi -ComputerName $server -Namespace root\sms\site_$code -class sms_application | ?{$_.LocalizedDisplayName -eq $Name -and $_.IsLatest -eq $true}).__Path
        $appXML = [SccmSerializer]::DeserializeFromString($app.SDMPackageXML,$true)
        $DTs = $appxml.DeploymentTypes
        foreach ($DT in $DTs)
            {
                $sizes = $dt.Installer.Contents.Files.Size
                foreach ($size in $sizes)
                    {$totalsize = $totalsize + $size}
            }
    }

Now let’s output the results:

write-host "Total Size of all content files for every application in the '$FolderName' folder is:" -ForegroundColor Green
write-host "$(($totalsize / 1GB).ToString(".00")) GB" -ForegroundColor Green

Capture

So based on my initial test, it could take around 15 hours to distribute 13.4 GB of data!  Wow, better schedule it for the weekend!

Here is the full script:

<#

This script gets the total size of all the content files for each deployment type for each application in the console folder you specify.

#>

$server = "sccmsrv-01"
$SiteCode = "ABC"
$FolderName = "Non-Default Apps" # Applications\Default Apps

# import assemblies
[System.Reflection.Assembly]::LoadFrom(“C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\Microsoft.ConfigurationManagement.ApplicationManagement.dll”) | Out-Null

# Creating Type Accelerators
$accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
$accelerators::Add('SccmSerializer',[type]'Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer')

# Get FolderID
Write-Host "Getting FolderID of Console Folder '$FolderName'"
$FolderID = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerNode where Name='$FolderName'" | Select ContainerNodeID
$FolderID = $FolderID.ContainerNodeID
Write-host "  $FolderID"

# Get InstanceKey of Folder Members
Write-Host "Getting Members of Folder"
$FolderMembers = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerItem where ContainerNodeID='$FolderID'" | Select * | Select InstanceKey
$FolderMembers = $FolderMembers.InstanceKey
write-host "  Found $($FolderMembers.Count) applications"

# Get Application name of each Folder member
write-host "Getting Application Names"
$NameList = @()
foreach ($foldermember in $foldermembers)
    {
        $Name = Get-wmiobject -Namespace "ROOT\SMS\Site_$SiteCode" -Query "select Name from SMS_ObjectName where ObjectKey='$foldermember'" | Select -ExpandProperty Name
        $NameList += $Name
    }
$namelist = $NameList | sort

# Deserialize each SDMPackageXML property, and get the file size of each file in the contents for each deployment type
$totalsize = 0
foreach ($name in $namelist)
    {
        write-host "  Deserializing $name"
        $app = [wmi](gwmi -ComputerName $server -Namespace root\sms\site_$code -class sms_application | ?{$_.LocalizedDisplayName -eq $Name -and $_.IsLatest -eq $true}).__Path
        $appXML = [SccmSerializer]::DeserializeFromString($app.SDMPackageXML,$true)
        $DTs = $appxml.DeploymentTypes
        foreach ($DT in $DTs)
            {
                $sizes = $dt.Installer.Contents.Files.Size
                foreach ($size in $sizes)
                    {$totalsize = $totalsize + $size}
            }
    }

write-host "Total Size of all content files for every application in the '$FolderName' folder is:" -ForegroundColor Green
write-host "$(($totalsize / 1GB).ToString(".00")) GB" -ForegroundColor Green

And here is a script I use to distribute all the packages in a console folder to a single distribution point:

$DP = "sccmsrvdp-02v.contoso.com"
$SiteCode = "ABC"
$FolderName = "Default Apps" # Applications\Default Apps

Write-Host "Getting FolderID of Console Folder '$FolderName'"
$FolderID = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerNode where Name='$FolderName'" | Select ContainerNodeID
$FolderID = $FolderID.ContainerNodeID
Write-host "  $FolderID"

Write-Host "Getting Members of Folder"
$FolderMembers = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_ObjectContainerItem where ContainerNodeID='$FolderID'" | Select InstanceKey
$FolderMembers = $FolderMembers.InstanceKey

write-host "Getting App Names"
foreach ($Folder in $FolderMembers)
{
$App = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_Application where ModelName='$Folder'" | Select LocalizedDisplayName
$App = $App.LocalizedDisplayName
Write-host "Distributing content for $App to $DP"
Start-CMContentDistribution -ApplicationName $App -DistributionPointName $DP
}

You can distribute any kind of package this way, for example to distribute driver packages instead, query the SMS_DriverPackage class. Replace the last section of the script with:

write-host "Getting DriverPack Names"
foreach ($Folder in $FolderMembers)
{
$DriverPack = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode" `
-Query "select * from SMS_DriverPackage where PackageID='$Folder'" | Select Name
$DriverPack = $DriverPack.Name
Write-host "Distributing content for $DriverPack to $DP"
Start-CMContentDistribution -DriverPackageName $DriverPack -DistributionPointName $DP
}

Installing Drivers with Standalone Media Deployments

Using SCCM standalone media can be a good way to deploy an OS when you have, for example, remote offices with no local distribution point.  However, if you use DVD media it can result in several DVDs if you include all the applications and drivers.  So I prefer to install the drivers and applications separately after deployment and keep the OS install on one or two DVDs. As we use Dell computers, it’s quite easy to get hold of the driver pack from the Dell website, which you can then install using PnPUnattend.  To speed up the process, I wrote a simple batch script that will download the relevant driver pack from Dell and install it.

This process can also be used for non-Dell models, you simply download the drivers you need, extract them, place them in the C:\Drivers folder, add the registry key, and run the PnPUnattend command.

Note: You need to have at least a NIC driver installed before running the script, so you can connect to the internet.

@echo off
cls
:Start
echo *********************************
echo Download and Install Dell Drivers
echo *********************************
echo _
echo 1. E4200
echo 2. E6220
echo 3. E6320
echo 4. E6410
echo 5. E6420
echo 6. E6430
echo 7. E6430s
echo 8. E6530
echo 9. Optiplex 9010
echo 10. Optiplex 9020
echo 11. Precision T5500
echo 12. E7240
echo 13. E7440
echo 14. E6440
echo 15. E6540
echo 16. E6230
echo 17. E6520
echo 18. Optiplex 990
echo _
set /p choice=Which Model do you wish to install Drivers for?
if '%choice%'=='1' goto E4200
if '%choice%'=='2' goto E6220
if '%choice%'=='3' goto E6320
if '%choice%'=='4' goto E6410
if '%choice%'=='5' goto E6420
if '%choice%'=='6' goto E6430
if '%choice%'=='7' goto E6430s
if '%choice%'=='8' goto E6530
if '%choice%'=='9' goto 9010
if '%choice%'=='10' goto 9020
if '%choice%'=='11' goto T5500
if '%choice%'=='12' goto E7240
if '%choice%'=='13' goto E7440
if '%choice%'=='14' goto E6440
if '%choice%'=='15' goto E6540
if '%choice%'=='16' goto E6230
if '%choice%'=='17' goto E6520
if '%choice%'=='18' goto 990
cls
echo !! "%choice%" is not a valid choice! Please try again"!
echo _
goto Start

mkdir C:\DriverPacks
mkdir C:\Drivers

:E4200

echo Downloading Driver cab from Dell for E4200...
echo _
start /wait bitsadmin /transfer E4200Drivers /download /priority normal http://downloads.dell.com/FOLDER00680122M/1/E4200-win7-A03-M90D4.CAB C:\DriverPacks\E4200-win7-A03-M90D4.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E4200-win7-A03-M90D4.CAB -f:* C:\Drivers
goto Install_Drivers

:E6220

echo Downloading Driver cab from Dell for E6220...
echo _
start /wait bitsadmin /transfer E6220Drivers /download /priority normal http://downloads.dell.com/FOLDER01449232M/1/E6220-win7-A08-0VK93.CAB C:\DriverPacks\E6220-win7-A08-0VK93.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6220-win7-A08-0VK93.CAB -f:* C:\Drivers
goto Install_Drivers

:E6320

echo Downloading Driver cab from Dell for E6320...
echo _
start /wait bitsadmin /transfer E6320Drivers /download /priority normal http://downloads.dell.com/FOLDER01440819M/1/E6320-win7-A10-2H76N.CAB C:\DriverPacks\E6320-win7-A10-2H76N.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6320-win7-A10-2H76N.CAB -f:* C:\Drivers
goto Install_Drivers

:E6410

echo Downloading Driver cab from Dell for E6410...
echo _
start /wait bitsadmin /transfer E6410Drivers /download /priority normal http://downloads.dell.com/FOLDER01384339M/1/E6410-win7-A11-D2H6P.CAB C:\DriverPacks\E6410-win7-A11-D2H6P.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6410-win7-A11-D2H6P.CAB -f:* C:\Drivers
goto Install_Drivers

:E6420

echo Downloading Driver cab from Dell for E6420...
echo _
start /wait bitsadmin /transfer E6420Drivers /download /priority normal http://downloads.dell.com/FOLDER01440751M/1/E6420-win7-A10-KFNW7.CAB C:\DriverPacks\E6420-win7-A10-KFNW7.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6420-win7-A10-KFNW7.CAB -f:* C:\Drivers
goto Install_Drivers

:E6430

echo Downloading Driver cab from Dell for E6430...
echo _
start /wait bitsadmin /transfer E6430Drivers /download /priority normal http://downloads.dell.com/FOLDER01659570M/1/E6430-win7-A07-KGR03.CAB C:\DriverPacks\E6430-win7-A07-KGR03.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6430-win7-A07-KGR03.CAB -f:* C:\Drivers
goto Install_Drivers

:E6430s

echo Downloading Driver cab from Dell for E6430s...
echo _
start /wait bitsadmin /transfer E6430SDrivers /download /priority normal http://downloads.dell.com/FOLDER01675322M/1/E6430s-win7-A05-GY4MD.CAB C:\DriverPacks\E6430s-win7-A05-GY4MD.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6430s-win7-A05-GY4MD.CAB -f:* C:\Drivers
goto Install_Drivers

:E6530

echo Downloading Driver cab from Dell for E6530...
echo _
start /wait bitsadmin /transfer E6530Drivers /download /priority normal http://downloads.dell.com/FOLDER01658654M/1/E6530-win7-A08-MK7V1.CAB C:\DriverPacks\E6530-win7-A08-MK7V1.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6530-win7-A08-MK7V1.CAB -f:* C:\Drivers
goto Install_Drivers

:9010

echo Downloading Driver cab from Dell for Optiplex 9010...
echo _
start /wait bitsadmin /transfer 9010Drivers /download /priority normal http://downloads.dell.com/FOLDER01566211M/1/9010-win7-A05-X8DTY.CAB C:\DriverPacks\9010-win7-A05-X8DTY.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\9010-win7-A05-X8DTY.CAB -f:* C:\Drivers
goto Install_Drivers

:9020

echo Downloading Driver cab from Dell for Optiplex 9020...
echo _
start /wait bitsadmin /transfer 9020Drivers /download /priority normal http://downloads.dell.com/FOLDER01764360M/1/9020-win7-A01-4CG9T.CAB C:\DriverPacks\9020-win7-A01-4CG9T.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\9020-win7-A01-4CG9T.CAB -f:* C:\Drivers
goto Install_Drivers

:T5500

echo Downloading Driver cab from Dell for T5500...
echo _
start /wait bitsadmin /transfer T5500Drivers /download /priority normal http://downloads.dell.com/sysman/T5500-Win7-A00-R253041.CAB C:\DriverPacks\T5500-Win7-A00-R253041.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\T5500-Win7-A00-R253041.CAB -f:* C:\Drivers
goto Install_Drivers

:E7240

echo Downloading Driver cab from Dell for E7240...
echo _
start /wait bitsadmin /transfer E7240Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01819337M/1/E7240-win7-A01-XK9P3.CAB C:\DriverPacks\E7240-win7-A01-XK9P3.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E7240-win7-A01-XK9P3.CAB -f:* C:\Drivers
goto Install_Drivers

:E7440

echo Downloading Driver cab from Dell for E7440...
echo _
start /wait bitsadmin /transfer E7440Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01819350M/1/E7440-win7-A01-XT5VG.CAB C:\DriverPacks\E7440-win7-A01-XT5VG.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E7440-win7-A01-XT5VG.CAB -f:* C:\Drivers
goto Install_Drivers

:E6440

echo Downloading Driver cab from Dell for E6440...
echo _
start /wait bitsadmin /transfer E6440Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01832732M/1/E6440-win7-A01-5DDKN.CAB C:\DriverPacks\E6440-win7-A01-5DDKN.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6440-win7-A01-5DDKN.CAB -f:* C:\Drivers
goto Install_Drivers

:E6540

echo Downloading Driver cab from Dell for E6540...
echo _
start /wait bitsadmin /transfer E6540Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01832723M/1/E6540-win7-A02-JF7Y8.CAB C:\DriverPacks\E6540-win7-A02-JF7Y8.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6540-win7-A02-JF7Y8.CAB -f:* C:\Drivers
goto Install_Drivers

:E6230

echo Downloading Driver cab from Dell for E6230...
echo _
start /wait bitsadmin /transfer E6230Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01692655M/1/E6230-win7-A05-7D8JC.CAB C:\DriverPacks\E6230-win7-A05-7D8JC.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6230-win7-A05-7D8JC.CAB -f:* C:\Drivers
goto Install_Drivers

:E6520

echo Downloading Driver cab from Dell for E6520...
echo _
start /wait bitsadmin /transfer E6520Drivers /download /priority normal http://downloads-us.dell.com/FOLDER01819787M/1/E6520-win7-A10-P2X6K.CAB C:\DriverPacks\E6520-win7-A10-P2X6K.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\E6520-win7-A10-P2X6K.CAB -f:* C:\Drivers
goto Install_Drivers

:990

echo Downloading Driver cab from Dell for Optiplex 990...
echo _
start /wait bitsadmin /transfer 990Drivers /download /priority normal http://downloads.dell.com/FOLDER01834164M/1/990-win7-A09-6DHG2.CAB C:\DriverPacks\990-win7-A09-6DHG2.CAB
echo _
echo Expanding Cab file...
echo _
start /wait expand C:\DriverPacks\990-win7-A09-6DHG2.CAB -f:* C:\Drivers
goto Install_Drivers

:Install_Drivers

echo _
echo Configuring Registry..
echo _
REG ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UnattendSettings\PnPUnattend\DriverPaths\1" /v Path /t REG_SZ /d "C:\Drivers"
echo _
Echo Initiating PNP driver installation...
echo _
start /wait C:\Windows\System32\pnpunattend.exe auditsystem /l
goto End
:End

rmdir C:\DriverPacks /S /Q
rmdir C:\Drivers /S /Q

pause