Creating a small footprint, base image Part 4 | Bringing it all together with automation

New-WindowsImage -size small | Test-Lab

One of the time consuming steps to deploying new VMs is the time spend managing Images and and applying patches. I’m not big on Golden images. I tend to use a fully patched VHDX or VMDK  and let DSC handle the configuration and software. This is not the fastest, and at scale you need to create more then one image based on what saves the most time.  (IIS, SQL, Exchange, etc…).

In this series, I’m going to go over how I create a a baseline image of 2012r2 with PowerShell. Also because Install-WindowsFeature has issues when the target VM has been patched, we are also going to create a fully patched WIM to use as -source.

Blogs in this Series

Get-Script | Set-Automation

This took a little longer than I had expected. However I’m satisfied with the results if not the quality of my code.

I shortened up the process in Part1 by pointing Convert-WindowsImage.ps1 at the ISO directly. And I added some code deal with working folder and cleaning files we dont need.

The basic process is as follows

  1. Create the VHDX from ISO
  2. Insert three files
    1. unattend.xml : sets up a silent OOBE, autologin and start the first script.
    2. FirstRun.ps1 : handles any windows features and sets creates an ‘atstartup’ task to run the next script
    3. AtStarup.ps1 : Installs windows updates, and reboots, once none left shuts down the computer
  3. Create a VM
    1. If WIM, attach the ISO
    2. start VM
    3. wait for VM to shutdown
  4. If it’s the Template
    1. copy the VHDX
    2. replace AtStartup with a script that cleans up extra files and runs sysprep
    3. createa new VMfor the sysprep
    4. start VM and wait for it to shutdown
    5. delete sysprep VM
  5. Create an WIM from VHDX
  6. If template
    1. create VHDX from WIM
  7. delete VM
  8. copy VHDX and for source the WIM to the OutFolder
  9. Create VM using VHDX in OutFolder
  10. Create a scheduled task to run every Wednesday to boot the VM’s and rerun steps 4-9 exporting the Template VHDX and Source.WIM to second output folder

The process takes a few hours based on the speed of your processor and hard drive.

Start-ImageBuild.ps1

There are two functions in this one.

Convert-WindowsImage is a wrapper around the ps1 file so I can use it multiple times.

Start-ImageBuild is an ungodly long function I created just so I can have parameters.

At the end it’ calls Start-ImageBuild with the values that match my test environment

now Start-ImageBuild can be provided a -WIMOnly parameter if I only want Source.WIM

