Script: Image Factory for Hyper-V
Update 7/20/2015: This script is now available on GitHub. Please go to https://github.com/BenjaminArmstrong/Hyper-V-PowerShell/tree/master/Image-Factory to get the latest version, and to contribute any changes and bug fixes.
Summer has come to Seattle - which means that it is time for me to get going on some overdue Summer projects.
The first one - that I have been working on for the last two weeks attempts to solve a simple problem:
"Why do I never have an updated Windows image handy?"
Anyone who has spent time with virtual machines understands this problem. It is far more efficient to install and update Windows once, and then copy the virtual hard disk each time you create a virtual machine, than it is to install Windows each time. The problem is that if you do this - you find that your original base image soon gets out of date, and you can either choose to slow down deployment times by applying updates - or to be insecure.
Unfortunately; many of us just end up running without the latest patches installed.
So I sat down to solve this problem. My goal was to come up with a solution where I could have a set of Windows virtual hard disks that were always up-to-date; with zero ongoing maintenance from me. The result is something that I call the Image factory.
This is essentially a PowerShell script that creates and maintains a set of Windows virtual hard disks for me that are always up to date. To make this work - I am using some tools / information provided by others:
- To create virtual hard disks straight from Windows installation media - I am using Convert-WindowsImage: https://gallery.technet.microsoft.com/scriptcenter/Convert-WindowsImageps1-0fe23a8f
- To automate the process of installing updates - I am using PSWindowsUpdate: https://gallery.technet.microsoft.com/scriptcenter/2d191bcd-3308-4edd-9de2-88dff796b0bc
- Inspiration (and sometimes code) was gained from the work that the CloudBase team have done on automating Windows in OpenStack: https://github.com/cloudbase/windows-openstack-imaging-tools
- To schedule this script to run on a daily schedule, I followed the steps documented here: https://blogs.technet.com/b/heyscriptingguy/archive/2012/08/11/weekend-scripter-use-the-windows-task-scheduler-to-run-a-windows-powershell-script.aspx
But what does this script actually do? Well, it uses the following logic:
The great thing about this approach is that it simultaneously ensures that my images are always up-to-date - without wasting time reinstalling Windows unnecessarily, and without downloading duplicated updates. The result is that, after the initial setup, it can update all of my images in under an hour. This in turn means that it is something that I can schedule to run each night.
If you do not want to read over all the code - and just want to run it; some key details to know:
- You *have* to fill out the variables at the top of the script appropriately for your environment.
- I have tested this script for Windows Server 2012, 2012 R2, Windows 8, Windows 8.1; Generation 1 and Generation 2; 32-bit and 64-bit; core, full and professional SKUs. I suspect that the script will need modification to work with Windows Server 2008 R2 / Windows 7 - but it should be possible.
- Note - If you do make this script work for those OSes, shoot me a note with the changes needed; and I will update this post.
- In the script I am passing in .WIM files for the install images - you can also use .ISO files with no modification of the script necessary.
- The script is a bit sensitive about file locations (I know it will break if the bases and share directory are not created ahead of time). For reference, here is the exact file and directory structure that I have when starting a clean run:
Directory of C:\ImageFactory
<DIR> ISOs
<DIR> Resources
<DIR> Share
<DIR> bases
Convert-WindowsImage.ps1
Factory.ps1
Directory of C:\ImageFactory\bases
0 File(s)
Directory of C:\ImageFactory\ISOs
en_windows_8_1_x64_dvd_2707217.wim
en_windows_8_1_x86_dvd_2707392.wim
en_windows_8_x64_dvd_915440.wim
en_windows_8_x86_dvd_915417.wim
en_windows_server_2012_r2_x64_dvd_2707946.wim
en_windows_server_2012_x64_dvd_915478.wim
Directory of C:\ImageFactory\Resources
<DIR> Bits
Directory of C:\ImageFactory\Resources\Bits
<DIR> PSWindowsUpdate
Directory of C:\ImageFactory\Resources\Bits\PSWindowsUpdate
Add-WUOfflineSync.ps1
Add-WUServiceManager.ps1
Get-WUHistory.ps1
Get-WUInstall.ps1
Get-WUInstallerStatus.ps1
Get-WUList.ps1
Get-WURebootStatus.ps1
Get-WUServiceManager.ps1
Get-WUUninstall.ps1
Hide-WUUpdate.ps1
Invoke-WUInstall.ps1
PSWindowsUpdate.Format.ps1xml
PSWindowsUpdate.psd1
PSWindowsUpdate.psm1
Remove-WUOfflineSync.ps1
Remove-WUServiceManager.ps1
Update-WUModule.ps1
Directory of C:\ImageFactory\Share
0 File(s)
Before jumping into the code - let me give you a short code analysis:
- You need to update all the variables at the top with the right paths, passwords, product keys and location of WMI / ISO files.
- UnattendSource - is the XML template that I modify for all unattended files.
- CSVLogger - is a function that creates / updates a CSV file in the Share directory - so you can easily tell when the factory last checked for updates, and when it last updated images.
- Logger - is the common function I use for outputting logging messages. If you want to change the way logging is done - this is the only place in the script that I output any information.
- cleanupFile - this is a simple little function that checks if a file exists, and deletes it if it does. I do this frequently, so it made sense to put it in a common routine
- GetUnattendChunk - this is just some minor code clean up, and wraps a bit of clunky XML parsing in a nicer function. I only call this from makeUnattendFile.
- makeUnattendFile - is a function that performs any modifications needed to the unnattend template to create a real unattend file.
- createRunAndWaitVM - A common pattern that I have is to create a virtual machine, start it, and then wait for it to shut itself down. That is what this function does.
- MountVHDandRunBlock - this is a handy helper function that does what it says. It takes a VHDX path and a script block, then it mounts the VHDX and runs the script block.
- updateCheckScriptBlock, sysprepScriptBlock and postSysprepScriptBlock- a set of script blocks that I turn into a PS1 files which are then injected into a virtual hard disk at the right point in time.
- RunTheFactory - this is the primary function that co-ordinates all the work
With all that said - here is the code (note - it is also attached as a .zip at the bottom of this post):
$workingDir = "C:\ImageFactory"
$logFile = "$($workingDir)\Share\Details.csv"
$factoryVMName = "Factory VM"
$virtualSwitchName = "Virtual Switch"
$ResourceDirectory = "$($workingDir)\Resources\Bits"
$Organization = "The Power Elite"
$Owner = "Ben Armstrong"
$Timezone = "Pacific Standard Time"
$adminPassword = "P@ssw0rd"
$userPassword = "P@ssw0rd"
# Keys
$Windows81Key = "..."
$Windows2012R2Key = "..."
$Windows8Key = "..."
$Windows2012Key = "..."
# ISOs / WIMs
$2012Image = "$($workingDir)\ISOs\en_windows_server_2012_x64_dvd_915478.wim"
$2012R2Image = "$($workingDir)\ISOs\en_windows_server_2012_r2_x64_dvd_2707946.wim"
$8x86Image = "$($workingDir)\ISOs\en_windows_8_x86_dvd_915417.wim"
$8x64Image = "$($workingDir)\ISOs\en_windows_8_x64_dvd_915440.wim"
$81x86Image = "$($workingDir)\ISOs\en_windows_8_1_x86_dvd_2707392.wim"
$81x64Image = "$($workingDir)\ISOs\en_windows_8_1_x64_dvd_2707217.wim"
$startTime = get-date
### Load Convert-WindowsImage
. "$($workingDir)\Convert-WindowsImage.ps1"
### Sysprep unattend XML
$unattendSource = [xml]@"
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<servicing></servicing>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<ComputerName>*</ComputerName>
<ProductKey>Key</ProductKey>
<RegisteredOrganization>Organization</RegisteredOrganization>
<RegisteredOwner>Owner</RegisteredOwner>
<TimeZone>TZ</TimeZone>
</component>
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://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="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<UserAuthentication>0</UserAuthentication>
</component>
<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<FirewallGroups>
<FirewallGroup wcm:action="add" wcm:keyValue="RemoteDesktop">
<Active>true</Active>
<Profile>all</Profile>
<Group>@FirewallAPI.dll,-28752</Group>
</FirewallGroup>
</FirewallGroups>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>1</ProtectYourPC>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>password</Value>
<PlainText>True</PlainText>
</AdministratorPassword>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Password>
<Value>password</Value>
<PlainText>True</PlainText>
</Password>
<DisplayName>Demo</DisplayName>
<Group>Administrators</Group>
<Name>demo</Name>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Password>
<Value>password</Value>
</Password>
<Enabled>true</Enabled>
<LogonCount>1000</LogonCount>
<Username>Administrator</Username>
</AutoLogon>
<LogonCommands>
<AsynchronousCommand wcm:action="add">
<CommandLine>%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -File %SystemDrive%\Bits\Logon.ps1</CommandLine>
<Order>1</Order>
</AsynchronousCommand>
</LogonCommands>
</component>
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<InputLocale>en-us</InputLocale>
<SystemLocale>en-us</SystemLocale>
<UILanguage>en-us</UILanguage>
<UILanguageFallback>en-us</UILanguageFallback>
<UserLocale>en-us</UserLocale>
</component>
</settings>
</unattend>
"@
function CSVLogger ([string]$vhd, [switch]$sysprepped) {
$createLogFile = $false
$entryExists = $false
$logCsv = @()
$newEntry = $null
# Check if the log file exists
if (!(test-path $logFile))
{$createLogFile = $true}
else
{$logCsv = import-csv $logFile
if (($logCsv.Image -eq $null) -or `
($logCsv.Created -eq $null) -or `
($logCsv.Sysprepped -eq $null) -or `
($logCsv.Checked -eq $null))
{# Something is wrong with the log file
cleanupFile $logFile
$createLogFile = $true}
}
if ($createLogFile) {$logCsv = @()} else {$logCsv = import-csv $logFile}
# If we find an entry for the VHD, update it
foreach ($entry in $logCsv)
{ if ($entry.Image -eq $vhd)
{$entryExists = $true
$entry.Checked = ((get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString())
if ($sysprepped) {$entry.Sysprepped = ((get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString())}
}
}
# if no entry is found, create a new one
If (!$entryExists)
{$newEntry = New-Object PSObject -Property @{Image=$vhd; `
Created=((get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()); `
Sysprepped=((get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()); `
Checked=((get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString())}}
# Write out the CSV file
$logCsv | Export-CSV $logFile -notype
if (!($newEntry -eq $null)) {$newEntry | Export-CSV $logFile -notype -Append}
}
function Logger ([string]$systemName, [string]$message)
{# Function for displaying formatted log messages. Also displays time in minutes since the script was started
write-host (Get-Date).ToShortTimeString() -ForegroundColor Cyan -NoNewline
write-host " - [" -ForegroundColor White -NoNewline
write-host $systemName -ForegroundColor Yellow -NoNewline
write-Host "]::$($message)" -ForegroundColor White}
# Helper function for no error file cleanup
Function cleanupFile ([string]$file) {if (test-path $file) {Remove-Item $file}}
function GetUnattendChunk ([string]$pass, [string]$component, [xml]$unattend)
{# Helper function that returns one component chunk from the Unattend XML data structure
return $Unattend.unattend.settings | ? pass -eq $pass `
| select -ExpandProperty component `
| ? name -eq $component}
function makeUnattendFile ([string]$key, [string]$logonCount, [string]$filePath, [bool]$desktop = $false, [bool]$is32bit = $false)
{# Composes unattend file and writes it to the specified filepath
# Reload template - clone is necessary as PowerShell thinks this is a "complex" object
$unattend = $unattendSource.Clone()
# Customize unattend XML
GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.ProductKey = $key}
GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.RegisteredOrganization = $Organization}
GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.RegisteredOwner = $Owner}
GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.TimeZone = $Timezone}
GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.UserAccounts.AdministratorPassword.Value = $adminPassword}
GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.AutoLogon.Password.Value = $adminPassword}
GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.AutoLogon.LogonCount = $logonCount}
if ($desktop)
{
GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.UserAccounts.LocalAccounts.LocalAccount.Password.Value = $userPassword}
}
else
{# Desktop needs a user other than "Administrator" to be present
# This will remove the creation of the other user for server images
$ns = New-Object System.Xml.XmlNamespaceManager($unattend.NameTable)
$ns.AddNamespace("ns", $unattend.DocumentElement.NamespaceURI)
$node = $unattend.SelectSingleNode("//ns:LocalAccounts", $ns)
$node.ParentNode.RemoveChild($node) | Out-Null}
if ($is32bit) {$unattend.InnerXml = $unattend.InnerXml.Replace('processorArchitecture="amd64"', 'processorArchitecture="x86"')}
# Write it out to disk
cleanupFile $filePath; $Unattend.Save($filePath)}
Function createRunAndWaitVM ([string]$vhd, [string]$gen) {
# Function for whenever I have a VHD that is ready to run
new-vm $factoryVMName -MemoryStartupBytes 2048mb -VHDPath $vhd -Generation $Gen `
-SwitchName $virtualSwitchName | Out-Null
set-vm -Name $factoryVMName -ProcessorCount 2
Start-VM $factoryVMName
# Give the VM a moment to start before we start checking for it to stop
Sleep -Seconds 10
# Wait for the VM to be stopped for a good solid 5 seconds
do {$state1 = (Get-VM | ? name -eq $factoryVMName).State; sleep -Seconds 5
$state2 = (Get-VM | ? name -eq $factoryVMName).State; sleep -Seconds 5}
until (($state1 -eq "Off") -and ($state2 -eq "Off"))
# Clean up the VM
Remove-VM $factoryVMName -Force}
Function MountVHDandRunBlock ([string]$vhd, [scriptblock]$block) {
# This function mounts a VHD, runs a script block and unmounts the VHD.
# Drive letter of the mounted VHD is stored in $driveLetter - can be used by script blocks
$driveLetter = (Mount-VHD $vhd –passthru | Get-Disk | Get-Partition | Get-Volume).DriveLetter
&$block
dismount-vhd $vhd
# Wait 2 seconds for activity to clean up
Start-Sleep -Seconds 2
}
### Update script block
$updateCheckScriptBlock = {
# Clean up unattend file if it is there
if (test-path "$ENV:SystemDrive\Unattend.xml") {Remove-Item -Force "$ENV:SystemDrive\Unattend.xml"}
# Check to see if files need to be unblocked - if they do, do it and reboot
if ((Get-ChildItem $env:SystemDrive\Bits\PSWindowsUpdate | `
get-item -Stream "Zone.Identifier" -ErrorAction SilentlyContinue).Count -gt 0)
{Get-ChildItem $env:SystemDrive\Bits\PSWindowsUpdate | Unblock-File
invoke-expression 'shutdown -r -t 0'}
# To get here - the files are unblocked
import-module $env:SystemDrive\Bits\PSWindowsUpdate\PSWindowsUpdate
# Check if any updates are needed - leave a marker if there are
if ((Get-WUList).Count -gt 0)
{if (!(test-path $env:SystemDrive\Bits\changesMade.txt))
{New-Item $env:SystemDrive\Bits\changesMade.txt -type file}}
# Apply all the updates
Get-WUInstall -AcceptAll -IgnoreReboot -IgnoreUserInput -NotCategory "Language packs"
# Reboot if needed - otherwise shutdown because we are done
if (Get-WURebootStatus -Silent) {invoke-expression 'shutdown -r -t 0'}
else {invoke-expression 'shutdown -s -t 0'}}
### Sysprep script block
$sysprepScriptBlock = {
# Windows 10 issue - if the tiledatamodelsvc is running, it must be stopped first
get-service | ? name -eq tiledatamodelsvc | stop-service
$unattendedXmlPath = "$ENV:SystemDrive\Bits\Unattend.xml"
& "$ENV:SystemRoot\System32\Sysprep\Sysprep.exe" `/generalize `/oobe `/shutdown `/unattend:"$unattendedXmlPath"}
### Post Sysprep script block
$postSysprepScriptBlock = {
Remove-Item -Force "$ENV:SystemDrive\Unattend.xml"
# Put any code you want to run Post sysprep here
}
# This is the main function of this script
Function RunTheFactory([string]$FriendlyName, `
[string]$ISOFile, `
[string]$ProductKey, `
[string]$SKUEdition, `
[bool]$desktop = $false, `
[bool]$is32bit = $false, `
[switch]$Generation2) {
logger $FriendlyName "Starting a new cycle!"
# Setup a bunch of variables
$sysprepNeeded = $true
$baseVHD = "$($workingDir)\bases\$($FriendlyName)-base.vhdx"
$updateVHD = "$($workingDir)\$($FriendlyName)-update.vhdx"
$sysprepVHD = "$($workingDir)\$($FriendlyName)-sysprep.vhdx"
$finalVHD = "$($workingDir)\share\$($FriendlyName).vhdx"
if ($Generation2) {$VHDPartitionStyle = "GPT"; $Gen = 2} else {$VHDPartitionStyle = "MBR"; $Gen = 1}
logger $FriendlyName "Checking for existing Factory VM"
# Check if there is already a factory VM - and kill it if there is
If ((Get-VM | ? name -eq $factoryVMName).Count -gt 0)
{stop-vm $factoryVMName -TurnOff -Confirm:$false -Passthru | Remove-VM -Force}
# Check for a base VHD
if (!(test-path $baseVHD)) {
# No base VHD - we need to create one
logger $FriendlyName "No base VHD!"
# Make unattend file
logger $FriendlyName "Creating unattend file for base VHD"
# Logon count is just "large number"
makeUnattendFile -key $ProductKey -logonCount "1000" -filePath "$($workingDir)\unattend.xml" -desktop $desktop -is32bit $is32bit
# Time to create the base VHD
logger $FriendlyName "Create base VHD using Convert-WindowsImage.ps1"
$ConvertCommand = "Convert-WindowsImage"
$ConvertCommand = $ConvertCommand + " -SourcePath `"$ISOFile`" -VHDPath `"$baseVHD`""
$ConvertCommand = $ConvertCommand + " -SizeBytes 80GB -VHDFormat VHDX -UnattendPath `"$($workingDir)\unattend.xml`""
$ConvertCommand = $ConvertCommand + " -Edition $SKUEdition -VHDPartitionStyle $VHDPartitionStyle"
Invoke-Expression "& $ConvertCommand"
# Clean up unattend file - we don't need it any more
logger $FriendlyName "Remove unattend file now that that is done"
cleanupFile "$($workingDir)\unattend.xml"
logger $FriendlyName "Mount VHD and copy bits in, also set startup file"
MountVHDandRunBlock $baseVHD {
# Copy ResourceDirectory in
copy-item ($ResourceDirectory) -Destination ($driveLetter + ":\") -Recurse
# Create first logon script
$updateCheckScriptBlock | Out-String | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096}
logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."
createRunAndWaitVM $baseVHD $Gen
# Remove Page file
logger $FriendlyName "Removing the page file"
MountVHDandRunBlock $baseVHD {attrib -s -h "$($driveLetter):\pagefile.sys"
cleanupFile "$($driveLetter):\pagefile.sys"}
# Compact the base file
logger $FriendlyName "Compacting the base file"
optimize-vhd -Path $baseVHD -Mode Full}
else
{# The base VHD existed - time to check if it needs an update
logger $FriendlyName "Base VHD exists - need to check for updates"
# create new diff to check for updates
logger $FriendlyName "Create new differencing disk to check for updates"
cleanupFile $updateVHD; new-vhd -Path $updateVHD -ParentPath $baseVHD | Out-Null
logger $FriendlyName "Copy login file for update check, also make sure flag file is cleared"
MountVHDandRunBlock $updateVHD {
# Make the UpdateCheck script the logon script, make sure update flag file is deleted before we start
cleanupFile "$($driveLetter):\Bits\changesMade.txt"
cleanupFile "$($driveLetter):\Bits\Logon.ps1"
$updateCheckScriptBlock | Out-String | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096}
logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."
createRunAndWaitVM $updateVHD $Gen
# Mount the VHD
logger $FriendlyName "Mount the differencing disk"
$driveLetter = (Mount-VHD $updateVHD –passthru | Get-Disk | Get-Partition | Get-Volume).DriveLetter
# Check to see if changes were made
logger $FriendlyName "Check to see if there were any updates"
if (test-path "$($driveLetter):\Bits\changesMade.txt") {cleanupFile "$($driveLetter):\Bits\changesMade.txt"; logger $FriendlyName "Updates were found"}
else {logger $FriendlyName "No updates were found"; $sysprepNeeded = $false}
# Dismount
logger $FriendlyName "Dismount the differencing disk"
dismount-vhd $updateVHD
# Wait 2 seconds for activity to clean up
Start-Sleep -Seconds 2
# If changes were made - merge them in. If not, throw it away
if ($sysprepNeeded) {logger $FriendlyName "Merge the differencing disk"; Merge-VHD -Path $updateVHD -DestinationPath $baseVHD}
else {logger $FriendlyName "Delete the differencing disk"; CSVLogger $finalVHD; cleanupFile $updateVHD}
}
# Final Check - if the final VHD is missing - we need to sysprep and make it
if (!(test-path $finalVHD)) {$sysprepNeeded = $true}
if ($sysprepNeeded)
{# create new diff to sysprep
logger $FriendlyName "Need to run Sysprep"
logger $FriendlyName "Creating differencing disk"
cleanupFile $sysprepVHD; new-vhd -Path $sysprepVHD -ParentPath $baseVHD | Out-Null
logger $FriendlyName "Mount the differencing disk and copy in files"
MountVHDandRunBlock $sysprepVHD {
# Make unattend file
makeUnattendFile -key $ProductKey -logonCount "1" -filePath "$($driveLetter):\Bits\unattend.xml" -desktop $desktop -is32bit $is32bit
# Make the logon script
cleanupFile "$($driveLetter):\Bits\Logon.ps1"
$sysprepScriptBlock | Out-String | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096}
logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."
createRunAndWaitVM $sysprepVHD $Gen
logger $FriendlyName "Mount the differencing disk and cleanup files"
MountVHDandRunBlock $sysprepVHD {
cleanupFile "$($driveLetter):\Bits\unattend.xml"
# Make the logon script
cleanupFile "$($driveLetter):\Bits\Logon.ps1"
$postSysprepScriptBlock | Out-String | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096}
# Remove Page file
logger $FriendlyName "Removing the page file"
MountVHDandRunBlock $sysprepVHD {attrib -s -h "$($driveLetter):\pagefile.sys"
cleanupFile "$($driveLetter):\pagefile.sys"}
# Produce the final disk
cleanupFile $finalVHD
logger $FriendlyName "Convert differencing disk into pristine base image"
Convert-VHD -Path $sysprepVHD -DestinationPath $finalVHD -VHDType Dynamic
logger $FriendlyName "Delete differencing disk"
CSVLogger $finalVHD -sysprepped
cleanupFile $sysprepVHD
}
}
RunTheFactory -FriendlyName "Windows Server 2012 R2 DataCenter with GUI" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenter"
RunTheFactory -FriendlyName "Windows Server 2012 R2 DataCenter Core" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenterCore"
RunTheFactory -FriendlyName "Windows Server 2012 R2 DataCenter with GUI - Gen 2" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenter" -Generation2
RunTheFactory -FriendlyName "Windows Server 2012 R2 DataCenter Core - Gen 2" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenterCore" -Generation2
RunTheFactory -FriendlyName "Windows Server 2012 DataCenter with GUI" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenter"
RunTheFactory -FriendlyName "Windows Server 2012 DataCenter Core" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenterCore"
RunTheFactory -FriendlyName "Windows Server 2012 DataCenter with GUI - Gen 2" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenter" -Generation2
RunTheFactory -FriendlyName "Windows Server 2012 DataCenter Core - Gen 2" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenterCore" -Generation2
RunTheFactory -FriendlyName "Windows 8.1 Professional" -ISOFile $81x64Image -ProductKey $Windows81Key -SKUEdition "Professional" -desktop $true
RunTheFactory -FriendlyName "Windows 8.1 Professional - Gen 2" -ISOFile $81x64Image -ProductKey $Windows81Key -SKUEdition "Professional" -Generation2 -desktop $true
RunTheFactory -FriendlyName "Windows 8.1 Professional - 32 bit" -ISOFile $81x86Image -ProductKey $Windows81Key -SKUEdition "Professional" -desktop $true -is32bit $true
RunTheFactory -FriendlyName "Windows 8 Professional" -ISOFile $8x64Image -ProductKey $Windows8Key -SKUEdition "Professional" -desktop $true
RunTheFactory -FriendlyName "Windows 8 Professional - Gen 2" -ISOFile $8x64Image -ProductKey $Windows8Key -SKUEdition "Professional" -Generation2 -desktop $true
RunTheFactory -FriendlyName "Windows 8 Professional - 32 bit" -ISOFile $8x86Image -ProductKey $Windows8Key -SKUEdition "Professional" -desktop $true -is32bit $true
I hope this is useful!
Cheers,
Ben