Blog Post

Applying Unattend.xml files using Desired State Configuration

GigaTrust™ is creating a fully automated solution to set up and install GigaCloud™ in whatever configuration our customers want: on-premises in Hyper-V and VMWare, in their own cloud subscriptions using Microsoft Azure, and even fully managed by GigaTrust as Software as a Service (SaaS).

Because GigaCloud is based on Microsoft’s Rights Management Services, we need a number of servers installed in a scalable, redundant fashion, running services including Active Directory Domain Services, Rights Management Services, Windows Software Update Service, Active Directory Federation Service, Windows Application Proxy, SQL Server, and more. The first thing we do, no matter which environment is chosen, is to create the base VM’s that will run GigaCloud.

Once the virtual machines are created, we’re using PowerShell Desired State Configuration to get them up and running with the services we need. We’re starting with a base installation of Windows Server, so the first step is to provide a Unattend.xml file to get Windows going.

Unattended Windows installation and DSC

The Unattend.xml file must be placed in the root directory of the system drive (usually C :\), before we first start up the server that’s going to use it. That means that we need to do two things:

  1. Check if an Unattend.xml file already exists; if it does, don’t overwrite it. This allows us to ensure idempotency for our DSC resources.
  2. If it’s not present, write the new Unattend.xml file to the root directory.

We’re using a DSC Script resource to do this work for us. Like all DSC resources, it has three parts:

  1. Get, to retrieve information about the resource;
  2. Test, to check if the resource is already in its desired state, which returns a Boolean value; and
  3. Set, to put the resource into the correct state when it needs to be changed.

For Get, we return an empty hashtable.

For Test, we have to:

  1. Mount the .vhdx file as a drive on the machine we’re executing the DSC script from. Fortunately, since Windows Server 2012, this has been built-in to Windows.
  2. Check if the Unattend.xml file exists.
  3. Dismount the .vhdx file.
  4. Return true or false.

Because the script resource allows us to write normal PowerShell code, we can use standard PowerShell commands like Mount-VHD and .NET classes like System.IO.File to accomplish our task. We store it in a ScriptBlock variable for easier editing:

<code block – monospaced font with this source code coloring>

[ScriptBlock]$TestScript =

{

$UnattendFileExists = $false

$exceptionCaught = $false

$DriveLetter = [string]::Empty


try

{

Mount-VHD -Path '$VHDXFilePath' -ReadOnly -Verbose -ErrorAction SilentlyContinue


# Find out which drive letter the .vhdx file was mounted with

$Disks = Get-CimInstance -ClassName Win32_DiskDrive | where Caption -eq "Microsoft Virtual Disk"

foreach ($Disk in $Disks)

{
$Volumes = Get-CimAssociatedInstance -CimInstance $Disk -ResultClassName Win32_DiskPartition

foreach ($Volume in $Volumes)

{

$LogicalDisk = Get-CimAssociatedInstance -CimInstance $Volume -ResultClassName Win32_LogicalDisk | where VolumeName -ne 'System Reserved'

foreach ($prop in $LogicalDisk.CimInstanceProperties)

{

if ($prop.Name -eq 'DeviceID')

{

$DriveLetter = $prop.Value

Write-Verbose -Message "Mounted as drive $DriveLetter"

break

}

}

}

}

 

# Check if the Unattend.xml file exists

if ($DriveLetter -ne [string]::Empty)

{

$UnattendFilePath = $DriveLetter + "\Unattend.xml"

$UnattendFileExists = [System.IO.File]::Exists($UnattendFilePath)




Write-Verbose -Message ([string]::Format("UnattendFilePath: {0}; UnattendFileExists: {1}", $UnattendFilePath, $UnattendFileExists))

}

else

{

$exceptionCaught = $true

}

}

catch

{
$exceptionCaught = $true

}

finally

{

Dismount-VHD -Path '$VHDXFilePath' -ErrorAction SilentlyContinue -Verbose

}

if ($exceptionCaught)

{

Write-Verbose -Message ('An exception was caught during Test-TargetResource. No changes will be made.')

return $true

}
if ($UnattendFileExists)

{

Write-Verbose -Message 'The Unattend.xml file already exists and no action is required.'

return $true

}

else

{

Write-Verbose -Message 'The Unattend.xml file was not found and needs to be created.'

return $false

}

}

</code block>

This script is straightforward… mount the .vhdx file, identify which drive letter it’s mounted as, and check if the file exists. If an exception is thrown, or the file already exists, return $true (already in-state, don’t change anything), otherwise we return $false (not in-state, need to run the set script).

For Set, though, we have an extra complication. The script resource, along with the other resources to be configured through DSC, are compiled into .mof files and sent to each server’s Local Configuration Module. That means that the actual text of the script blocks must be fully set at compile-time. There won’t be any opportunity for run-time substitution of strings. In other words, we need to put the entire contents of the Unattend.xml file into the Set script block at compile-time. Because it’s a multi-line string, we’ll use PowerShell’s Here-Strings to make that happen.

First, we create a here-string called $UnattendContents that uses a double-quoted string to get variable substitution for the machine name, administrator password, and organization name.

<code block>

$UnattendContents = @"

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">

<settings pass="windowsPE">

<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<UserData>

<AcceptEula>true</AcceptEula>

<FullName>$AdministratorAccount</FullName>

<Organization>$OrganizationName</Organization>

</UserData>

</component>

</settings>