#requires -Version 3 -Modules Dism, Hyper-V, ScheduledTasks
#Wrapper arround Convert-WindowsImage script to it acts like a function.
function Convert-WindowsImage
{
Param
(
[Parameter(ParameterSetName = 'SRC', Mandatory = $true, ValueFromPipeline = $true)]
[Alias('WIM')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$SourcePath,
[Parameter(ParameterSetName = 'SRC')]
[Alias('VHD')]
[string]
[ValidateNotNullOrEmpty()]
$VHDPath,
[Parameter(ParameterSetName = 'SRC')]
[Alias('WorkDir')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path $_
}
)]
$WorkingDirectory = $pwd,
[Parameter(ParameterSetName = 'SRC')]
[Alias('Size')]
[UInt64]
[ValidateNotNullOrEmpty()]
[ValidateRange(512MB, 64TB)]
$SizeBytes = 40GB,
[Parameter(ParameterSetName = 'SRC')]
[Alias('Format')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('VHD', 'VHDX')]
$VHDFormat = 'VHD',
[Parameter(ParameterSetName = 'SRC')]
[Alias('DiskType')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('Dynamic', 'Fixed')]
$VHDType = 'Dynamic',
[Parameter(ParameterSetName = 'SRC')]
[Alias('Unattend')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$UnattendPath,
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
$Feature,
[Parameter(ParameterSetName = 'SRC')]
[Alias('SKU')]
[string]
[ValidateNotNullOrEmpty()]
$Edition,
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[string]
$BCDBoot = 'bcdboot.exe',
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[switch]
$Passthru,
[Parameter(ParameterSetName = 'UI')]
[switch]
$ShowUI,
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('None', 'Serial', '1394', 'USB', 'Local', 'Network')]
$EnableDebugger = 'None',
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('MBR', 'GPT')]
$VHDPartitionStyle = 'MBR',
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('NativeBoot', 'VirtualMachine')]
$BCDinVHD = 'VirtualMachine',
[Parameter(ParameterSetName = 'SRC')]
[Switch]
$ExpandOnNativeBoot = $true,
[Parameter(ParameterSetName = 'SRC')]
[Switch]
$RemoteDesktopEnable = $False,
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$Driver
)
#$psboundparameters
. "$($PSScriptRoot)\Convert-WindowsImage.ps1" @psboundparameters
}
#Creates fully patched and compatcted CORE VHDX and Fully Patched GUI WIM with -Features installed. and Places them in -OutPath
function Start-ImageBuild
{
[CmdletBinding()]
[Alias()]
Param
(
# OutPath
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias('op')]
[String]
$OutPath,
# ISO Path
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias('ip')]
[String]
$IsoPath,
# VMSwitch to attach to
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias('vs')]
[String]
$VmSwitch,
# Create WIM Only
[switch]
$WimOnly,
# Iso Core Edition
[int]
[Alias('CoreEdition')]
$IsoCoreEdition = 3,
# Iso GUI Edition
[int]
[Alias('GuiEdition')]
$IsoGuiEdition = 4,
#features used by get-windowsFeature that you want installed on the WIM
# [string]
# [ValidateNotNullOrEmpty()]
# $Feature,
#Save patched vhdx for use with update-ImageBuild
[switch]
$SavedPatchVHDX,
# Working Folder
[Parameter()]
[Alias('wf')]
[String]
$WorkingFolder = $OutPath
)
#region validate input and dependent files
Try
{
Write-Verbose -Message "Testing $OutPath"
if (-not (Test-Path $OutPath))
{
Write-Verbose -Message "Creating $OutPath"
New-Item -ItemType directory -Path $OutPath -ErrorAction Stop
}
Write-Verbose -Message "Testing $WorkingFolder"
if (-not (Test-Path $WorkingFolder))
{
Write-Verbose -Message "Creating $WorkingFolder"
New-Item -ItemType directory -Path $WorkingFolder -ErrorAction Stop
}
Write-Verbose -Message "Testing $VmSwitch"
$null = Get-VMSwitch $VmSwitch -ErrorAction Stop
Write-Verbose -Message "Testing $IsoPath"
$null = Test-Path -Path $IsoPath -ErrorAction stop
Write-Verbose -Message "Testing $PSScriptRoot\unattend.xml"
$null = Test-Path -Path $PSScriptRoot\unattend.xml -ErrorAction stop
Write-Verbose -Message "Testing $PSScriptRoot\Convert-WindowsImage.ps1"
$null = Test-Path -Path $PSScriptRoot\Convert-WindowsImage.ps1 -ErrorAction stop
}
catch
{
$msg = "Failed $($_.Exception.Message)"
Write-Error $msg
throw 'Input validation failed'
}
#endregion
if ($WimOnly)
{
$vmSet = 'Source'
}
else
{
$vmSet = 'Source', 'Template'
}
foreach ($vmType in $vmSet )
{
#region Create VM
$vhdFile = "$($vmType)_Patch.vhdx"
$vmName = "$($vmType)"
#<#
# Remove any previous VMs
if (Test-Path -Path "$WorkingFolder\$($vmSet).wim")
{
Write-Warning -Message "Removinmg old WIM file: $WorkingFolder\$($vmSet).wim"
Remove-Item -Path "$WorkingFolder\$($vmSet).wim" -Force
}
if (Test-Path -Path $WorkingFolder\$vhdFile)
{
Write-Warning -Message "Removinmg old vhdx file: $WorkingFolder\$vhdFile"
Remove-Item -Path "$WorkingFolder\$vhdFile" -Force
}
if (Get-VM -Name "$($vmType)" -ErrorAction SilentlyContinue)
{
Write-Warning -Message "Removinmg old vm $($vmType)"
Remove-VM -Name "$($vmType)" -Force
}
Write-Verbose -Message "Start creation of $vmType"
if ($vmType -eq 'Template')
{
$UseEdition = $IsoCoreEdition
}
else
{
$UseEdition = $IsoGuiEdition
}
$CwiParamaters = @{
SourcePath = $IsoPath
VHDPath = "$WorkingFolder\$vhdFile"
SizeBytes = 40GB
VHDFormat = 'VHDX'
VHDPartitionStyle = 'GPT'
VHDType = 'Dynamic'
UnattendPath = "$PSScriptRoot\unattend.xml"
Edition = $UseEdition
}
$CwiParamaters |Format-Table
Write-Verbose -Message 'Creating VHDX from ISO'
#. "$($PSScriptRoot)\Convert-WindowsImage.ps1" @Paramaters -Passthru
Convert-WindowsImage @CwiParamaters -Passthru
if (-not (Test-Path -Path "$WorkingFolder\Mount" ))
{
mkdir -Path "$WorkingFolder\Mount" -Verbose
}
Mount-WindowsImage -ImagePath "$WorkingFolder\$vhdFile" -Path "$WorkingFolder\Mount" -Index 1
if (-not (Test-Path -Path "$WorkingFolder\Mount\PSTemp"))
{
mkdir -Path "$WorkingFolder\Mount\PSTemp" -Verbose
}
Copy-Item -Path "$PSScriptRoot\$($vmType)-FirstRun.ps1" -Destination "$WorkingFolder\Mount\PSTemp\FirstRun.ps1" -Verbose
Copy-Item -Path "$PSScriptRoot\$($vmType)-Features.txt" -Destination "$WorkingFolder\Mount\PSTemp\Features.txt" -ErrorAction SilentlyContinue -Verbose
Copy-Item -Path "$PSScriptRoot\$($vmType)-FeaturesIncludingSub.txt" -Destination "$WorkingFolder\Mount\PSTemp\FeaturesIncludingSub.txt" -ErrorAction SilentlyContinue -Verbose
Copy-Item -Path "$PSScriptRoot\WinUpdate.ps1" -Destination "$WorkingFolder\Mount\PSTemp\AtStartup.ps1" -Verbose
Dismount-WindowsImage -Path "$WorkingFolder\Mount" -Save
Write-Verbose -Message "Creating $vmName"
New-VM -Name $vmName -VHDPath "$WorkingFolder\$vhdFile" -MemoryStartupBytes 1024MB -SwitchName $VmSwitch -Generation 2 -Verbose|
Set-VMProcessor -Count 2 -Verbose
if ($vmType -eq 'Source')
{
Add-VMDvdDrive -Path $IsoPath -VMName $vmName -Verbose
}
Write-Verbose -Message "Starting Patchrun on $vmName"
Start-VM $vmName -Verbose
#endregion
#region Wait for Patch
while (Get-VM $vmName | Where-Object -Property state -EQ -Value 'running')
{
Write-Verbose -Message "Wating for $vmName to stop"
Start-Sleep -Seconds 30
}
#endregion
#region Sysprep
if ($vmType -eq 'Template')
{
Write-Verbose -Message "Copying $($vmType)_Patch.vhdx to$($vmType)_Sysprep.vhdx"
Copy-Item -Path "$WorkingFolder\$($vmType)_Patch.vhdx" -Destination "$WorkingFolder\$($vmType)_Sysprep.vhdx" -Force -Verbose
$vhdFile = "$($vmType)_Sysprep.vhdx"
$vmName = "$($vmType)_Sysprep"
New-VM -Name $vmName -VHDPath "$WorkingFolder\$vhdFile" -MemoryStartupBytes 1024MB -Generation 2 -Verbose |
Set-VMProcessor -Count 2 -Verbose
Write-Verbose -Message "Adding SysPrep script to $WorkingFolder\$vhdFile"
Mount-WindowsImage -ImagePath "$WorkingFolder\$vhdFile" -Path "$WorkingFolder\Mount" -Index 1 -Verbose
Copy-Item -Path "$PSScriptRoot\SysPrep.ps1" -Destination "$WorkingFolder\Mount\PSTemp\AtStartup.ps1" -Force -Verbose
Dismount-WindowsImage -Path "$WorkingFolder\Mount" -Save -Verbose
Write-Verbose -Message "Starting Cleanup and Sysprep of $vmName"
Start-VM $vmName -Verbose
while (Get-VM $vmName | Where-Object -Property state -EQ -Value 'running')
{
Write-Verbose -Message "Wating for $vmName to stop"
Start-Sleep -Seconds 30
}
Remove-VM $vmName -Force -Verbose
}
#endregion
#region Create WIM
Write-Verbose -Message "Creating WIM from $WorkingFolder\$vhdFile"
Mount-WindowsImage -ImagePath "$WorkingFolder\$vhdFile" -Path "$WorkingFolder\Mount" -Index 1 -Verbose
New-WindowsImage -CapturePath "$WorkingFolder\Mount" -Name "2012r2_$vmType" -ImagePath "$WorkingFolder\$($vmType).wim" -Description "2012r2 $vmType Patched $(Get-Date)" -Verify -Verbose
Dismount-WindowsImage -Path "$WorkingFolder\Mount" -Discard -Verbose
if ($vmType -eq 'Template')
{
$vhdFile = "$($vmType)_Sysprep.vhdx"
$CwiParamaters = @{
SourcePath = "$WorkingFolder\$($vmType).wim"
VHDPath = "$WorkingFolder\$($vmType)_Production.vhdx"
SizeBytes = 40GB
VHDFormat = 'VHDX'
VHDPartitionStyle = 'GPT'
VHDType = 'Dynamic'
Edition = 1
}
$CwiParamaters | Format-Table
Write-Verbose -Message " Creating VHDX from WIM : $WorkingFolder\$($vmType).wim"
Convert-WindowsImage @CwiParamaters -Passthru -Verbose
Write-Verbose -Message 'Removing Temp files'
Remove-Item -Path "$WorkingFolder\$($vmType).wim" -Force
Remove-Item -Path "$WorkingFolder\$($vmType)_Sysprep.vhdx" -Force
}
#endregion
#region Cleanup
if ($OutPath -ne $WorkingFolder)
{
Write-Verbose -Message "Moving $vmType to $OutPath"
Remove-VM $vmType -Force -Verbose
Copy-Item "$WorkingFolder\$($vmType)*.vhdx" $OutPath -Force -Verbose
Copy-Item "$WorkingFolder\$($vmType).wim" $OutPath -Force -Verbose
Write-Verbose -Message "Creating vm : $vmType"
New-VM -Name $vmType -VHDPath "$OutPath\$($vmType)_Patch.vhdx" -MemoryStartupBytes 1024MB -SwitchName $VmSwitch -Generation 2 |
Set-VMProcessor -Count 2
}
#endregion
}
#region remove working if diferent from Out
if ($OutPath -ne $WorkingFolder)
{
Write-Verbose -Message "Cleandup of $WorkingFolder"
Remove-Item -Path $WorkingFolder -Recurse -Force -Verbose
}
#endregion
#region setup Monthly update
if ( -not (Get-ScheduledTask -TaskName UpdateSourceAndTemplate))
{
$Paramaters = @{
Action = New-ScheduledTaskAction -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File $PSScriptRoot\Update-SourceAndTemplate.ps1"
Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Wednesday -At 1AM
Settings = New-ScheduledTaskSettingsSet
}
$TaskObject = New-ScheduledTask @Paramaters -Verbose
Register-ScheduledTask UpdateSourceAndTemplate -InputObject $TaskObject -User 'nt authority\system' -Verbose
}
#endregion
}
Start-Transcript -Path $env:ALLUSERSPROFILE\logs\ImageBuild.log
# Production
#Start-ImageBuild -OutPath 'D:\BuildOut' -WorkingFolder 'd:\BuildWorking' -IsoPath 'D:\ISO\WindowsServer\Win_Svr_2012_R2_64Bit_English.ISO' -VmSwitch Isolated1 -Verbose
# Lab
Start-ImageBuild -OutPath 'g:\BuildOut' -WorkingFilder 'd:\BuildWorking' -IsoPath 'C:\iso\Server2012R2.ISO' -VmSwitch TestLab -Verbose
Stop-Transcript
view raw Start-ImageBuild.ps1 hosted with ❤ by GitHub

