OSD Driver Management the easy way…

Driver management in OSD has always been a hot topic for deployment folks. If your using ConfigMgr for deployment you have the option of Applying Driver Packages or Auto-Apply drivers. There have been many posts over the years to discuss the merits of both so I won’t go over old ground…but if you want a refresher please refer to the many posts from @mniehaus and @jarwidmark

Back when Windows 8 was released and everyone was preoccupied with the Start Screen a new command was added to DISM to add drivers to an offline image. This command combined with a standard ConfigMgr package containing driver inf files gives us a third driver management option in ConfigMgr. I had never thought of using the command during OSD until I read @agerlund mention this in a tweet a few months back and I thought I would give it a go.

This is how it works:

  1. Download drivers for the chosen model from the vendor website.
  2. Extract drivers to a source folder and remove any unnecessary drivers (this is important…don’t include everything…only what is needed. Weed out drivers for other OS’s and different architectures.)
  3. Create a standard ConfigMgr Package (Not a driver package) pointing to the source folder containing the drivers. No program is needed.
  4. Edit your Task Sequence and add a “Run command Line” step in the “Post Install” phase of the Task Sequence…immediately after the default “Auto Apply drivers” step.
  5. Rename the Step to something more appropriate.
  6. Add the following command line:

    DISM.exe /Image:%OSDisk%\ /Add-Driver /Driver:.\ /Recurse

  7. Tick “Package” and browse to the package you created in step 3:
  8. Add a WMI query condition for the model you are deploying.
  9. Save changes.
  10. Deploy the Task Sequence

This is what the logs look like:
Driver Package method:
Drivers_03DISM Add-Driver Method:

As you can see from above, DISM does a pretty good job logging out to SMSTS however if you want to run a post deployment verification check you can run the following PowerShell command to ensure all the drivers have been successfully added to the driver store:

Get-WindowsDriver -Online -All | ? {$_.Inbox -eq $false} | Select-object -Property OriginalFileName

Why I like this option:

  1. It’s simple. No need to import drivers into the ConfigMgr driver store which has always been slow and clunky. You can pretty much do away with the driver store completely unless you want to add drivers to boot images through the console.
  2. It’s quick. In my limited testing I found this to be quicker than applying the same drivers with a Driver Package (via unattend.xml). Around 40% faster in my test on a Surface Pro 3.

The downside is that as this is a legacy package it will not benefit from single instance storage…so takes up more space on DP’s if the content is duplicated.

Also note DISM with the recurse parameter will import every driver conatined in the package into the driver store. So keep the drivers clean.

. Surj

Dynamically set Computer name of Physical and Virtual Machines via CustomSettings.ini

I needed a clean way to set the hostname of physical and virtual machines in MDT via the customsettings.ini file. Sure I could use %SerialNumber% variable but if I deploy onto a virtual machine I would hit an error because the Serial Number of a VM tends to be too long for a hostname.

So rather than write a script and have additional steps in the Task Sequence I got the desired outcome with a few extra lines in the customsettings.ini:

Here’s how:



[VM-TRUEset ]




The first line “Subsection=VM-%ISVM%” is added to the Default section of the customsettings.ini file. This line essentially tells ZTIGather to process another section in the ini file. The section we want to process is either VM-TRUE or VM-False. Which section is processed will depend on the in-built %IsVM% Task Sequence Variable.

The next part of the solution is to set the OSDComputerName variable.

For physical machines we simply user the %SerialNumber% variable. In the example I prefix the hostname with PH.

For Virtual machines we use the MAC address instead of the serial number. This ensures we stay within the 15 character hostname limit. We cannot use the %MACADDRESS001% variable directly, we first need to remove the “:” separators. To do this we can get ztigather to process code directly from the customsettings.ini. Anything inside “#” is treated as code, in our example we are simply using the Replace function to remove the “:”.

This works with an SCCM OSD Task Sequence too (with MDT integration).

DISM Cmdlets fail to run in Win PE with MDT 2013 Update 1 – WORKAROUND

Whilst working with Windows 10 and MDT 2013 Update 1 my colleague Graham Hardie and I ran into an issue which was preventing us from running DISM Cmdlets in Win PE.

The issue became apparent whilst trying to implement Michael Niehaus’s Windows 10 Inbox App Removal script and specifically trying to run the script offline in Win PE.

