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:

ConfigMgr Content Distribution Fails with 0x80041001

Today I came across an unusual issue on a couple of SCCM distribution points where two particular software update packages were failing to distribute. Using the distmgr.log and the PkgXferMgr.log on the site server, as well as the Distribution Point Job Queue Manager tool, I could see that these packages were trying to distribute again and again, but returning a failure on certain files.

In the PkgXferMgr.log I found these entries repeatedly:


ExecStaticMethod failed (80041001) SMS_DistributionPoint, FinalizeContent
CSendFileAction::SendContent failed; 0x80041001
Sending failed. Failure count = 2, Restart time = 24/04/2017 15:30:57 W. Europe Daylight Time

0x80041001 is a WMI error meaning “Generic failure” – not overly helpful.

So I went to the distribution point itself to investigate, and found these entries repeatedly in the smsdpprov.log


[BA4][Mon 04/24/2017 22:24:19]:MoveFile failed for \\?\C:\SCCMContentLib\DataLib\24688509-2940-44e9-9d7d-9a6c2e33c9a1.ABC002C2.temp to \\?\C:\SCCMContentLib\DataLib\24688509-2940-44e9-9d7d-9a6c2e33c9a1
[BA4][Mon 04/24/2017 22:24:19]:FileRename failed; 0x80070005
[BA4][Mon 04/24/2017 22:24:19]:CContentDefinition::Finalize failed; 0x80070005
[BA4][Mon 04/24/2017 22:24:19]:Failed to finalize content '24688509-2940-44e9-9d7d-9a6c2e33c9a1' for package 'ABC002C2'. Error code: 0X80070005

0x80070005 means “access denied”. So I browsed to the location in Explorer and sure enough:

pic

If I try to view the Security tab on the directory, apparently I don’t have permission:

pic2

If I click Advanced, it seems we don’t have an owner, so clearly something is corrupted as the other files and directories in this location are owned by the SYSTEM account.

pic3

If I try to change the ownership in the UI, nothing happens.

So I use PSEXEC to open a cmd prompt in SYSTEM context, and try to take ownership on one of the directories, but still no joy:

pic4

Next I run Process Explorer to find if something has a handle on it, and yes, WMI does.

pic5

So I stop the WMI service, and suddenly the directory disappears!

Start the WMI service (and it’s dependencies) and then kick off the distributions again using the DP Job Queue Manager tool, and finally the packages distribute successfully 🙂

New Free App – ConfigMgr Deployment Reporter

Just released a new free application for ConfigMgr admins – ConfigMgr Deployment Reporter.  I developed this app for use in the organisation I currently work for, and it turned out quite well, so I decided to release a public version to the community!

capture

I developed this app as an alternative (and IMO easier) way to report on ConfigMgr deployments than using the ConfigMgr console. It uses a little different format than the console node allowing you to select which deployment you wish to view data for based on the “feature type” (ie application, package etc) and report on only that deployment.  It also introduces a separation of results between all applicable systems for a deployment, and only those systems which have currently reported status, which allows for a more accurate view of the success of a deployment as it progresses.

The app allows the creation of charts and HTML-format reports to give a nice graphical snapshot of a deployment.

I also added the capability to report per-device for Software Update and Task Sequence deployments.  For Software Updates, this allows you to see which updates from the deployment are applicable to the machine and the status of each update, and for Task Sequences it allows viewing the execution status of each step in the task sequence for the selected device.

As usual, I code purely in PowerShell using WPF for the UI.  This time I added metro styling using the excellent MahApps.Metro project 🙂

Download the app from here.

Temporarily Increasing the ConfigMgr Client Cache Size for a Large Application

Recently I had to deploy an application whose content files were larger than the default SCCM client cache size (5120 MB).  This will return an error in the Software Center, such as:

0x87D01201 (The content download cannot be performed because there is not enough available space in cache or the disk is full.)

I didn’t want to permanently increase the cache size, or require that user do it manually, so I investigated some options and came up with a couple of simple PowerShell scripts that can increase or decrease the cache size.  I put these scripts into a standard package and created a program for each script using a command-line like:

powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File .\Increase-CCMCacheSize