Unattend.xml

This is a simple Unattend.xml that:

  • Sets the Language to US English
  • Accepts the License
  • Sets the the Administrator Password to P@ssword
  • Sets 1 autologon
  • runs the FirstRun.ps1 script on logon once.
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="oobeSystem">
<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>en-us</InputLocale>
<SystemLocale>en-us</SystemLocale>
<UILanguage>en-us</UILanguage>
<UserLocale>en-us</UserLocale>
</component>
<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">
<AutoLogon>
<Password>
<Value>P@ssword</Value>
</Password>
<LogonCount>1</LogonCount>
<Username>administrator</Username>
<Enabled>true</Enabled>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<CommandLine>%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\PSTemp\FirstRun.ps1</CommandLine>
<Description>Run Execution Policy</Description>
<Order>1</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
<OOBE>
<HideEULAPage>true</HideEULAPage>
<NetworkLocation>Work</NetworkLocation>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>P@ssword</Value>
</AdministratorPassword>
</UserAccounts>
</component>
</settings>
<cpi:offlineImage cpi:source="wim:c:/iso/install.wim#Windows Server 2012 R2 SERVERDATACENTERCORE" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>
view raw unattend.xml hosted with ❤ by GitHub

Source-FirstRun.ps1

This will be copied into the image as FirstRun.ps1

  • Add and Windows Feature listed as ‘Removed’ using the attached ISO as source
  • Adds any Feature from Feature.txt
  • Adds any Feature with sub-feature from FeaturesIncludingSub.txt
  • Create AtStartup Task to run AtStartup.ps1
  • Reboot
