Hey Checkyourlogs fans,

Today I peeled out and updated part of my BigDemo Script that builds Base Images for my lab as a Standalone Script.
It’s called BaseImageBuilder.PS1 and the source code can be found at:
https://github.com/dkawula/Operations/blob/master/Hyper-V/BaseImageBuilder.ps1
<#
Created: 2018-02-01
Version: 1.0
Author Dave Kawula MVP and Thomas Rayner MVP
Homepage: http://www.checkyourlogs.net
Disclaimer:
This script is provided "AS IS" with no warranties, confers no rights and
is not supported by the authors or Checkyourlogs or MVPDays Publishing
Author - Dave Kawula
Twitter: @DaveKawula
Blog : http://www.checkyourlogs.net
Author - Thomas Rayner
Twitter: @MrThomasRayner
Blog : http://workingsysadmin.com
.Synopsis
Creates Gold VHDx files used for your Hyper-V Lab
.DESCRIPTION
Script is used to create base images for the lab
You will need to change the <ProductKey> Variable as it has been removed.
.EXAMPLE
TODO: Dave, add something more meaningful in here
.PARAMETER WorkingDir
Transactional directory for files to be staged and written
.PARAMETER Organization
Org that the VMs will belong to
.PARAMETER Owner
Name to fill in for the OSs Owner field
.PARAMETER TimeZone
Timezone used by the VMs
.PARAMETER AdminPassword
Administrative password for the VMs
.PARAMETER DomainName
#>
#region Parameters
[cmdletbinding()]
param
(
[Parameter(Mandatory)]
[ValidateScript({ $_ -match '[^\\]$' })] #ensure WorkingDir does not end in a backslash, otherwise issues are going to come up below
[string]
$WorkingDir = 'c:\ClusterStoreage\Volume1\DCBuild',
[Parameter(Mandatory)]
[string]
$Organization = 'MVP Rockstars',
[Parameter(Mandatory)]
[string]
$Owner = 'Dave Kawula',
[Parameter(Mandatory)]
[ValidateScript({ $_ -in ([System.TimeZoneInfo]::GetSystemTimeZones()).ID })] #ensure a valid TimeZone was passed
[string]
$Timezone = 'Pacific Standard Time',
[Parameter(Mandatory)]
[string]
$adminPassword = 'P@ssw0rd'
)
#endregion
#region functions ...
function New-UnattendFile
{
param
(
[string] $filePath
)
# Reload template - clone is necessary as PowerShell thinks this is a "complex" object
$unattend = $unattendSource.Clone()
# Customize unattend XML
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.RegisteredOrganization = 'Azure Sea Class Covert Trial' #TR-Egg
}
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.RegisteredOwner = 'Thomas Rayner - @MrThomasRayner - workingsysadmin.com' #TR-Egg
}
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.TimeZone = $Timezone
}
Get-UnattendChunk 'oobeSystem' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.UserAccounts.AdministratorPassword.Value = $adminPassword
}
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.ProductKey = $WindowsKey
}
Clear-File $filePath
$unattend.Save($filePath)
}
function New-UnattendFile1
{
param
(
[string] $filePath
)
# Reload template - clone is necessary as PowerShell thinks this is a "complex" object
$unattend = $unattendSource.Clone()
# Customize unattend XML
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.RegisteredOrganization = 'Azure Sea Class Covert Trial' #TR-Egg
}
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.RegisteredOwner = 'Thomas Rayner - @MrThomasRayner - workingsysadmin.com' #TR-Egg
}
Get-UnattendChunk 'specialize' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.TimeZone = $Timezone
}
Get-UnattendChunk 'oobeSystem' 'Microsoft-Windows-Shell-Setup' $unattend | ForEach-Object -Process {
$_.UserAccounts.AdministratorPassword.Value = $adminPassword
}
Clear-File $filePath
$unattend.Save($filePath)
}
Function Initialize-BaseImage
{
Mount-DiskImage $ServerISO
$DVDDriveLetter = (Get-DiskImage $ServerISO | Get-Volume).DriveLetter
Copy-Item -Path "$($DVDDriveLetter):\NanoServer\NanoServerImageGenerator\Convert-WindowsImage.ps1" -Destination "$($WorkingDir)\Convert-WindowsImage.ps1" -Force
Import-Module -Name "$($DVDDriveLetter):\NanoServer\NanoServerImagegenerator\NanoServerImageGenerator.psm1" -Force
<#>
if (!(Test-Path "$($BaseVHDPath)\NanoBase.vhdx"))
{
New-NanoServerImage -MediaPath "$($DVDDriveLetter):\" -BasePath $BaseVHDPath -TargetPath "$($BaseVHDPath)\NanoBase.vhdx" -Edition Standard -DeploymentType Guest -Compute -Clustering -AdministratorPassword (ConvertTo-SecureString $adminPassword -AsPlainText -Force)
# New-NanoServerImage -MediaPath "$($DVDDriveLetter):\" -BasePath $BaseVDHPath -TargetPath "$($BaseVHDPath)\NanoBase.vhdx" -GuestDrivers -DeploymentType Guest -Edition Standard -Compute -Clustering -Defender -Storage -AdministratorPassword (ConvertTo-SecureString $adminPassword -AsPlainText -Force)
}
</#>
#Copy-Item -Path '$WorkingDir\Convert-WindowsImage.ps1' -Destination "$($WorkingDir)\Convert-WindowsImage.ps1" -Force
New-UnattendFile "$WorkingDir\unattend.xml"
New-UnattendFile1 "$WorkingDir\unattend1.xml"
#Build the Windows 2016 Core Base VHDx for the Lab
if (!(Test-Path "$($BaseVHDPath)\VMServerBaseCore.vhdx"))
{
Set-Location $workingdir
#Watch the Editions --> 17079 is SERVERDATACENTERACORE and 2016 is SERVERDATACENTERCORE
# Load (aka "dot-source) the Function
. .\Convert-WindowsImage.ps1
# Prepare all the variables in advance (optional)
$ConvertWindowsImageParam = @{
SourcePath = $ServerISO1
RemoteDesktopEnable = $True
Passthru = $True
Edition = "SERVERDATACENTERACORE"
VHDFormat = "VHDX"
SizeBytes = 60GB
WorkingDirectory = $workingdir
VHDPath = "$($BaseVHDPath)\VMServerBaseCore.vhdx"
DiskLayout = 'UEFI'
UnattendPath = "$($workingdir)\unattend1.xml"
}
$VHDx = Convert-WindowsImage @ConvertWindowsImageParam
}
#Build the Windows 2016 Full UI Base VHDx for the Lab
if (!(Test-Path "$($BaseVHDPath)\VMServerBase.vhdx"))
{
Set-Location $workingdir
# Load (aka "dot-source) the Function
. .\Convert-WindowsImage.ps1
# Prepare all the variables in advance (optional)
$ConvertWindowsImageParam = @{
SourcePath = $ServerISO
RemoteDesktopEnable = $True
Passthru = $True
Edition = "ServerDataCenter"
VHDFormat = "VHDX"
SizeBytes = 60GB
WorkingDirectory = $workingdir
VHDPath = "$($BaseVHDPath)\VMServerBase.vhdx"
DiskLayout = 'UEFI'
UnattendPath = "$($workingdir)\unattend.xml"
Package = @(
"$($BaseVHDPath)\windows10.0-kb3213986-x64_a1f5adacc28b56d7728c92e318d6596d9072aec4.msu"
)
}
$VHDx = Convert-WindowsImage @ConvertWindowsImageParam
}
Clear-File "$($BaseVHDPath)\unattend.xml"
Clear-File "$($BaseVHDPath)\unattend1.xml"
Dismount-DiskImage $ServerISO
Dismount-DiskImage $ServerISO1
#Clear-File "$($WorkingDir)\Convert-WindowsImage.ps1"
}
function Download-BaseImageUpdates
{
if (!(Test-Path "$($BaseVHDPath)\windows10.0-kb3213986-x64_a1f5adacc28b56d7728c92e318d6596d9072aec4.msu"))
{
Invoke-WebRequest -Uri http://download.windowsupdate.com/d/msdownload/update/software/secu/2016/12/windows10.0-kb3213986-x64_a1f5adacc28b56d7728c92e318d6596d9072aec4.msu -OutFile "$($BaseVHDPath)\windows10.0-kb3213986-x64_a1f5adacc28b56d7728c92e318d6596d9072aec4.msu" -Verbose
}
}
function Get-ISOUI {
#Ask for ISO
[reflection.assembly]::loadwithpartialname("System.Windows.Forms")
$openFile = New-Object System.Windows.Forms.OpenFileDialog -Property @{
Title="Please select ISO image with Windows Server 2016"
}
$openFile.Filter = "iso files (*.iso)|*.iso|All files (*.*)|*.*"
If($openFile.ShowDialog() -eq "OK")
{
Write-Log "File $($openfile.FileName) selected"
}
if (!$openFile.FileName){
WriteErrorAndExit "Iso was not selected... Exitting"
}
$ServerISO = $openfile.FileName
#$ServerISO
}
function Get-ISOUI1 {
#Ask for ISO
[reflection.assembly]::loadwithpartialname("System.Windows.Forms")
$openFile = New-Object System.Windows.Forms.OpenFileDialog -Property @{
Title="Please select ISO image with Windows Server Insider"
}
$openFile.Filter = "iso files (*.iso)|*.iso|All files (*.*)|*.*"
If($openFile.ShowDialog() -eq "OK")
{
Write-Log "File $($openfile.FileName) selected"
}
if (!$openFile.FileName){
WriteErrorAndExit "Iso was not selected... Exitting"
}
$ServerISO1 = $openfile.FileName
$ServerISO1
}
function Confirm-Path
{
param
(
[string] $path
)
if (!(Test-Path $path))
{
$null = mkdir $path
}
}
function Write-Log
{
param
(
[string]$systemName,
[string]$message
)
Write-Host -Object (Get-Date).ToShortTimeString() -ForegroundColor Cyan -NoNewline
Write-Host -Object ' - [' -ForegroundColor White -NoNewline
Write-Host -Object $systemName -ForegroundColor Yellow -NoNewline
Write-Host -Object "]::$($message)" -ForegroundColor White
}
function Clear-File
{
param
(
[string] $file
)
if (Test-Path $file)
{
$null = Remove-Item $file -Recurse
}
}
function Get-UnattendChunk
{
param
(
[string] $pass,
[string] $component,
[xml] $unattend
)
return $unattend.unattend.settings |
Where-Object -Property pass -EQ -Value $pass `
|
Select-Object -ExpandProperty component `
|
Where-Object -Property name -EQ -Value $component
}
#endregion
#region Variable Init
$BaseVHDPath = "$($WorkingDir)\BaseVHDs"
$WindowsKey = '<PRODUCTKEY>' #Dave's Technet KEY Remove for Publishing of Book
$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="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ComputerName>*</ComputerName>
<ProductKey><PRODUCTKEY></ProductKey>
<RegisteredOrganization>Organization</RegisteredOrganization>
<RegisteredOwner>Owner</RegisteredOwner>
<TimeZone>TZ</TimeZone>
</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">
<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>
</UserAccounts>
</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>en-us</InputLocale>
<SystemLocale>en-us</SystemLocale>
<UILanguage>en-us</UILanguage>
<UILanguageFallback>en-us</UILanguageFallback>
<UserLocale>en-us</UserLocale>
</component>
</settings>
</unattend>
"@
#endregion
Write-Log 'Host' 'Getting started...'
Confirm-Path $BaseVHDPath
Write-Log 'Host' 'Building Base Images'
Write-Log 'Host' 'Downloading January 2018 CU for Windows Server 2016'
Write-Log 'Host' 'Locate the Windows Server 2016 ISO'
. Get-ISOUI
Write-Log 'Host' 'Locate the Windows Server 2016 Insider ISO'
. Get-ISOUI1
Write-Log $ServerISO
Write-Log $ServerISO1
. Download-BaseImageUpdates
. Initialize-BaseImage
Write-Log 'Host' 'Tasks Complete'
The cool part about this script is that we have added a couple of cool new functions. One of them is a UI Prompt that will ask
where your ISO’s are located. This is so much easier that having to type in a path.

Now automatically downloads the January 2018 Cumulative Update for Windows Server 2016 and apply it to the Gold VHDx.

With this you can enjoy your Hyper-V Images with a fully patched and service experience.
As always, we really hope you enjoy this.
Happy Learning,
Dave Kawula – MVP