You then have various options for how you can run that.  For example, my application was being deployed using a task sequence as there are multiple steps, so I simply right-click the task sequence and on the Advanced tab, I check the option to Run another program first:

capture

This will increase the cache size before the task sequence starts to run, which means it will no longer give an error.

To restore the cache to it’s default size after the application install, I simply add an additional step in the task sequence at the end using the package I created:

capture

I haven’t tested it, but you could do something similar with the standard package model by right-clicking the package program, and setting the Run another program first option. The only issue there is that there is no option to run the script to restore the cache size after, unless you create a kind of dependency chain, ie:

Restore Cache size > (depends on) My Package > (depends on) Increase Cache size

For applications, you could also use the capability for a dependecy chain, but you would need to create the script as an application and use a detection method.

Increase-CCMCacheSize


# Increase SCCM Client cache size to 20000 MB (20GB)
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCM.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -lt 20000)
    {
        write-host "Setting cache size to 20000"
        try
        {
            $CC.TotalSize = 20000
        }
        Catch
        {
            $_
        }
}

Restore-CCMCacheSize


# Restore SCCM Client cache size to default (5120 MB)
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCM.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -gt 5120)
    {
        write-host "Setting cache size to 5120"
        try
        {
            $CC.TotalSize = 5120
        }
        Catch
        {
            $_
        }
}

Detection Method

For an application detection method, you could also use a PowerShell script, something like this:


# Detection method to check the SCCM Client cache size
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCm.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -eq 20000)
{
    write-host "Compliant"
}
Else
{
    write-host "Not-Compliant"
}

Prompting the End-User during ConfigMgr Application Installs

As a Configuration Manager administrator, from time to time I have to deploy an application where I need to notify the end-user of something before the installation begins. A recent example was a plugin for IE that would fail to install if Internet Explorer was running at the time. I can force-ably kill the running process of course, but that’s not necessarily a nice experience for the user – without warning their browser and any open tabs get closed. So better to notify them first, and give them a chance to close the application themselves and save any work. Rather than email each targeted user and warn them to close Internet Explorer before the plugin installs (which they probably ignore or forget anyway), I wanted the installation process to handle that by some kind of prompt.

I could create a script wrapper for the plugin but that would necessitate running in the user context to display interactively. An easier way is simply to install it using a task sequence with some additional steps that will prompt the user first, kill the process if necessary, then install the plugin. A task sequence also gives me better logging.

The problem with a task sequence is that it runs in the system context, so I cannot interact with the end user who is effectively working in a different session. This can be solved however by using the ServiceUI.exe that comes with MDT. Sometime ago I wrote a post about how to prompt for input during a task sequence, but in this case I don’t want input, I simply want to use a message box.  I also want something reusable – so I don’t have to create a new package for each custom prompt.

I have a nice PowerShell function that will create a message box for me using the Wscript.shell “popup” method, so I added this function to a script, where I have also defined the message parameters I want to use at the bottom.


function New-PopupMessage {
# Return values for reference (https://msdn.microsoft.com/en-us/library/x83z1d9f(v=vs.84).aspx)

# Decimal value    Description  
# -----------------------------
# -1               The user did not click a button before nSecondsToWait seconds elapsed.
# 1                OK button
# 2                Cancel button
# 3                Abort button
# 4                Retry button
# 5                Ignore button
# 6                Yes button
# 7                No button
# 10               Try Again button
# 11               Continue button

# Define Parameters
[CmdletBinding()]
    [OutputType([int])]
    Param
    (
        # The popup message
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Message,

        # The number of seconds to wait before closing the popup.  Default is 0, which leaves the popup open until a button is clicked.
        [Parameter(Mandatory=$false,Position=1)]
        [int]$SecondsToWait = 0,

        # The window title
        [Parameter(Mandatory=$true,Position=2)]
        [string]$Title,

        # The buttons to add
        [Parameter(Mandatory=$true,Position=3)]
        [ValidateSet('Ok','Ok-Cancel','Abort-Retry-Ignore','Yes-No-Cancel','Yes-No','Retry-Cancel','Cancel-TryAgain-Continue')]
        [array]$ButtonType,

        # The icon type
        [Parameter(Mandatory=$true,Position=4)]
        [ValidateSet('Stop','Question','Exclamation','Information')]
        $IconType
    )

# Convert button types
switch($ButtonType)
    {
        "Ok" { $Button = 0 }
        "Ok-Cancel" { $Button = 1 }
        "Abort-Retry-Ignore" { $Button = 2 }
        "Yes-No-Cancel" { $Button = 3 }
        "Yes-No" { $Button = 4 }
        "Retry-Cancel" { $Button = 5 }
        "Cancel-TryAgain-Continue" { $Button = 6 }
    }

# Convert Icon types
Switch($IconType)
    {
        "Stop" { $Icon = 16 }
        "Question" { $Icon = 32 }
        "Exclamation" { $Icon = 48 }
        "Information" { $Icon = 64 }
    }

# Create the popup
(New-Object -ComObject Wscript.Shell).popup($Message,$SecondsToWait,$Title,$Button + $Icon)
}