#requires -Version 3 -Modules ScheduledTasks, ServerManager
# Add any features required.
Start-Transcript -Path $PSScriptRoot\FirstRun.log
Get-WindowsFeature |
Where-Object -Property InstallState -EQ -Value Removed |
Install-WindowsFeature -Source D:\sources\sxs -Verbose
$features = Get-Content -Path $PSScriptRoot\Features.txt
Install-WindowsFeature $features -Verbose
$features = Get-Content -Path $PSScriptRoot\FeaturesIncludingSub.txt
Install-WindowsFeature $features -IncludeAllSubFeature -Verbose
$Paramaters = @{
Action = New-ScheduledTaskAction -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\PSTemp\AtStartup.ps1'
Trigger = New-ScheduledTaskTrigger -AtStartup
Settings = New-ScheduledTaskSettingsSet
}
$TaskObject = New-ScheduledTask @Paramaters
Register-ScheduledTask AtStartup -InputObject $TaskObject -User 'nt authority\system' -Verbose
Start-Sleep -Seconds 20
Restart-Computer -Verbose -Force
Stop-Transcript
view raw Source-FirstRun.ps1 hosted with ❤ by GitHub

Template-FirstRun.ps1

Same as Source-FirstRun.ps1, without adding any ‘Removed’ Features

