Automating MDT Drivers with The HP Client Management Script Library

Recently I was asked to give a demo on MDT driver management best practices. Truth be told I’d not used MDT for actual hardware in several years. Sure, I use it as a golden image factory, but for bare metal hardware deployments, I’ve been using SCCM for several years and given MDT little thought. I had a few days to prepare for the demo so I figured I’d setup a new deployment share.
 

Historically we’ve always recommended using the Win32_baseboard product value to control driver injection (MDT or SCCM). MDT sets this value to %product% when running the Gather script. The traditional “Total Control” approach to using this value for driver injection involves multiple driver injection steps pointing to multiple selection profiles with conditions that state “product equals 8470.”

This method gives fantastic control over what ends up injected into an image, but is a pain to maintain. Add to that, selection profiles in general are a pain. There has to be a better way.

In this post I’m going to borrow insipration from the blog posts of a few different people:

 

The Goals:

  • I want to set up my driver injection task sequence once
  • I want an automated way to import drivers into MDT
  • I want to leverage the Win32_Baseboard product value
  • I want to maintain driver pack version control
  • I want the folders in the deployment console to be human readable (I can never remember the conversion from product to model name)

 

Deployment Share Setup:

  • Create a Win10 folder in your Out-of-Box Drivers folder
  • Create a WinPE 10 with x86 and x64 folders under it
  • Create 2 WinPE selection profiles for the folders
  • WinPE 10 x86
  • WinPE 10 x64
  • In the deployment share properties assign each boot image this share will support to those selection profiles.
  • Import only WinPE 10 drivers into the Winpe 10 folders. See https://ftp.hp.com/pub/caps-softpaq/cmit/softpaq/WinPE10.html for HP’s WinPE 10 drivers packs. 1.70 is current at the time of this writing.

 

Task Sequence Set Up:

  • Leverage the Set Task Sequence Variable step to assign Drivergroup001 to Win10\%Product%
     

 

  • Set the Inject Drivers task sequence to:
    • Selection profile “Nothing”
    • Install all drivers from the selection profile

 

The Script:

# Very little of this script is my work @nkofahl - Nathan Kofahl, HP inc. Most of it I stole with pride from people mentioned in these comments. 
# model tabel and download and extact logic from @gwblok - GARYTOWN.COM He built an excellent script for importing drivers into SCCM 
# HP Client Management Script Library https://ftp.hp.com/pub/caps-softpaq/cmit/hp-cmsl.html 
#Logging: The Log function was created by Ryan Ephgrave (@ephingposh) https://www.ephingadmin.com/powershell-cmtrace-log-function/


#Script Variables 
$scriptName = "MDT Driverpack Download"
$OS = "Win10"
$OSVER = "1809"
$LogFile = "$PSScriptRoot\MDTDriverPackDownload.log"
$DownloadDir = "E:\Driversource\softpaqs"
$ExtractedDir = "E:\driversource\Extracted"
$MDTModule = "C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"
$deploymentshare = "E:\DeploymentShare"




#Reset Vars
$DriverPack = ""
$Model = ""