# Close the Task Sequence Progress UI temporarily (if it is running) so the popup is not hidden behind
try
    {
        $TSProgressUI = New-Object -COMObject Microsoft.SMS.TSProgressUI
        $TSProgressUI.CloseProgressDialog()
    }
Catch {}

# Define the parameters.  View the function parameters above for other options.
$Params = @(
    "The software 'Custom IE Plugin' is being installed to your computer. Please close Internet Explorer then click OK to continue." # Popup message
    0                           # Seconds to wait till the popup window is closed
    "Contoso IT: Custom IE Plugin" # title
    "Ok"                        # Button type
    "Exclamation"               # Icon type
    )

# Run the function
New-PopupMessage @Params

I place this script in a network share that everyone can access, and then simply call it during the task sequence using ServiceUI.exe.

How to Do It

Firstly, I need to create a package in SCCM containing the ServiceUI.exe for x86 and x64 architectures.  This package has no program, but simply contains the exe files, which I have renamed per architecture.  You can find the ServiceUI.exe in the following locations in your MDT install:

C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x64, or
C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x86

Capture

Once I have created and distributed the package, I create a new task sequence and add two “Run command line” steps at the beginning where I will prompt the user, one for x86 OS and one for x64.

Capture

The following things are needed in this step:

  • Use the package you created that contains the ServiceUI executables
  • Call ServiceUI using a process that the end user is running.  This enables ServiceUI to detect the session of the end user and interact with it.  If you are using a task sequence deployment with the option “Show task sequence progress” enabled, then you can use the tsprogressui.exe process, however if you are hiding the task sequence progress from the user, then this process will not exist, so you can call Explorer.exe which is certain to be running in the user session.
    • Eg, ServiceUI_x86 -process:Explorer.exe
  • You must specify the full path to powershell.exe
    • Eg, %SYSTEMROOT%\System32\WindowsPowershell\v1.0\powershell.exe
  • Use the “-File” parameter to call the powershell script that displays the popup.
  • Do NOT use the “timeout” option in the step, as this will cause ServiceUI to give an access denied error.
  • On the Option tab of the step, I use a couple of WMI queries so that the step only runs if the correct OS architecture is detected, and the Internet Explorer process is actually running.  I don’t want to prompt the user to close IE if it’s not actually open.
    • Eg, Select * from win32_OperatingSystem where OSArchitecture = ’32-bit’
    • Select * from Win32_Process where Name = ‘iexplore.exe’

Capture

A couple of things to note:

  • You could include the PowerShell script in the package with the ServiceUI executables, then you can call it locally instead of from a network share.  But the advantage of keeping the script and the executables separate is that you don’t need to create a new package each time you want to add a prompt – you simply reuse the ServiceUI package and create a new PowerShell script in the network share by copying and updating and existing script.
  • If you are using the “Show task sequence progress” option, the script includes some code that will hide the progress UI temporarily while the popup is displayed, otherwise it may appear behind the progress UI.
  • Don’t try to pass parameters when calling the PowerShell script, ServiceUI doesn’t seem to like that.
  • The script function includes a “SecondsToWait” parameter – this is set to 0 by default, which means the popup will stay on the screen indefinitely until a button is clicked.  In some cases this may not be desirable, so you can set a value here such that the task sequence will continue if no button has been clicked for some time.