Note: I don’t actually add any Features to Template VHDX in my example but the code is there in case it’s needed. You just have to add a Template-Features.txt and/or Template-FeaturesIncludingSub.txt

#requires -Version 2 -Modules ScheduledTasks, ServerManager
# Add any features required.
Start-Transcript -Path $PSScriptRoot\FirstRun.log
$features = Get-Content -Path $PSScriptRoot\Features.txt
Install-WindowsFeature $features -IncludeAllSubFeature -Verbose
$features = Get-Content -Path $PSScriptRoot\FeaturesIncludingSub.txt
Install-WindowsFeature $features -Verbose
$Paramaters = @{
Action = New-ScheduledTaskAction -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\PSTemp\AtStartup.ps1'
Trigger = New-ScheduledTaskTrigger -AtStartup
Settings = New-ScheduledTaskSettingsSet
}
$TaskObject = New-ScheduledTask @Paramaters
Register-ScheduledTask AtStartup -InputObject $TaskObject -User 'nt authority\system' -Verbose
Start-Sleep -Seconds 20
Restart-Computer -Verbose -Force
Stop-Transcript

Source-Features.txt

Features to add to Sorce.WIM

AD-Domain-Services
AD-Certificate
ADCS-Cert-Authority
view raw Source-Features.txt hosted with ❤ by GitHub

Source-FeaturesIncludingSub.txt

Features including Sub-Features to add to Source.WIM

DNS
DHCP
File-Services
RSAT

WinUpdate.ps1

Copied into both Template.VHDX and Source.VHDX as AtStarup.ps1. This scrip is a modification of Add-WindowsUpdate James O’Neil’s Blog with some additional parameters

  • -ForceRestart  to reboot after each patch run if it needs it or not
  • -ShutdownOnNoUpdate  to shutdown once there are no additional patches.