The MDT boot Image was successfully generated with the MDAC, .NET, PowerShell and DISM Cmdlets features but would fail to run the “Get-AppxProvisionedPackage” Cmdlet with the following error:

Get-AppxProvisionedPackage : The ‘Get-AppxProvisionedPackage’ command was found in the module ‘Dism’, but the module could not be loaded. For more information, run ‘Import-Module Dism’.
At line:1 char:1
+ Get-AppxProvisionedPackage -Path e:\
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (Get-AppxProvisionedPackage:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CouldNotAutoloadMatchingModule

We found that the Microsoft.Dism.Powershell.dll file was becoming corrupt at boot up…not had the chance to investigate what exactly is causing the corruption yet.

However, to workaround the issue you need to do the following:

  1. Delete the file from:
  2. Copy the file back down to the X: drive from the Deployment share:

You can obviously automate this as part of your Task Sequence. I simply added two ‘Run Command Line’ steps to Delete the file and copy the new file.  The steps need to be added before you run any script which has a dependency on DISM Cmdlets…in our case before Michael’s Inbox App Removal script:

The first step:

Command Line:

cmd.exe /c Del “%SystemRoot%\System32\WindowsPowerShell\v1.0\Modules\Dism\Microsoft.Dism.Powershell.dll” /q /s

The Second step:

Command Line:

robocopy Z:\Servicing\x64 X:\Windows\System32\WindowsPowerShell\v1.0\Modules\Dism Microsoft.Dism.Powershell.dll /is

Note: Add Error Code 1 to Success Codes for this step…robocopy can report a success with a non zero return.

A bug has also been opened on Connect.

Let us know if this works for you or better still if you identify the root cause of the issue.

Moving MDT Domain Join to the end of the Task Sequence

This sounds easy…but it’s not as straight forward as you may think so I put together a quick blog post:

Default MDT Behaviour:
By default, MDT will join a client to the domain via Windows Setup.  This is driven by the Unattend.xml which MDT populates with the required settings. There is also another step (Recover from Domain) which can be used later in the deployment process (during State Restore) to re-try a Domain Join should the previous attempt have failed during OS setup.

Why does this cause an issue?
Joining the domain before the state restore phase can cause issues later in the deployment because the machine will start to process Group Policy during the build process itself. In most cases this is not an issue if:

  1. You pre-stage all your clients in a staging OU which has no GPO’s linked to it.
  2. Or you are confident there is nothing in your Group Policy which could break the imaging process.

In my case policy would break deployments (renaming/disabling admin accounts, legal notices etc..) and pre-staging was not an option so I needed to shift the Domain Join to later in the Task Sequence.


To move the Domain Join process to later in the deployment you need to update the Unattend.xml and the ZTIDomainJoin script. Here’s how you do it:

  1. Update Unattend.XML
    The first thing we need to do is to stop MDT from performing the Domain Join during OS Setup. One way to achieve this is to remove the MDT Domain Join Task Sequence Variables in CustomSettings.ini and instead set these on the Task Sequence in the “State Restore” phase.  But I generally prefer to set my variables in CustomSettings.ini so to make this work I have to remove the Domain Join Node from the unattend.xml.
    This will prevent MDT from adding the Domain Join settings to the unattend.xml and OS Setup will therefore not join the machine to the domain.
  2. Update ZTIDomainJoin.wsf
    An update to the ZTIDomainJoin.wsf script is required to prevent the script from rebooting the machine after joining the domain. I usually avoid editing built-in MDT scripts but if I have to, I prefer to make a custom subdirectory in the MDT scripts folder and edit a copy of the script in the subdirectory. This will ensure that any subsequent updates to MDT will not overwrite the script and break your Task Sequence (remember to update the relative path to ZTIUtility at the top of the script). The following TWO lines of the script need to be commented out to suppress the reboot:

    oEnvironment.Item(“LTISuspend”) = “” ‘oEnvironment.Item(“SMSTSRetryRequested”) = “true” ‘oEnvironment.Item(“SMSTSRebootRequested”) = “true”

    iRetVal = SUCCESS

  3. Add a Domain Join Step into the Task Sequence
    Now add a new command line step into the Task Sequence to run the updated script:
  4. Set Domain Join Variables in CustomSettings.ini
    Next, you need to set the following six variables in customsettings.ini for ZTIDomainJoin.wsf to work:

    JoinDomain (Domain to join)
    DomainAdmin (Account to be used to perform Domain Join)
    DomainAdminDomain (Domain of Domain Join account)
    DomainAdminPassword (Password for Domain Join account)
    MachineObjectOU (OU in which machine object should be created)
    DomainErrorRecovery (What to do on failure e.g FAIL)

  5. Update the Unattend.xml template (Optional)
    This step is not essential but if you want to ensure that all new Task Sequences on the given Deployment Share always generate an unattend.xml without the Domain Join node you can update the template.To achieve this copy the unattend.xml for your OS (in my case Unattend_x64.xml) from:
    %ProgramFiles%\Microsoft Deployment Toolkit\Templates
    Into the Templates directory in the root of you Deployment share and then remove the Domain Join node from the file.

And that’s it..you are good to go 🙂

Surjit Khera

Automating the Import of Office 2013 Language Packs into MDT with PowerShell

I recently needed to add Office Language Pack support into an MDT deployment and found this to be a looooong time consuming task…especially if you need to support a number of languages. So I ended up writing a PowerShell script to automate the steps.

Some background first. The following blog by Johan Arwidmark and Patrick Zovistoski is a great post and should be used in conjunction with my script:

deploymentresearch.com – Installing Office 2013 Language Packs in MDT

The high level steps to Install Office Language Packs with MDT are as follows:

  1. Obtain the Office 2013 Language Pack DVD media from the Microsoft VL website.
  2. Create the correct Folder structure for each language you need to import (containing all component language folders, setup files etc…)
  3. Update the config.XML for each language to run silent and suppress reboot.
  4. Import each language into MDT as an application.
  5. Add the Applications into the MDT Task Sequence.

My script automates the first 4 steps…so all you need to do manually is add the languages to the Task Sequence 🙂

So onto the script.  You need to do the following:

  1. Mount the two Office 2013 Language Pack ISO files on your MDT server.
  2. Create a staging folder on your server.  This location will be used to create the source folders for the Language packs.
  3. Copy the script and CSV file (Download from here) into the staging folder on your MDT Server.
  4. Update the varibles at the top of the script with details of your deployment share, path of the Language Pack DVD’s etc…SetVars
  5. Update the CSV file to reflect the languages you want to import.csv
  6. Run the script.

The script will create the source folders for each language you specify and update the config XML’s for each language:Staging afterFinally the script will import each language pack into your MDT deployment share:DSAfter

And that’s it… A good hour of work down to about 5 mins.

Download the script from here

Enjoy 🙂

Fixing Office 2013 1920 Error during OSD on Windows 10 Technical Preview

I have been playing with the Windows 10 Technical Preview this week and I ran into an obscure error when trying to install Office 2013 as part of my Deployment Task Sequence in SCCM. The Office 2013 setup.exe returned a 1920 error when deploying the Task Sequence to Virtual Machines (Physical were fine). The Office Setup log reports the following error:

Error 1920. Service ‘Windows Font Cache Service’ (FontCache) failed to start. Verify that you have sufficient privileges to start system services.

Taking a closer look I could see the service was in a stopped state and I could not start it manually…that was until I restarted 🙂  Even with Office being the first Application installation in the Task Sequence it would fail. So if you are installing Office 2013 on the Win 10 TP you need to make sure you add an additional restart immediately before the Office installation step.


Snippet #2 – Checking for free disk space with PowerShell

Inspired by Dan’s last post, I thought I would share some more PowerShell goodness. A very quick one to start with: How to check for free disk space with PowerShell.

As you would expect with PowerShell its amazingly simple. Here is the function:

Function GetFreeDiskSpace([string]$DriveLetter, [string]$Measurement)


$Drives=Get-WmiObject -Class Win32_LogicalDisk

foreach($Drive in $Drives)


If ($Drive.Name -eq $DriveLetter)






The function simply queries the WMI LogicalDisk class and then converts the fresspace to GB, MB or KB. For example, if you want to find out how much free disk space is available for the c:\ in Gigabytes, you would call the function as follows:

GetFreeDiskSpace “C:” “GB”

write-host “Freespace = $Freespace

That’s all for now 🙂