Next, in case the user ignored the prompt or it timed-out, we add another “Run command line” step to kill the process forcefully using taskkill, if it is still running.

  • Eg, cmd /c taskkill /F /IM iexplore.exe

Capture

Make sure to add the same WMI process query to this step:

Capture

Then in the last step, we install the application itself.

Now, when the application is deployed to the end user’s machine, the first thing that happens is they get a popup on the screen warning them to close Internet Explorer.

Capture

Sweet 🙂

You could customise this further by adding some code to the script that will set a task sequence variable based on the exit code of the popup function, which will tell you what button was pressed, for example Yes, No, Ok, Cancel, Abort, Retry etc.  Then you could perform different activities in the task sequence based on the value of the variable.

Redistribute Failed Package Distributions in ConfigMgr with PowerShell

Here’s a little script I wrote based on one written by David O’Brien that allows you to redistribute failed package distributions in Configuration Manager by selecting which packages you want to redistribute.

First the script queries WMI to find packages that are not in the “installed” state, ie the distribution is not successfully completed.  It will then display these in PowerShell’s gridview so you can view details about the package and the distribution.

capture

Simply select which packages you wish to redistribute and click OK.

Using the “Distribution Point Job Queue Manager” available from the Configuration Manager toolkit is a great way to monitor the distributions:

capture

Invoke-PackageRedistribution

Enter your sitecode at the top of the script, and run in on your site server.


$SiteCode = "ABC"
$failures = Get-WmiObject -Namespace root\sms\site_$SiteCode -Query "SELECT * FROM SMS_PackageStatusDistPointsSummarizer WHERE State <> 0" |
    Select ServerNALPath,LastCopied,PackageType,State,PackageID,SummaryDate |
        ForEach-Object {
            $PKG = Get-WmiObject -NameSpace root\sms\site_$SiteCode -Class SMS_Packagebaseclass -Filter "PackageID = '$($_.PackageID)'" | Select Name,PackageSize
            $server = $_.ServerNALPath.Split("\\")[2]
            $size = $PKG.PackageSize / 1KB
            $State =  switch ($_.State)
                {
                    1 {"Install_Pending"}
                    2 {"Install_Retrying"}
                    3 {"Install_Failed"}
                    4 {"Removal_Pending"}
                    5 {"Removal_Retrying"}
                    6 {"Removal_Failed"}
                    7 {"Instal_Start_Pending"}
                    8 {"Content Validation Failed"}
                }
            $Type = switch ($_.PackageType)
                {
                    0 {"Standard Package"}
                    3 {"Driver Package"}
                    4 {"Task Sequence Package"}
                    5 {"Software Update Package"}
                    6 {"Device Setting Package"}
                    7 {"Virtual App Package"}
                    8 {"Application Package"}
                    257 {"OS Image Package"}
                    258 {"Boot Image Package"}
                    259 {"OS Install Package"}
                }
            $LastCopied = [System.Management.ManagementDateTimeconverter]::ToDateTime($_.LastCopied)
            $SummaryDate = [System.Management.ManagementDateTimeconverter]::ToDateTime($_.SummaryDate)
            New-Object psobject -Property @{
                Name = $PKG.Name
                'PackageSize (MB)' = $size
                PackageType = $Type
                PackageID = $_.PackageID
                State = $State
                StateCode = $_.State
                DistributionPoint = $server
                LastCopied = $LastCopied
                SummaryDate = $SummaryDate
                }
        } |
            Select Name,'PackageSize (MB)',PackageType,PackageID,State,StateCode,DistributionPoint,LastCopied,SummaryDate | Sort LastCopied -Descending |
                Out-GridView -Title "Select package/s to redistribute" -OutputMode Multiple |
                 ForEach-Object {
                    Get-WmiObject -Namespace root\sms\site_$SiteCode -Query "SELECT * FROM SMS_DistributionPoint WHERE PackageID='$($_.PackageID)' and ServerNALPath like '%$($_.DistributionPoint)%'" |
                        ForEach-Object {
                            $_.RefreshNow = $true
                            $_.Put()
                        }
                    }