#requires -Version 2
Function Add-WindowsUpdate
{
param (
[string]$Criteria = "IsInstalled=0 and Type='Software'" ,
[switch]$AutoRestart,
[Switch]$ShutdownAfterUpdate,
[switch]$ForceRestart,
[Switch]$ShutdownOnNoUpdate
)
$resultcode = @{
0 = 'Not Started'
1 = 'In Progress'
2 = 'Succeeded'
3 = 'Succeeded With Errors'
4 = 'Failed'
5 = 'Aborted'
}
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
Write-Progress -Activity 'Updating' -Status 'Checking available updates'
$updates = $updateSession.CreateupdateSearcher().Search($Criteria).Updates
$updates
if ($updates.Count -eq 0)
{
Write-Verbose -Message 'There are no applicable updates.' -Verbose
if ($ShutdownOnNoUpdate)
{
Stop-Computer
}
}
else
{
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updates
Write-Progress -Activity 'Updating' -Status "Downloading $($downloader.Updates.count) updates"
$Result = $downloader.Download()
$Result
if (($Result.Hresult -eq 0) -and (($Result.resultCode -eq 2) -or ($Result.resultCode -eq 3)) )
{
$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates |
Where-Object -FilterScript {
$_.isdownloaded
} |
ForEach-Object -Process {
$null = $updatesToInstall.Add($_)
}
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
Write-Progress -Activity 'Updating' -Status "Installing $($installer.Updates.count) updates"
$installationResult = $installer.Install()
$installationResult
$Global:counter = -1
$installer.updates | Format-Table -AutoSize -Property Title, EulaAccepted, @{
label = 'Result'
expression = {
$resultcode[$installationResult.GetUpdateResult($Global:counter++).resultCode ]
}
}
if ($AutoRestart -and $installationResult.rebootRequired)
{
Restart-Computer
}
if ($ForceRestart)
{
Restart-Computer
}
if ($ShutdownAfterUpdate)
{
Stop-Computer
}
}
}
}
Start-Transcript -Path $PSScriptRoot\Patch.log
Add-WindowsUpdate -ForceRestart -ShutdownOnNoUpdate
Stop-Transcript
view raw WinUpdate.ps1 hosted with ❤ by GitHub

SysPrep.ps1

Copied into Templat_Sysprep.VHDX as AtStartup.ps1

  • Unregistered the AtStartup Task
  • Deletes c:\Unattent.xml
  • Deletes all other files in c:\pstemp but itself.
  • Removes all ‘Available’ Windows Features
  • Use Dism to remove any overridden patches.
  • Defrag and consolidate free space (still don’t think this is necessary, but it’s not going to hurt)
  • Sysprep
    • Silent
    • OOBE
    • leave Hyper-V drivers active (this speeds up first boot so long as the VHDX is ran on the same version of Hyper-V as the machine your building on.)
    • Reboot
#requires -Version 1 -Modules ScheduledTasks, ServerManager
#region cleanup
Start-Transcript -Path c:\sysprep.log
Get-ScheduledTask -TaskName AtStartup | Unregister-ScheduledTask -Confirm:$false
Remove-Item -Path c:\unattend.xml
Get-ChildItem -Path c:\pstemp\ -Exclude AtStartup.ps1 | Remove-Item
Get-WindowsFeature |
Where-Object -FilterScript {
$_.Installed -eq 0 -and $_.InstallState -eq 'Available'
} |
Uninstall-WindowsFeature -remove
Dism.exe /online /cleanup-image /StartComponentCleanup /ResetBase
Defrag.exe c: /UVX
#endregion
#region sysprep
C:\Windows\System32\sysprep\sysprep.exe /quiet /generalize /oobe /shutdown /mode:vm
Stop-Transcript
#endregion
view raw SysPrep.ps1 hosted with ❤ by GitHub

Update-SourceAndTempldate.ps1

Script scheduled to run every Wednesday to update the Template and Source VHDX and WIM. This is a lot like Start-ImageBuild.ps1. you will have to edit the bottom of the script to match your environment

  • Has a wrapper for Convert-WindowsImage
  • Starts the Existing VM left in place by Start-ImageBuild.ps1
  • waits for VM to shutdown
  • gets the VHDX path from the VM configuration
  • for Template
    • Copies VHDX
    • Create’s new VM
    • copies in Sysprep.ps1
    • Starts VM
    • waits for it to stop
    • Delets VM
  • creates WIM form VHDX
  • for Template
    • Delets SysPrep VHDX
    • Converts WIM to VHDX
  • copies Templast_Production.VHDX and Source.WIM to OutPath