<settings pass="offlineServicing">

<component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<EnableLUA>true</EnableLUA>

</component>

</settings>

<settings pass="specialize">

<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<ComputerName>$VMResourceName</ComputerName>

<RegisteredOrganization>$OrganizationName</RegisteredOrganization>

<RegisteredOwner></RegisteredOwner>

</component>

<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<DomainProfile_EnableFirewall>false</DomainProfile_EnableFirewall>

<PrivateProfile_EnableFirewall>false</PrivateProfile_EnableFirewall>
<PublicProfile_EnableFirewall>false</PublicProfile_EnableFirewall>

</component>

<component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<fDenyTSConnections>false</fDenyTSConnections>

</component>

<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<UserAuthentication>0</UserAuthentication>

</component>

<component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"

xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<CEIPEnabled>1</CEIPEnabled>

</component>

<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<InputLocale>0409:00000409</InputLocale>

<SystemLocale>en-US</SystemLocale>

<UILanguage>en-US</UILanguage>
<UILanguageFallback>en-US</UILanguageFallback>

<UserLocale>en-US</UserLocale>

</component>

</settings>

<settings pass="oobeSystem">

<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<UserAccounts>
<AdministratorPassword>

<Value>$AdministratorPassword</Value>

<PlainText>true</PlainText>

</AdministratorPassword>

</UserAccounts>

<RegisteredOrganization>$OrganizationName</RegisteredOrganization>

<RegisteredOwner></RegisteredOwner>

<OOBE>
<HideEULAPage>true</HideEULAPage>

<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>

<HideOnlineAccountScreens>true</HideOnlineAccountScreens>

<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>

<NetworkLocation>Work</NetworkLocation>

<ProtectYourPC>1</ProtectYourPC>

<SkipUserOOBE>true</SkipUserOOBE>
<SkipMachineOOBE>true</SkipMachineOOBE>

</OOBE>

<TimeZone>UTC</TimeZone>

<VisualEffects>

<SystemDefaultBackgroundColor>4</SystemDefaultBackgroundColor>

</VisualEffects>

</component>

</settings>

</unattend>

"@

</code block>

Once we have the Unattend.xml contents, we need to inject it into the Set script block. Here’s the Set script… it looks very much like the Test script. We mount the .vhdx file, identify the drive letter, write the Unattend.xml file, and dismount the drive.

<code block>

[ScriptBlock]$SetScript =

{

try

{

# Mount the .vhdx file, and identify the drive letter it's using

Mount-VHD -Path '$VHDXFilePath' -Verbose

$Disks = Get-CimInstance -ClassName Win32_DiskDrive | where Caption -eq "Microsoft Virtual Disk"

foreach ($Disk in $Disks)

{

$Volumes = Get-CimAssociatedInstance -CimInstance $Disk -ResultClassName Win32_DiskPartition

foreach ($Volume in $Volumes)

{

$LogicalDisk = Get-CimAssociatedInstance -CimInstance $Volume -ResultClassName Win32_LogicalDisk | where VolumeName -ne 'System Reserved'

foreach ($prop in $LogicalDisk.CimInstanceProperties)

{

if ($prop.Name -eq 'DeviceID')

{

$DriveLetter = $prop.Value

Write-Verbose -Message "Mounted as drive $DriveLetter"

break

}

}

}

}

# Tell PowerShell that it has a new drive to know about... without this, it can't see the drive

New-PSDrive -Name $DriveLetter.Substring(0,1) -PSProvider FileSystem -Root "$($DriveLetter)\"

$UnattendFilePath = $DriveLetter + "\Unattend.xml"



# Create a local variable to hold the contents of the Unattend.xml file... we'll use String.Replace to insert the contents below

$LocalUnattendContents = '$UnattendContents'


# Write the Unattend.xml file to the mounted .vhdx file

$LocalUnattendContents | Out-File $UnattendFilePath -Force -Encoding Default


Write-Verbose -Message ('Unattend.xml file written to ' + $UnattendFilePath)

}

catch

{

Write-Verbose -Message ([string]::Format('Exception: {0}; {1}', $_.Exception.Message, $_.Exception.StackTrace))

}

finally

{

Dismount-VHD -Path '$VHDXFilePath' -ErrorAction SilentlyContinue -Verbose

}

}

</code block>

 

Finally, we use String.Replace() when we define the DSC Script resource to inject the contents:

 

<code block>

Script $VMUnattendScript {

    GetScript = $GetScript.ToString().Replace('$VHDXFilePath', $VHDXFilePath)

    SetScript = $SetScript.ToString().Replace('$VHDXFilePath', $VHDXFilePath).Replace('$UnattendContents', $UnattendContents)

    TestScript = $TestScript.ToString().Replace('$VHDXFilePath', $VHDXFilePath)

    DependsOn = '[File]' + $BaseFileResourceName

}

</code block>

With this set of code, we can now drop the Unattend.xml file onto the .vhdx file, and when we fire up the VM for the first time, it bootstraps itself and is ready to take on the rest of the DSC resources we’ve scripted for it.

Using PowerShell Desired State Configuration has been an interesting, challenging, and rewarding journey for us. Automation helps us create high-quality, repeatable processes, and we’re using DSC to turn what was a complex installation process into an easy-to-run, high-quality, repeatable process, one step at a time. In my next blog, we’ll dive into getting our domain controllers installed using DSC.

By Scott Arbeit, Chief Cloud Architect