Retrieving Software Inventory for a ConfigMgr Site with PowerShell

In my last post, I demonstrated how we can retrieve software inventory information for a single ConfigMgr client or an array of clients, using PowerShell.  In this post, we will change the scope from the client to the entire site.  Using this script, you can query for all installations of a specific software/s in your ConfigMgr site, returning either the count, or the full list of machines with the software installed.

When I say “software inventory”, I’m actually referring to the “hardware inventory” process (strange but true) in Configuration Manager that collects data from WMI classes, including the installed software, and not to be confused with the “software inventory” process in ConfigMgr which is used to inventory file types.

As previously, you need ‘db_datareader’ permission to your ConfigMgr database, with your logged on account, and you need to add the sql server and database name in the script.

Examples

Find a count of machines that have “Microsoft Silverlight” installed


Get-CMInstalledSoftware -SoftwareName "Microsoft Silverlight%" -Count

capture1

Get the list of machines that have “Microsoft Silverlight 5” installed and output to Gridview


Get-CMInstalledSoftware -SoftwareName "Microsoft Silverlight 5%" | Out-GridView

Capture2

Get the list of machines that have “Microsoft Silverlight 5” installed and output to CSV

 Get-CMInstalledSoftware -SoftwareName "Microsoft Silverlight 5" -CSV 

Capture3

Get-CMInstalledSoftware


[CmdletBinding(SupportsShouldProcess=$True)]
    param
        (
        [Parameter(Mandatory=$False,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
            [string]$SoftwareName = "%",
        [Parameter(Mandatory=$False)]
            [switch]$Count,
        [Parameter(Mandatory=$False)]
            [switch]$CSV,
        [Parameter(Mandatory=$False)]
            [string]$SQLServer = “mysqlserver\INST_SCCM”, # eg, <sqlserver>, or <sqlserver>\<instance>
        [Parameter(Mandatory=$False)]
            [string]$Database = “CM_ABC”
        )
 
# Open a connection
$connectionString = “Server=$SQLServer;Database=$database;Integrated Security=SSPI;”
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
 
# Set queries
if ($count)
    {
        $query = "
        SELECT Count(sof.NormalizedName) AS 'Count',
        sof.NormalizedName, sof.NormalizedVersion, sof.NormalizedPublisher, sof.FamilyName, sof.CategoryName
        FROM v_GS_INSTALLED_SOFTWARE_CATEGORIZED sof
        where sof.NormalizedName like '$SoftwareName'
        GROUP BY sof.NormalizedName, sof.NormalizedVersion, sof.NormalizedPublisher, sof.FamilyName, sof.CategoryName
        ORDER BY 'Count' DESC, sof.NormalizedName, sof.NormalizedVersion
        "
    }
else
    {
        $query = "select Name0 as 'Computer Name',
        User_Name0 as 'Last Logged-On User',
        NormalizedName as 'Software Name',
        NormalizedPublisher as Publisher,
        NormalizedVersion as Version,
        FamilyName as 'Software Family',
        CategoryName as 'Software Category',
        InstallDate0 as 'Install Date',
        RegisteredUser0 as 'Registered User',
        InstalledLocation0 as 'Install Location',
        InstallSource0 as 'Source Location',
        UninstallString0 as 'Uninstall String',
        TimeStamp as 'Inventory Time'
        from v_GS_INSTALLED_SOFTWARE_CATEGORIZED sof
        inner join v_R_System sys on sof.ResourceID = sys.ResourceID
        where sof.NormalizedName like '$SoftwareName' order by Name0
        "
    }
    
$command = $connection.CreateCommand()
$command.CommandText = $query
$result = $command.ExecuteReader()

$table = new-object “System.Data.DataTable”
$table.Load($result)

# Output results
if ($CSV)
    {
        $Path = "$env:TEMP\SoftwareQuery-$(Get-date -format hh-mm).csv"
        $table | Export-Csv -Path $Path -Force -NoTypeInformation
        Invoke-Item $Path
    }
Else {$table}

# Close the connection
$connection.Close()