#requires -Version 3 -Modules Dism, Hyper-V
#Wrapper arround Convert-WindowsImage script to it acts like a function.
function Convert-WindowsImage
{
Param
(
[Parameter(ParameterSetName = 'SRC', Mandatory = $true, ValueFromPipeline = $true)]
[Alias('WIM')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$SourcePath,
[Parameter(ParameterSetName = 'SRC')]
[Alias('VHD')]
[string]
[ValidateNotNullOrEmpty()]
$VHDPath,
[Parameter(ParameterSetName = 'SRC')]
[Alias('WorkDir')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path $_
}
)]
$WorkingDirectory = $pwd,
[Parameter(ParameterSetName = 'SRC')]
[Alias('Size')]
[UInt64]
[ValidateNotNullOrEmpty()]
[ValidateRange(512MB, 64TB)]
$SizeBytes = 40GB,
[Parameter(ParameterSetName = 'SRC')]
[Alias('Format')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('VHD', 'VHDX')]
$VHDFormat = 'VHD',
[Parameter(ParameterSetName = 'SRC')]
[Alias('DiskType')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('Dynamic', 'Fixed')]
$VHDType = 'Dynamic',
[Parameter(ParameterSetName = 'SRC')]
[Alias('Unattend')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$UnattendPath,
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
$Feature,
[Parameter(ParameterSetName = 'SRC')]
[Alias('SKU')]
[string]
[ValidateNotNullOrEmpty()]
$Edition,
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[string]
$BCDBoot = 'bcdboot.exe',
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[switch]
$Passthru,
[Parameter(ParameterSetName = 'UI')]
[switch]
$ShowUI,
[Parameter(ParameterSetName = 'SRC')]
[Parameter(ParameterSetName = 'UI')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('None', 'Serial', '1394', 'USB', 'Local', 'Network')]
$EnableDebugger = 'None',
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('MBR', 'GPT')]
$VHDPartitionStyle = 'MBR',
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateSet('NativeBoot', 'VirtualMachine')]
$BCDinVHD = 'VirtualMachine',
[Parameter(ParameterSetName = 'SRC')]
[Switch]
$ExpandOnNativeBoot = $true,
[Parameter(ParameterSetName = 'SRC')]
[Switch]
$RemoteDesktopEnable = $False,
[Parameter(ParameterSetName = 'SRC')]
[string]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $(Resolve-Path $_)
}
)]
$Driver
)
#$psboundparameters
. "$($PSScriptRoot)\Convert-WindowsImage.ps1" @psboundparameters
}
function Update-SourceAndTemplate
{
[CmdletBinding()]
[Alias()]
Param
(
# OutPath
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias('op')]
[String]
$OutPath,
# Create WIM Only
[switch]
$WimOnly,
# Working Folder
[Parameter()]
[Alias('wf')]
[String]
$WorkingFolder = $OutPath
)
#region validate input and dependent files
Try
{
Write-Verbose -Message "Testing $OutPath"
if (-not (Test-Path $OutPath))
{
Write-Verbose -Message "Creating $OutPath"
New-Item -ItemType directory -Path $OutPath -ErrorAction Stop -Verbose
}
Write-Verbose -Message "Testing $WorkingFolder"
if (-not (Test-Path $WorkingFolder))
{
Write-Verbose -Message "Creating $WorkingFolder"
New-Item -ItemType directory -Path $WorkingFolder -ErrorAction Stop -Verbose
}
if (-not (Test-Path -Path "$WorkingFolder\Mount" ))
{
mkdir -Path "$WorkingFolder\Mount" -Verbose
}
}
catch
{
$msg = "Failed $($_.Exception.Message)"
Write-Error $msg
throw 'Input validation failed'
}
#endregion
if ($WimOnly)
{
$vmSet = 'Source'
}
else
{
$vmSet = 'Source', 'Template'
}
foreach ($vmType in $vmSet )
{
#region cleanup target WIM
if (Test-Path -Path "$WorkingFolder\$($vmType).wim")
{
Remove-Item -Path "$WorkingFolder\$($vmType).wim" -Verbose
}
#endregion
#region StartVM and wait
Write-Verbose -Message "starting vm : $vmType"
Start-VM $vmType -Verbose
Start-Sleep -Seconds 30
while (Get-VM $vmType | Where-Object -Property state -EQ -Value 'running')
{
Write-Verbose -Message "Wating for $vmType to stop"
Start-Sleep -Seconds 30
}
$vhdFile = (Get-VM $vmType | Get-VMHardDiskDrive -Verbose).Path
#region Sysprep
if ($vmType -eq 'Template')
{
Write-Verbose -Message "Copying $vhdFile to $WorkingFolder\$($vmType)_Sysprep.vhdx"
Copy-Item -Path "$vhdFile" -Destination "$WorkingFolder\$($vmType)_Sysprep.vhdx" -Force -Verbose
$vhdFile = "$WorkingFolder\$($vmType)_Sysprep.vhdx"
$vmName = "$($vmType)_Sysprep"
New-VM -Name $vmName -VHDPath $vhdFile -MemoryStartupBytes 1024MB -Generation 2 -Verbose|
Set-VMProcessor -Count 2 -Verbose
Write-Verbose -Message "Adding SysPrep script to $vhdFile"
Mount-WindowsImage -ImagePath $vhdFile -Path "$WorkingFolder\Mount" -Index 1 -Verbose
Copy-Item -Path "$PSScriptRoot\SysPrep.ps1" -Destination "$WorkingFolder\Mount\PSTemp\AtStartup.ps1" -Force -Verbose
Dismount-WindowsImage -Path "$WorkingFolder\Mount" -Save -Verbose
Write-Verbose -Message "Starting Cleanup and Sysprep of $vmName"
Start-VM $vmName -Verbose
Start-Sleep -Seconds 30
while (Get-VM $vmName | Where-Object -Property state -EQ -Value 'running')
{
Write-Verbose -Message "Wating for $vmName to stop"
Start-Sleep -Seconds 30
}
Remove-VM $vmName -Force
}
#endregion
#region Create WIM
Write-Verbose -Message "Creating WIM from $vhdFile"
Mount-WindowsImage -ImagePath "$vhdFile" -Path "$WorkingFolder\Mount" -Index 1 -Verbose
New-WindowsImage -CapturePath "$WorkingFolder\Mount" -Name "2012r2_$vmType" -ImagePath "$WorkingFolder\$($vmType).wim" -Description "2012r2 $vmType Patched $(Get-Date)" -Verify -Verbose
Dismount-WindowsImage -Path "$WorkingFolder\Mount" -Discard -Verbose
if ($vmType -eq 'Template')
{
$vhdFile = "$WorkingFolder\$($vmType)_Sysprep.vhdx"
$CwiParamaters = @{
SourcePath = "$WorkingFolder\$($vmType).wim"
VHDPath = "$WorkingFolder\$($vmType)_Production.vhdx"
SizeBytes = 40GB
VHDFormat = 'VHDX'
VHDPartitionStyle = 'GPT'
VHDType = 'Dynamic'
Edition = 1
}
$CwiParamaters | Format-Table
Write-Verbose -Message " Creating VHDX from WIM : $WorkingFolder\$($vmType).wim"
Convert-WindowsImage @CwiParamaters -Passthru -Verbose
Write-Verbose -Message 'Removing Temp files'
Remove-Item -Path "$WorkingFolder\$($vmType).wim" -Force -Verbose
Remove-Item -Path "$WorkingFolder\$($vmType)_Sysprep.vhdx" -Force -Verbose
}
#endregion
if ($OutPath -ne $WorkingFolder)
{
Write-Verbose -Message "Moving $vmType to $OutPath"
Copy-Item "$WorkingFolder\$($vmType)*.vhdx" $OutPath -Force -Verbose
Copy-Item "$WorkingFolder\$($vmType).wim" $OutPath -Force -Verbose -ErrorAction SilentlyContinue
}
}
if ($OutPath -ne $WorkingFolder)
{
Remove-Item -Path $WorkingFolder -Recurse -Force
}
}
Start-Transcript -Path "$env:ALLUSERSPROFILE\Logs\UpdateSorce.log"
#production
#Update-SourceAndTemplate -OutPath d:\UpdateOut -WorkingFolder d:\UpdateWorking -Verbose
#Lab
Update-SourceAndTemplate -OutPath G:\UpdateOut -WorkingFolder G:\UpdateWorking -Verbose
Stop-Transcript

Now the Convert-WindowsImage i’m useing has been modified as noted in Part 3 of this Series

I have packaged all the files together, including the modified Convert-WindowsImage.ps1 in a single Download (This is a temporary location until I find a better place to store files.)