#region: CMTraceLog Function formats logging in CMTrace style
function CMTraceLog {
	[CmdletBinding()]
	param(
		[Parameter(Mandatory = $false)]
		$Message,

		[Parameter(Mandatory = $false)]
		$ErrorMessage,

		[Parameter(Mandatory = $false)]
		$Component = "HP BIOS Downloader",

		[Parameter(Mandatory = $false)]
		[int]$Type,

		[Parameter(Mandatory = $true)]
		$LogFile
	)
	<#
    Type: 1 = Normal, 2 = Warning (yellow), 3 = Error (red)
    #>
	$Time = Get-Date -Format "HH:mm:ss.ffffff"
	$Date = Get-Date -Format "MM-dd-yyyy"

	if ($ErrorMessage -ne $null) { $Type = 3 }
	if ($Component -eq $null) { $Component = " " }
	if ($Type -eq $null) { $Type = 1 }

	$LogMessage = "<![LOG[$Message $ErrorMessage" + "]LOG]!><time=`"$Time`" date=`"$Date`" component=`"$Component`" context=`"`" type=`"$Type`" thread=`"`" file=`"`">"
	$LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile
}

function Get-FolderSize {
	[CmdletBinding()]
	param(
		[Parameter(Mandatory = $true,ValueFromPipeline = $true)]
		$Path,
		[ValidateSet("KB","MB","GB")]
		$Units = "MB"
	)
	if ((Test-Path $Path) -and (Get-Item $Path).PSIsContainer) {
		$Measure = Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum
		$Sum = $Measure.Sum / "1$Units"
		[pscustomobject]@{
			"Path" = $Path
			"Size($Units)" = $Sum
		}
	}
}

CMTraceLog -Message "Starting Script: $scriptName" -Type 1 -LogFile $LogFile
Write-Output "Starting Script: $scriptName"

Import-Module $MDTModule -Verbose

if (!(Get-PSDrive -LiteralName PSDeploymentshare -ErrorAction 'silentlycontinue')) {
	New-PSDrive -Name "PSDeploymentShare" -PSProvider MDTProvider -Root $deploymentshare
	Write-Host Adding MDT Deploymentshare
	CMTraceLog -Message "Adding $deploymentshare as a PSdrive" -Type 1 -LogFile $LogFile
}
else
{
	Remove-PSDrive PSDeploymentshare
	Write-Host removing MDT Deploymentshare
	New-PSDrive -Name "PSDeploymentShare" -PSProvider MDTProvider -Root $deploymentshare
	Write-Host Re-adding MDT Deploymentshare
	CMTraceLog -Message "Adding $deploymentshare as a PSdrive" -Type 1 -LogFile $LogFile
}


$HPModelsTable = @(
	@{ ProdCode = '8438'; Model = "HP EliteBook x360 1030 G3" }
	@{ ProdCode = '844F'; Model = "HP ZBook Studio x360 G5" }
	@{ ProdCode = '83B3'; Model = "HP ELITEBOOK 830 G5" }
	@{ ProdCode = '83B2'; Model = "HP ELITEBOOK 840 G5" }
	@{ ProdCode = '8470'; Model = "HP ELITEBOOK X360 1040 G5" }
)


foreach ($HPModel in $HPModelsTable)
{
	Write-Output "Checking Model $($HPModel.Model) Product Code $($HPModel.ProdCode) for Driver Pack Updates"
	CMTraceLog -Message "Checking Model $($HPModel.Model) Product Code $($HPModel.ProdCode) for Driver Pack Updates" -Type 1 -LogFile $LogFile
	$SoftPaq = Get-SoftpaqList -platform $HPModel.ProdCode -os $OS -osver $OSVER
	$DriverPack = $SoftPaq | Where-Object { $_.category -eq 'Manageability - Driver Pack' }
	$DriverPack = $DriverPack | Where-Object { $_.Name -notmatch "Windows PE" }
	$DriverPack = $DriverPack | Where-Object { $_.Name -notmatch "WinPE" }
	$DownloadDriverPackRootArchiveFullPath = "$($DownloadDir)\$($HPModel.Model)\$($DriverPack.Version)"
	$DownloadDriverPackExtractFullPath = "$($ExtractedDir)\$($HPModel.Model)"
	$MDTTargetFolder = "PSDeploymentShare:\Out-of-Box Drivers\$OS\$($HPModel.ProdCode)\$($HPModel.Model)_$($Driverpack.Version)"

	if ($DriverPack) {
		if (Test-Path $MDTTargetFolder) {
			CMTraceLog -Message "$($driverpack.Version) already exists for $($HPModel.Model)" -Type 1 -LogFile $LogFile
			Write-Output "$($HPModel.Model) Driverpack already Current"
		}
		else
		{
			CMTraceLog -Message "Found $DriverPack for $($HPModel.Model) Product Code $($HPModel.ProdCode)" -Type 1 -LogFile $LogFile

			if (-not (Test-Path $DownloadDriverPackRootArchiveFullPath)) { New-Item -Path $DownloadDriverPackRootArchiveFullPath -ItemType Directory -Force }
			$SaveAs = "$($DownloadDriverPackRootArchiveFullPath)\$($DriverPack.id).exe"
			Get-Softpaq -number $DriverPack.id -saveAs $SaveAs -overwrite yes
			if (Test-Path $DownloadDriverPackExtractFullPath) { Remove-Item -Path $DownloadDriverPackExtractFullPath -Recurse -Force -ErrorAction SilentlyContinue }
			New-Item $DownloadDriverPackExtractFullPath -ItemType Directory -Force

			$TempDir = "$($env:temp)\SPExtract\$($HPModel.ProdCode)"
			if (Test-Path $TempDir) { Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue }
			New-Item $TempDir -ItemType Directory -Force
			Start-Process $SaveAs -ArgumentList "-e -s -f$($TempDir)" -Wait
			$CopyFromDir = (Get-ChildItem -Path ((Get-ChildItem -Path $TempDir -Directory).FullName) -Directory).FullName
			Copy-Item "$($CopyFromDir)\*" -Destination $DownloadDriverPackExtractFullPath -Force -Recurse
			Export-Clixml -InputObject $DriverPack -Path "$($DownloadDriverPackExtractFullPath)\DriverPackInfo.XML"
			if (Test-Path $TempDir) { Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue }



			#remove old driver paths in $deploymentshare 
			$oldpath = "PsDeploymentShare:\Out-of-Box Drivers\$($OS)\$($HPModel.ProdCode)"
			if (Test-Path $oldpath) { Remove-Item -Path "$oldpath" -Recurse -ErrorAction SilentlyContinue }
			Write-Output "Removing out of date Drivers and creating new folder $MDTTargetFolder"
			CMTraceLog -Message "Removing out of date Drivers and creating new folder $MDTTargetFolder" -Type 1 -LogFile $LogFile
			New-Item -Path "PsDeploymentShare:\Out-of-Box Drivers\$OS" -enable "True" -Name "$($HPModel.ProdCode)" -Comments "Product Code for $($HPModel.Model)" -ItemType "folder" -Verbose
			New-Item -Path "PsDeploymentShare:\Out-of-Box Drivers\$OS\$($HPModel.ProdCode)\" -Name "$($HPModel.Model)_$($Driverpack.Version)" -Comments "HP Model and Version of Driver Pack" -ItemType "folder" -Verbose

			#import updated drivers into deployment share  
			Write-Output "Importing $($Driverpack.id).exe into MDT $MDTTargetFolder"
			CMTraceLog -Message "Importing $($Driverpack.id).exe into MDT $MDTTargetFolder" -Type 1 -LogFile $LogFile
			import-mdtdriver -Path "PSDeploymentShare:\Out-of-Box Drivers\$OS\$($HPModel.ProdCode)\$($HPModel.Model)_$($Driverpack.Version)" -SourcePath "$DownloadDriverPackExtractFullPath" –Verbose


			#Driver Pack File Clean Up
			Write-Output "Deleteing $saveas and $DownloadDriverPackExtractFullPath"
			CMTraceLog -Message "Deleteing $saveas and $DownloadDriverPackExtractFullPath" -Type 1 -LogFile $LogFile
			Remove-Item -Path $SaveAs -Force
			Remove-Item -Path $DownloadDriverPackExtractFullPath -Force -Recurse
		} } else
	{
		Write-Output "No Driver Pack Available for $($HPModel.Model) Product Code $($HPModel.ProdCode) $($os) $($osver) via Internet"
		CMTraceLog -Message "No Driver Pack Available for $($HPModel.Model) Product Code $($HPModel.ProdCode) $($os) $($osver) via Internet" -Type 3 -LogFile $LogFile
	}
}


#remove psdrive and exit script        
if (Get-PSDrive -LiteralName PSDeploymentshare -ErrorAction 'silentlycontinue') { Remove-PSDrive -Name "PSDeploymentShare" }
CMTraceLog -Message "Finished Script: $scriptName" -Type 1 -LogFile $LogFile
Write-Output "Finished Script: $scriptName"

 

  • Fill out the variables

  • As mentioned in the introduction I stole most of the script from the excellent work of Gary Blok. The script relies on a model table and loop code. Add the models you wish to support to the model table and let the script do the rest
     

  • Prodcode is the Win32_baseboard product value
  • Model is the human readable model name
  • An excellent place to get this information is the .cva file that accompanies any softpaq on our website. Just convert the link from .exe to .cva such as the 1903 driver pack for the Elitebook g6 series notebooks  https://ftp.hp.com/pub/softpaq/sp95501-96000/sp95820.cva
  • Scroll to the [System Information] section and you’ll see the win32_baseboard product value for each system and what is generally the marketing model name.

I

 

  • When run, the script leverages  the HP Client Management Script Library
    • Get-SoftpaqList -platform $HPModel.ProdCode -os $OS -osver $OSVER
      • This command pulls the reference XML for the platform down from our FTP site and turns each driver into a PowerShell object
    • The above is filtered via | Where-Object { $_.category -eq 'Manageability - Driver Pack'}
      • Since each PowerShell object has a category, the above filters out everything but the platform driver pack.
    • Then Get-Softpaq is leveraged to automatically download the driver pack
    • Run manually this is how those functions work

 

  • The script handles version control by appending the version of the driver pack to the model name in the deployment share

“Out-of-Box Drivers\$OS\$($HPModel.ProdCode)\$($HPModel.Model)_$($Driverpack.Version)"
 

  • When you run the script it tests the path inside the deployment share
    • If the path for the new driver pack doesn’t exist in the share it starts the download/import process for the new driver pack
    • If the version already exists in the share the script moves onto the next platform in the table.

  • If a new driver pack is found the script will remove any old driver packs in this folder \Out-of-Box Drivers\$OS\$($HPModel.ProdCode) so if you use the script to update your drivers in the deployment share you only have the current version of the driver pack inside the share.
  • The important bits are logged in a CMTrace readable file. Because of the build process for the XML sometimes a platform doesn’t have a driver pack listed for a few days. I’ve set this to be an error condition in the script which shows up in the log.


 

The Results:

  • Human readable folders in the deployment share

  • Drivergroups point to the root folder based on %product%

  • Drivers in the sub folders get injected

  • To add support for a model in my task sequence, I only have to add a platform to the model table.
  • To update the whole share to 1903, I’ll only have to update one variable in the script.
  • Note, you can change this behavior by changing the path of the script. I’m only using a single Windows 10 version in my share, but you could easily rework the script
    • Add an $OSRoot variable that is the win10 version you want to add to your deployment share
    • Change everything that references a deployment share  path containing the $OS variable to $OSRoot
       

Overall I’m happy with how this turned out. In a future post, I’ll probably cover how I setup SSM and HPIA to work with this deployment share to give me all the non-injectable stuff that needs to be installed as software for a given HP platform. The combination of one of those, with the above script, will largely get you out of the driver management business and back to doing something far more productive.


Summary image credit Tookapic, via pexels.com