Shaving the Yak, leads me to create new module WindowsImageTools

My Yak needs shaving.

If your not familiar with the term.

http://www.hanselman.com/blog/YakShavingDefinedIllGetThatDoneAsSoonAsIShaveThisYak.aspx

It’s been quite a journey since my last post.

Windows 10 came out, and Convert-WindowsImage.ps1 was upgraded (braking my scripts I blogged about this summer) and Server 2016 Preview 3 was released.

Looking at the Nano folder on Preview 3 they are using a WIM and Convert-WindowsImage.ps1to create a VHDX. Good move Microsoft.

Now for the bad part.

Convert-WindowImage.ps1 is buggy, and not a module. But Microsoft is working hart to fix this. Not being one to wait. I decided to take the functionality I need and re-work the whole process as a module. (and found some underlying bugs in PowerShell in the process )  The results of my effort is documented below.

As for Windows 10.

There are a number of changes to DSC that have broken all my production configuration scripts. And Configurations created on Windows10 or WMF 5 preview have bugs when using depends on, that cause the LCM on 2012R2 to hang.

For Production this is a show stopper, but I’m already working to separate out the configuration’s based on target OS.

WindowsImageTools

Microsoft recently moved Convert-WindowsImage over to GitHub and added some nice features, but it’s still a script, not a module. They also nicely added an MIT licence to that repo. So taking advantage of that I started my own project based on that code. I’m calling it WindowsImageTools

So far there are four exported functions.

  1. Initialize-VHDPartition
    1. Create a VHD with correct partition for BIOS or UEFI with or without Recovery tools/image
  2. Set-VHDPartition
    1. take an ISO or WIM and populate the VHD. This detects the layout and acts accordingly.
    2. It also can add drivers, enable features, inject unattend.xml and inject additional files or folders
  3. Convert-Wim2VHD
    1. This is a wrapper functions around the first two
  4. New-UnattendXml
    1. Create an Unattend.xml that works with both 32 and 64 bit in a single file
    2. Sets the admin password and autologin count
    3. Creates then deletes a second user (for Windows7)
    4. Sets TimeZone
    5. Starts a PowerShell script to bootstrap the system configuration

That last one took quite some work to figure out. It only fully works with Volume media because it does not set the license key.

I also discovered that 64bit windows will run both the 32bit sections for adding users and running scripts, but not the part for skipping licensing and autologin. This is true from win7 forward. If your not familiar with Unattend.xml thoes parts are all under the same section in the xml.

If you want to give it a spin it’s available at the PowerShell Gallery https://www.powershellgallery.com/packages/WindowsImageTools/

Onward

So equipped with thease tools i’m now going to reword my auto patching and WIM creation script. and add that into the module.

Building the basics Part 1 | PKI: RootCA

edit:7/26/2015

  • Added xNetworking and removed requirement for DHCP.
  • Please NOTE that this method does not currently work with WMF5 as the Host when targeting a WMF4 WM.

Invoke-DSCBuild -Target PKI | Test-Lab

Before I can get into the really fun stuff with DSC I need the prerequisites for a secure DSC environment. This involves a few things. The minimum is a Certificate Authority, the other is a HTTPS pull server. For my Lab I’m also going to also have a Domain Controller, because most enterprises will have a domain, and because it simplifies permissions.

  1. RootCA
  2. Domain Controller
  3. Enterprise CA
  4. Pull Server

Initial server build and Self Signed Certificate

I’m going to use my patched image as a baseline. Now because xAdcsDeployment and File resources require Credentials, I will have to create a self signed certificate to secure the passwords. I don’t want to use plaintext in a .MOF file.

Now I created a helper function that does the following.

Start-Deploy.ps1

  1. Copy the patch VHDX
  2. Mount to a temporary folder
  3. copy in files
    1. unattnd.xml (same one I used for the Patched Image creation)
    2. FirstRun.ps1 script
    3. AtStartup.ps1 script (SCC-Init.ps1)
    4. any other supporting scripts (New-SelfSignedCertificateEx)
  4. Save VHDX
  5. Start VM
  6. Wait for VM to stop

FirstRun.sp1:

  1. Create the AtStartup job to run AtStartup.ps1
  2. reboot
#requires -Version 2 -Modules ScheduledTasks
Start-Transcript -Path $PSScriptRoot\FirstRun.log
#Create AtStartup Task.
$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 FirstRun.ps1 hosted with ❤ by GitHub

SCC-Init.sp1

  1. Start a transcript to SCC-Init.log
  2. check if pfx was copied in, (and I did not)
    1. if PFX present install that.
  3. if not, use New-SelfSignedCertificateEx (from Script Center)to create a certificate
  4. Export the certificate to a CER file
  5. shutdown the VM

Extracting the Certificate

Once we have a self signed certificate for encrypting passwords we need to get if off the VM and back to the host.

Again I created a helper function for this.

Get-VMCert.ps1

  1. Verify the VM is off
    1. if on, run stop-vm
  2. Mount the VHDX to a temporary folder
  3. copy the file from the relative path inside the VHDX to the supplied path on the host
  4. Dis-Mount the VHDX discarding changes changes.

Creating the MOF

Now due to how the ISE reacts to the having a “Configuration” in a file I separated out the configuration to a .ps1(Intelisence tends to timeout in scripts with a configuration in the ISE in WMF 4)

RootCA_Config.ps1

  1. Get the Certificate thumbprint from the CER file.
  2. Import Credentials from clixml for:
    1. Local Administrator (used to change the built-in Administrator and for setting up a Certificate Authority)
    2. Remote user needed to access the source.wim for adding features
  3. Create a hash table with AllNodes to pass the certificate thumbprint and CER file path with
  4. Configuration Block
    1. Set the IP Address
    2. Set Computer name
    3. Copy the source.wim from a remote share (my win8 workstation on the lab network)
      1. Now this point was something of a pain. In a production environment you don’t want to copy this file over. However due to how DISM (what WindowsFeature and install-WindowsFeatuere are wrappers for) Cant access a network share (even when guest in enabled) on WMF4. (WMF5 adds the ability to run each resource in specified user accounts and may fix this) The LCM runs as the local System account, In a Domain environment you can grant permissions via “Domain Computers” to a share and point the WindowsFeature resorce at the UNC to Source.WIM
    4. Install the Certificate Authority WindowsFeature
    5. Initialize the Certficate Authority useing xAdcsCertificationAuthority
    6. Set the location for the Certificate Revocation list and re-export
      1. This is done via a script resource, as xAdcsCertificationAuthority does not currently have a way to do this
  5. Create the LocalHost.Meta.MOF and LocalHost.MOF files

I had planed to set the IP address, but xNetworking was causing the DSC to hang in my tests.
So I had to turn on DHCP on my Voys router used in the Lab. I hope to track this down and submit a fix.

This issue was resolved and noted below

if (-not ($vmname))
{
$vmname = 'Root-CA'
}
$certpath = "$PSScriptRoot\$vmname.crt"
if (-not(Test-Path $certpath))
{
throw 'Certificate not found'
}
$cert = "$PSScriptRoot\$vmname.crt"
$certPrint = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
$certPrint.Import("$cert")
try
{
$LocalAdmin = Import-Clixml -Path $PSScriptRoot\$vmname.LocalAdminCred.xml
$RemoteUser = Import-Clixml -Path $PSScriptRoot\$vmname.RemoteUserCred.xml
}
catch
{
throw 'error importing passwords from xml'
}
$ConfigData = @{
AllNodes = @(
@{
NodeName = 'localhost'
CertificateFile = "$PSScriptRoot\$vmname.crt"
Thumbprint = "$($certPrint.Thumbprint)"
#PSDscAllowPlainTextPassword = $true;
};
)
}
configuration RootCA
{
Import-DscResource -ModuleName xComputerManagement, xAdcsDeployment, xNetworking,
PSDesiredStateConfiguration
node $AllNodes.NodeName
{
xIPAddress Ethernet
{
InterfaceAlias = 'Ethernet 4'
IPAddress = '10.20.1.51'
AddressFamily = 'IPv4'
SubnetMask = 24
}
User LocalAdmin
{
UserName = 'Administrator'
Disabled = $false
Ensure = 'Present'
Password = $LocalAdmin
PasswordChangeNotAllowed = $false
PasswordNeverExpires = $true
}
xComputer ComputerName
{
Name = 'Root-CA'
WorkGroupName = 'Workgroup'
}
File Source
{
DestinationPath = 'c:\Source.wim'
Credential = $RemoteUser
Ensure = 'Present'
SourcePath = '\\10.20.1.41\DSC\WIM\Source.wim'
Type = 'File'
DependsOn = '[xIPAddress]Ethernet'
}
WindowsFeature ADCS_Cert_Authority
{
Name = 'ADCS-Cert-Authority'
DependsOn = '[File]Source'
Ensure = 'Present'
Source = 'WIM:c:\Source.wim:1'
}
xAdcsCertificationAuthority Root_CA
{
CAType = 'StandaloneRootCA'
Credential = $LocalAdmin
CACommonName = 'Root-CA'
DependsOn = '[WindowsFeature]ADCS_Cert_Authority', '[xComputer]ComputerName'
Ensure = 'Present'
}
Script 'SetRevocationList'
{
GetScript = {
(Get-CACrlDistributionPoint).Uri
}
SetScript = {
$crllist = Get-CACrlDistributionPoint; foreach ($crl in $crllist)
{
Remove-CACrlDistributionPoint $crl.uri -Force
}
Add-CACRLDistributionPoint -Uri C:\Windows\System32\CertSrv\CertEnroll\%3%8.crl -PublishToServer -Force
Add-CACRLDistributionPoint -Uri http://pki.contoso.com/pki/%3%8.crl -AddToCertificateCDP -Force
$aialist = Get-CAAuthorityInformationAccess; foreach ($aia in $aialist)
{
Remove-CAAuthorityInformationAccess $aia.uri -Force
}
certutil.exe -setreg CA\CRLOverlapPeriodUnits 12
certutil.exe -setreg CA\CRLOverlapPeriod 'Hours'
certutil.exe -setreg CA\ValidityPeriodUnits 10
certutil.exe -setreg CA\ValidityPeriod 'Years'
certutil.exe -setreg CA\AuditFilter 127
Restart-Service -Name certsvc
certutil.exe -crl
}
TestScript = {
if ((Get-CACrlDistributionPoint).Uri -contains 'http://pki.contoso.com/pki/<CAName><CRLNameSuffix&gt;.crl')
{
return $true
}
else
{
return $false
}
}
DependsOn = '[xAdcsCertificationAuthority]Root_CA'
}
LocalConfigurationManager
{
CertificateId = $node.Thumbprint
ConfigurationMode = 'ApplyandAutoCorrect'
RebootNodeIfNeeded = $true
}
}
}
RootCA -ConfigurationData $ConfigData -OutputPath "$PSScriptRoot\RootCA"
view raw RootCA_Config.ps1 hosted with ❤ by GitHub

Starting DSC

Armed with the required .MOF files I need to get them copied into the VM and and start DSC. Once again a helper function, (pardon my poor naming conventions)

Set-VmLcm.ps1

  1. Stop the VM if it’s running
  2. Mount the VHDX to a temp folder
  3. Copy in a new AtStartup.ps1 (DSC-Init.ps1)
  4. If LocalHostMofFolder set
    1. Copy in LocalHost.mof and LocalHost.Meta.Mof
  5. Else copy
    1. CofnigMof as Pending.mof
  6. Create metaconfig.mof from supplied string
  7. Dismount VHDX saving changes

I initially was going to go with copy in Pending and set Metaconfig.mof directly then use  Invoke-CimMethod, as shown by the PowerShell team blog However after fighting with a failing config I went to Start-DscConfiguration. This did not fix my issue with xNetworking but I have had better luck with this method.

This issue actually had to do having WMF5 on the host. Removing WMF5 preview and returning to WMF4 and my problems were resolved. so I have added xNetworking back in.

However I’m staying with the current use of start-DSCConfiguration rather than a WMI call. The debugging is simpler with -verbose.

DSC-Init.ps1

  1. Starts a transcript to DSC-Init.log
  2. Unregister the AtStartup scheduled task
  3. Remove c:\Unattend.xml
    1. The plain text password contained inside will be changed by DSC but not point in leaving a mess.
  4. Enable the Analytic and Debug logs for DSC
  5. Run Set-DSCLocalConfigurationManager
  6. Run Start-DscConfiguration with -Wait -Verbose
  7. Stop-Transcript

Enabling the log channels and using -wait -verbose with a transcript helps a lot with debugging

Putting it all together

I put all the functions into a quickly made module to simplify importing them.

#requires -Version 1
. $PSScriptRoot\Get-VmCert.ps1
. $PSScriptRoot\Set-VmLcm.ps1
. $PSScriptRoot\Start-Deploy.ps1
view raw Functions.psm1 hosted with ❤ by GitHub

I created a wrapper script that will put all this together and build my Root-CA

Build-RootCA.ps1

  1. Set the a number of variables
    1. VM-Name
    2. Path to DSC Config Sript
    3. Start-Deploy parameters for splating
    4. Get-VmCert parameters for splating
    5. Set-VmMof Parameters fro splatting
    6. Create the clixml files containing the Passwords
  2. Import-Module on the sub folder holding all my functions
  3. Start-Deploy
  4. Get-VMCert
  5. Dot Source the DSC Config Script
  6. Set-VmLcm
  7. Start the VM
  8. Remove-Module -Name Functions

The reason for saving the passwords to XML avoids the need to enter credentials on subsequent runs of the script. I want to be able to tare down and rebuild the lab from scratch at any time.

One other file you will need is the unattend.xml

The folder structure for all of this is

G:\BuildLab
│   Build-DC.ps1
│   Build-RootCA.ps1
│   Root-CA.LocalAdminCred.xml
│   Root-CA.RemoteUserCred.xml
│   RootCA_Config.ps1
│   Unattend.xml
│
├───Functions
│       Functions.psd1
│       Functions.psm1
│       Get-VmCert.ps1
│       Set-VmLcm.ps1
│       Start-Deploy.ps1
│       Unattend.xml
│
├───Helpers
│       New-SelfSignedCertificateEx.ps1
│
├───Resorces_RootCA
│   ├───xAdcsDeployment
│   ├───xComputerManagement
│   ├───xDscDiagnostics
│   └───xNetworking
│
├───RootCA
│       localhost.meta.mof
│       localhost.mof
│
└───VMScripts
        DSC-Init.ps1
        FirstRun.ps1
        SSC-Init.ps1

Wrapping up

This script took a lot longer then planed and I got to use xDscDiagnostics a lot to track errors. That is outside of the scope of this post but I plan to write about it at some time. I did have to enable DHCP on the router, and that is something I hope to address and update this post. I want to use a DSC configured DHCP. but in this case I have a clasic chicken and egg issue. I need the CA, DC and Pull server first.

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.)

Creating a small footprint, base image Part 3 | SysPrep and Compacting Images

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

SysPrep and Compacting Images

Sysprep the Core Image

Now I need to sysprep the core image before it’s usable. but I don’t want to repeat this whole process from scratch next month. So I’m going to copy the VHDX and create a new VM that I will sysprep.  So back to the host

copy .\CorePatched.vhdx CoreSysprep.vhdx
new-vm -Name CoreSysprep -MemoryStartupBytes 512MB -BootDevice VHD -VHDPath G:\temp\CoreSysprep.vhdx -Generation 2 -SwitchName TestLab
Name        State CPUUsage(%) MemoryAssigned(M) Uptime   Status
 ----        ----- ----------- ----------------- ------   ------
 CoreSysprep Off   0           0                 00:00:00 Operating normally
start-vm coreSysprep
vmconnect localhost coresysprep

Now on the new VM run

c:\windows\System32\Sysprep\sysprep.exe /quiet /oobe /generalize /shutdown

Once the VM is shutdown it’s time to look at our progress on the host.

Compacting the Images

dir core*
    Directory: G:\temp
Mode                LastWriteTime         Length Name
 ----                -------------         ------ ----
 -a----        5/14/2015   9:20 PM     5641338880 CoreFromIso.vhdx
 -a----        5/15/2015   4:10 PM     9265217536 CorePatched.vhdx
 -a----        5/15/2015   4:27 PM     9265217536 CoreSysprep.vhdx

Wow, almost 10 gigs. not exactly small. Well we are not done. We need to convert both our images to WIM and the core one back to VHDX. The transfer from VHDX to WMI and back to VHDX does a better job then using a utility to write all 0’s to the disk and then compact it. This is partly due to the swap file. Back on the host we need to create a folder to mount the VHDX into and then create the WIM.

mkdir g:\mount
    Directory: G:\
Mode                LastWriteTime         Length Name
 ----                -------------         ------ ----
 d-----        5/15/2015   8:54 PM                mount
Mount-WindowsImage -ImagePath G:\temp\CoreSysprep.vhdx -Path G:\Mount -Index 1
Path           : G:\Mount
 Online         : False
 Restart Needed : False
New-WindowsImage -CapturePath G:\Mount -Name 2012r2_Core -ImagePath G:\Temp\2012r2Core.wim -Description "2012r2 Core Patched May 2015" -Verify
LogPath : C:\Windows\Logs\DISM\dism.log
Dismount-WindowsImage -Path G:\mount -Discard
LogPath : C:\Windows\Logs\DISM\dism.log
Mount-WindowsImage -ImagePath G:\temp\GUIPatched.vhdx -Path G:\Mount -Index 1
Path           : G:\Mount
 Online         : False
 Restart Needed : False
New-WindowsImage -CapturePath G:\Mount -Name 2012r2_Source -ImagePath G:\Temp\2012r2Source.wim -Description "2012r2 Source Patched May 2015" -Verify
LogPath : C:\Windows\Logs\DISM\dism.log
Dismount-WindowsImage -Path G:\mount -Discard
LogPath : C:\Windows\Logs\DISM\dism.log

Converting the Core WIM back to VHDX is going to require that we make a change to Convert-WindowsImage.ps1. The logic of the script looks at the WIM and if there is only one image it uses that one. But it does so using metadata that does not exist when a WIM is created using New-WindowsImage. So down on line 4020 we nee to comment out a few lines

        # If there's only one image in the WIM, just selected that.
        #if ($openWim.Images.Count -eq 1) { 
        #    $Edition   = $openWim.Images[0].ImageFlags
        #    $openImage = $openWim[$Edition]
        #} else {

            if ([String]::IsNullOrEmpty($Edition)) {
                Write-W2VError "You must specify an Edition or SKU index, since the WIM has more than one image."
                Write-W2VError "Valid edition names are:"
                $openWim.Images | %{ Write-W2VError "  $($_.ImageFlags)" }
                throw
            } 

            if ([Int32]::TryParse($Edition, [ref]$null)) {
                $openImage = $openWim[[Int32]$Edition]    
            } else {
                $openImage = $openWim[$Edition]
            }        
        #}

Now it will work using -edition 1 (There is only one OS inside the WIM)

G:\temp\Convert-WindowsImage.ps1 -SourcePath G:\temp\2012r2Core.wim -VHDPath G:\temp\2012R2Core.vhdx -SizeBytes 40gb -VHDType Dynamic -VHDFormat VHDX -VHDPartitionStyle GPT -Edition 1 -Verbose
Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 8
 Copyright (C) Microsoft Corporation.  All rights reserved.
 Version 6.3.9600.7.amd64fre.fbl_core1_hyp_dev(mikekol).140217-3000 Release to Web
 VERBOSE: isUserAdmin? True
 VERBOSE: isWindows8? True
 VERBOSE: Temporary VHDX path is : G:\temp\faf6d4fa-fb64-41d7-95f3-5a1ab3825940.vhdx
 INFO   : Image 1 selected ()...
 INFO   : Creating sparse disk...
 INFO   : Attaching VHDX...
 INFO   : Disk initialized with GPT...
 INFO   : Disk partitioned with two Volumes...
 INFO   : System Volume formatted (with DiskPart)...
 INFO   : Boot Volume formatted (with Format-Volume)...
 INFO   : Access path (L:\) has been assigned to the System Volume...
 INFO   : Access path (M:\) has been assigned to the Boot Volume...
 INFO   : Applying image to VHDX. This could take a while...
 INFO   : Signing disk...
 INFO   : Image applied. Making image bootable...
 VERBOSE: Running bcdboot.exe M:\Windows /s L:\ /v /f UEFI
 VERBOSE: Return code was 0.
 INFO   : Drive is bootable. Cleaning up...
 INFO   : Closing VHDX...
 INFO   : Closing Windows image...
 INFO   : Done.
dir *core* | sort -Property LastWriteTime
    Directory: G:\temp
 Mode                LastWriteTime         Length Name
 ----                -------------         ------ ----
 -a----        5/14/2015   9:20 PM     5641338880 CoreFromIso.vhdx
 -a----        5/15/2015   4:10 PM     9265217536 CorePatched.vhdx
 -a----        5/15/2015   8:00 PM     9265217536 CoreSysprep.vhdx
 -a----        5/15/2015   9:39 PM     2376356767 2012r2Core.wim
 -a----        5/15/2015  10:15 PM     4936695808 2012R2Core.vhdx
dir *gui*, *source* | sort -Property LastWriteTime
    Directory: G:\temp
 Mode                LastWriteTime         Length Name
 ----                -------------         ------ ----
 -a----        5/14/2015   9:29 PM     9130999808 GUIFromIso.vhdx
 -a----        5/15/2015   3:56 PM    16143876096 GuiPatched.vhdx
 -a----        5/15/2015  10:03 PM     6153716725 2012r2Source.wim

As you can see our Source.WIM is 6.1Gig and our Core VHDX is only 4.9gig. fully patched it’s smaller then the baseline Core image. We can place the WIM on a share that is accessible to new VM’s and they can use that for adding features. In another blog I will go over how to update the WIM file automatically each month.

Creating a small footprint, base image Part 2 | Patching and Cleanup via PowerShell

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

Patching and Cleanup via PowerShell

Patching The Core Image

Starting with our Core image we are going to Patch and clean up extra files. At the command prompt I’m going to type: start Powershell Now I could use sconfig to patch, but we are doing this via PowerShell. Using James Oniel’s blog as a reference. I have cut the code down to just the needed commands because the paste keystrokes has a limited size. And sometimes opening a fresh VM does not give me the options for an Advanced Session. I’m going to take the code below and “Paste from clipboard” into PowerShell to download the all patches, including optional patches

$Criteria = "IsInstalled=0 and Type='Software'"
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updates = $updateSession.CreateupdateSearcher().Search($Criteria).Updates
$downloader = $updateSession.CreateUpdateDownloader()   
$downloader.Updates = $updates  
"Downloading $($downloader.Updates.count) updates"
$Result = $downloader.Download() 
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


    "Installing $($installer.Updates.count) updates"
    $installationResult = $installer.Install()
    $Global:counter = -1
    $installer.updates | Format-Table -AutoSize -Property Title, EulaAccepted, @{
        label      = 'Result'
        expression = {
            $resultcode[$installationResult.GetUpdateResult($Global:counter++).resultCode ]
        }
    } 
}

$installationResult

This takes a while. Once it’s done I’m going to reboot the VM and paste the script again until there are 0 updates. It can take 2-3 times depending on the age of your ISO.

Restart-Computer

Once we have updated Core with every patch, it’s time to remove unneeded file. If you going install any features, special software or setup boot time scripts. now is the time to do so. I’m not going to this time. I will start by removing the source file for adding windows features. We will be creating a WIM from our GUI image for that purpose.

Get-WindowsFeature | where-object{$_.Installed -eq 0 -and $_.InstallState -eq 'Available'} | uninstall-windowsfeature -remove
dism /online /cleanup-image /StartComponentCleanup /ResetBase
defrag c: /UVX
  • First we removes the source from SxS
  • Second we remove any “superseded” items in SxS. So any older patches that have been updated with a newer one will be removed.
  • Third, we defrag the drive and consolidate free space (this probably is not necessary)

Preparing and Patching the GUI Image

With that done lets move to the GUI VM. I’m going to first make sure every feature is available

get-windowsfeature | Where InstallState -eq Removed
Display Name                                            Name                       Install State
 ------------                                            ----                       -------------
     [ ] .NET Framework 3.5 (includes .NET 2.0 and 3.0)  NET-Framework-Core               Removed
     [ ] Windows PowerShell 2.0 Engine                   PowerShell-V2                    Removed

So .NET 3.5 and PowerShell 2.0 are removed. If your never going to use them that is ok. I find that .NET 3.5 is frequently needed so to be safe I’m going to add them both to SxS.

get-windowsfeature | Where InstallState -eq Removed | Install-WindowsFeature -Source D:\sources\sxs
Success Restart Needed Exit Code      Feature Result
 ------- -------------- ---------      --------------
 True    No             Success        {.NET Framework 3.5 (includes .NET 2.0 and...
 WARNING: Windows automatic updating is not enabled. To ensure that your newly-installed role or feature is
 automatically updated, turn on Windows Update.

Because we want the WIM to be a source when adding to VM’s created from our Core Image, and we want the feature to be patched from the get-go. They need to be installed and patched on this VM. If they are not all installed, that is OK, it will still work as a source so long as the installed features in the WIM are the same version or newer then the VM your adding the feature to.So if we don’t mind patching the new items after the fact when we can just remove the GUI, patch and move on. I’m going to keep the GUI, and add some features I know I will be using in my lab. (now before you go and think I’ll just run Get-WindowsFeature | Add-WindowsFeature, that does not work because some features are mutually exclusive)

Install-WindowsFeature DNS, DHCP, File-Services, RSAT -IncludeAllSubFeature
Install-WindowsFeature AD-Domain-Services, AD-Certificate, ADCS-Cert-Authority

There will be a few warnings but everything should install Now I’m going to use the same code used on the Core VM to run updates. Once that is patched and shutdown it’s ready for Part 3.

Creating a small footprint, base image Part 1 | VHDX from ISO

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

VHDX from ISO

First you need a 20112r2 ISO, at work you would get this from the Volume License Service center. Every time Microsoft releases a roll-up, they usually update the ISO file so it’s a good idea check every few months for an updated ISO. Now for a home lab, unless you have a personal MSDN, your going to need an ISO from another source.  I’m just going with the latest trial from the TechNet Evaluation Center. Don’t download the VHD. We are going to need both Core and GUI from the ISO. While we could get there from the VHD, It’s not the path I’m taking.

I’m saving the ISO in c:\ISO\ and renaming it Server2012R2.ISO

Now what I’m going to do is create two VHDX one core and one with a GUI. I could use the ISO to manually install but I want to limit my use of the mouse, (and screen shots are a pain to insert in the blog)

Once I have that downloaded I can mount it on My windows 8.1 hyper-v host that contains the Test Lab. The reason for this is I will need the VHDX on the host. The WMI file can be on any share accessible by the VM’s.

PS C:\> Mount-DiskImage -ImagePath c:\ISO\Server2012R2.ISO

This will mount it as it’s own drive. in my case I:

Next I’m going to need a way to extract the contents of Install.wim and create a properly formatted VHDX. Now many of the examples of this online are for Gen1 machines. I want Gen2. so I’m going to get a copy of Convert-WindowsImage.ps1
Unfortunately this is not a module but and advanced script.  I’m not going to go in-depth on how Convert-WindowsImage works. Check it’s help and open it up if your curious.

Unblock-File G:\temp\Convert-WindowsImage.ps1
G:\temp\Convert-WindowsImage.ps1 -SourcePath I:\sources\install.wim -VHDPath G:\temp\CoreFromIso.vhdx -SizeBytes 40gb -VHDType Dynamic -VHDFormat VHDX -VHDPartitionStyle GPT -Edition 3 -Verbose
Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 8
 Copyright (C) Microsoft Corporation.  All rights reserved.
 Version 6.3.9600.7.amd64fre.fbl_core1_hyp_dev(mikekol).140217-3000 Release to Web
 VERBOSE: isUserAdmin? True
 VERBOSE: isWindows8? True
 VERBOSE: Temporary VHDX path is : c:\temp\a585fb17-2bfe-4db6-8440-034c561e53ce.vhdx
 INFO   : Image 3 selected (ServerDataCenterEvalCore)...
 INFO   : Creating sparse disk...
 INFO   : Attaching VHDX...
 INFO   : Disk initialized with GPT...
 INFO   : Disk partitioned with two Volumes...
 INFO   : System Volume formatted (with DiskPart)...
 INFO   : Boot Volume formatted (with Format-Volume)...
 INFO   : Access path (J:\) has been assigned to the System Volume...
 INFO   : Access path (K:\) has been assigned to the Boot Volume...
 INFO   : Applying image to VHDX. This could take a while...
 INFO   : Signing disk...
 INFO   : Image applied. Making image bootable...
 VERBOSE: Running bcdboot.exe K:\Windows /s J:\ /v /f UEFI
 VERBOSE: Return code was 0.
 INFO   : Drive is bootable. Cleaning up...
 INFO   : Closing VHDX...
 INFO   : Closing Windows image...
 INFO   : Done.
G:\temp\Convert-WindowsImage.ps1 -SourcePath G:\sources\install.wim -VHDPath G:\temp\GUIFromIso.vhdx -SizeBytes 40gb -VHDType Dynamic -VHDFormat VHDX -VHDPartitionStyle GPT -Edition 4 -Verbose
Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 8
 Copyright (C) Microsoft Corporation.  All rights reserved.
 Version 6.3.9600.7.amd64fre.fbl_core1_hyp_dev(mikekol).140217-3000 Release to Web
 VERBOSE: isUserAdmin? True
 VERBOSE: isWindows8? True
 VERBOSE: Temporary VHDX path is : G:\temp\dd0ac2d0-b05c-461f-b071-d73b59522bb4.vhdx
 INFO   : Image 4 selected (ServerDataCenterEval)...
 INFO   : Creating sparse disk...
 INFO   : Attaching VHDX...
 INFO   : Disk initialized with GPT...
 INFO   : Disk partitioned with two Volumes...
 INFO   : System Volume formatted (with DiskPart)...
 INFO   : Boot Volume formatted (with Format-Volume)...
 INFO   : Access path (K:\) has been assigned to the System Volume...
 INFO   : Access path (L:\) has been assigned to the Boot Volume...
 INFO   : Applying image to VHDX. This could take a while...
 INFO   : Signing disk...
 INFO   : Image applied. Making image bootable...
 VERBOSE: Running bcdboot.exe L:\Windows /s K:\ /v /f UEFI
 VERBOSE: Return code was 0.
 INFO   : Drive is bootable. Cleaning up...
 INFO   : Closing VHDX...
 INFO   : Closing Windows image...
 INFO   : Done.
disMount-DiskImage -ImagePath c:\ISO\Server2012R2.ISO
dir
Directory: C:\temp
Mode                LastWriteTime     Length Name
 ----                -------------     ------ ----
 -a---         5/14/2015   3:16 PM     187332 Convert-WindowsImage.ps1
 -a---         5/14/2015   3:29 PM 5104467968 CoreFromIso.vhdx
 -a---         5/14/2015   3:42 PM 8157921280 GUIFromIso.vhdx

 We now have two VHDX files however they are still a little out of date, and we can still trim them up a bit.

I’m going to copy the VHDX files, just so we can see how the size changes on each step.

copy .\CoreFromIso.vhdx CorePatched.vhdx

copy .\GUIFromIso.vhdx GuiPatched.vhdx

Now we will create VM’s and attach the two drives.  I will also add the ISO to the GUI version as we need it for making sure all windows features are ‘Available”

new-vm -Name CorePatch -MemoryStartupBytes 1024MB -BootDevice VHD -VHDPath G:\temp\CorePatched.vhdx -Generation 2 -SwitchName TestLab
Name      State CPUUsage(%) MemoryAssigned(M) Uptime   Status
 ----      ----- ----------- ----------------- ------   ------
 CorePatch Off   0           0                 00:00:00 Operating normally
new-vm -Name GuiPatch -MemoryStartupBytes 1024MB -BootDevice VHD -VHDPath G:\temp\GuiPatched.vhdx -Generation 2 -SwitchName TestLab
Name     State CPUUsage(%) MemoryAssigned(M) Uptime   Status
 ----     ----- ----------- ----------------- ------   ------
 GuiPatch Off   0           0                 00:00:00 Operating normally
Add-VMDvdDrive -Path C:\ISO\Server2012R2.ISO -VMName guipatch

Start-VM *patch

vmconnect localhost corepatch
vmconnect localhost guipatch

Those last two command connect me to the servers.

Now I’m going to go on the assumption that you have experience setting up server and you can handle the Out of Box experience on your own.

In Part 2 we will cover Windows Updates with PowerShell, along with cleaning up unneeded files.

Test-Lab | Update-GitHub

Posts in this series

  1. Test-HomeLab -InputObject ‘The Plan’
  2. Get-Posh-Git | Test-Lab
  3. Get-DSCFramework | Test-Lab
  4. Invoke-DscBuild | Test-Lab
  5. Test-Lab | Update-GitHub

Last time we were able to get the sample DSC config to build. The problem is we had to modify the sample script and the instructions were not clear on the setup of the files.

So to day I plan to update the SampleBuild.ps1, SampleConfiguration.psm1 and readme.md with my changes and submit a pull request.

The first thing I’m going to modify is SampleConfiguration.psm1, now this file worked as is with the previous blog. but some of the things we did were to get arround an issue with this file.

The line i’m talking about is here

 Import-Module DscConfiguration -ErrorAction Stop

This imports a module that is already loaded by SampleBuild.ps1 but is also not in the path at this point of Invoke-DscBuild, unless the DscConfiguration module is added to a path in -ModulePath used by Invoke-DscBuild.  Now if your going to run import-module SampleConfiguration crate the ConfigurationData hash table your self and call SampleConfiguration then it makes sense to have that here. I don’t imagine anyone doing that but stranger things have happen, so I’m going to keep that line but wrap it in something to avoid the requirement of placing extra modules in DSC_Tools

if (-not (Get-Module DscConfiguration)) {
 Import-Module DscConfiguration -ErrorAction Stop
}

This checks to see if DscConfiguration is already loaded into memory and skips the import is it is.

Now I don’t need any modules in DSC_Tooling. Unless I have modules used by my scripts that I dont import prior to calling Invoke-DscBuild

Next up is SampleBuild.ps1, I dont have any changes to make from what I did last time. so i will add that to my git repo in just a bit.

Now Readme.md in the examples folder needs some serious work.

Example DSC Build
------

This folder contains some very basic examples of what a DSC configurationData folder structure, script, and call to Invoke-DscBuild might look like.  If you want to execute SampleBuild.ps1, there are a few dependencies you need to set up ahead of time:

- You must install all of the DSC tooling modules (content of \Tools minus the example folder) from this repository into your PSModulePath (typically into C:\Program Files\WindowsPowerShell\Modules\)
- You must also copy the Tooling\Examples\SampleConfiguration folder to the PSModulePath.
- You must copy [Pester](https://github.com/pester/Pester) (version 3.0.0 or later) and [ProtectedData](https://github.com/dlwyatt/ProtectedData) (version 2.1 or later) into the PSModulePath.
- You should create a DSC_Resources folder in the same directory as SampleBuild.ps1 and DSC_Configuration.  Copy the following modules into that DSC_Resources folder:
  - [StackExchangeResources](https://github.com/PowerShellOrg/StackExchangeResources)
  - [cWebAdministration](https://github.com/PowerShellOrg/cWebAdministration)
  - [cSmbShare](https://github.com/PowerShellOrg/cSmbShare)

Create a folder to place all the files into. i.e. c:\DSC, inside that folder create folders named BuildOutput, DSC_Configuration, DSC_Resorces, DSC_Script, DSC_Tooling. 

the folder structure should look like this
C:\DSC                # copy SampleBuild.ps1 here
+---BuldOutput        # Where the MOF files and ziped modules end up
+---DSC_Configuration # Copy \Tooling\Examples\DSC_Configuration\*  here
+---DSC_Resources     # copy StackExchangeResources, cSmbShare and cWebAdministration here
+---DSC_Script        # copy \Tooling\Examples\SampleConfiguration here
+---DSC_Tooling       # This is for any modules that may be used in a Configuration script, in the case of SampleConfiguration it would be empty.

If you plan on modifying SampleConfiguration.psm1 inside of DSC_Script you will also want to add the content of DSC_Modules to C:\Program Files\WindowsPowerShell\Modules\ but that is not necessary if your just building configurations that are authored on another machine. 

Once these dependencies are set up, you can execute SampleBuild.ps1.  It will run tests against the 3 modules in your DSC_Resources folder, compile your configuration into MOF documents, produce zip files for the resource modules, generate checksums for everything and copy them into BuildOutput

_Note:  The SampleBuild.ps1 file currently just dumps DSC_Tooling modules into the temporary folder, since I wasn't using that feature.  We'll build on these examples soon to show off some of the other functionality in the DscBuild and DscConfiguration modules, such as encrypting credentials in source control._

If you compare with my last post you will notice I removed the line about placing SampleConfguration into C:\Program Files\WindowsPowerShell\Modules\. this path is no longer in the psmodulepath when inovke-DscBuild runs, as it used to be. This is due to needing to keep psmodulepath clean so that DSC_Resorces are accessible for configuration building but not necessarily needed.

I also added a section showing the required folder structure and what each folder is used for, along with what needs to be done if your going to author on the same machine you build on.

Now that that is done. I’m going to copy the files into C:\GitHub\PshOrgDSC\Tooling\Examples one at a time and add and commit changes.

copy SampleConfiguration.psm1 first. Notice the prompt change thanks to posh-git, (you will not see the color change)

C:\GitHub\PshOrgDSC\Tooling\Examples\SampleConfiguration [development +0 ~1 -0]> git add *

C:\GitHub\PshOrgDSC\Tooling\Examples\SampleConfiguration [development +0 ~1 -0]> git commit -m 'Fixed issue with loading scamplescript moduel not loading if module is already loaded but no longer in path'
[development a5301a8] Fixed issue with loading scamplescript moduel not loading if module is already loaded but no longer in path
 1 file changed, 3 insertions(+), 1 deletion(-)

C:\GitHub\PshOrgDSC\Tooling\Examples\SampleConfiguration [development]> 

next up SampleBuild.ps1

C:\GitHub\PshOrgDSC\Tooling\Examples [development +0 ~1 -0]> git add *

C:\GitHub\PshOrgDSC\Tooling\Examples [development +0 ~1 -0]> git commit -m 'updated SampleBuild.ps1 to include missing value to invoke-DSCBuild. and added -verbose'
[development 5c35462] updated SampleBuild.ps1 to include missing value to invoke-DSCBuild. and added -verbose
 1 file changed, 4 insertions(+), 3 deletions(-)

C:\GitHub\PshOrgDSC\Tooling\Examples [development]>

last up readme.md

C:\GitHub\PshOrgDSC\Tooling\Examples [development +0 ~1 -0]> git add *

C:\GitHub\PshOrgDSC\Tooling\Examples [development +0 ~1 -0]> git commit -m 'updated \Tooling\Example\Readme.md to matche the current state of development, and included seciton showing required folder structure'
[development 3aa570d] updated \Tooling\Example\Readme.md to matche the current state of development, and included seciton showing required folder structure
 1 file changed, 14 insertions(+), 2 deletions(-)

C:\GitHub\PshOrgDSC\Tooling\Examples [development]>

finaly I push the commits to my fork

C:\GitHub\PshOrgDSC\Tooling\Examples [development]> git push
git : To https://github.com/BladeFireLight/DSC.git

Now I can subit a pull request.

I head over to github https://github.com/BladeFireLight/DSC/tree/development

compaireandpull

Clicking Compare and pull request gives me a diff of each file and it’s changes.

exampleDiff

It also tells me there are no issues merging my commits into the upstream Development branch.

ExamplePull

I’m submitting the pull request and will update this post with the results.

Invoke-DscBuild | Test-Lab

Posts in this series

  1. Test-HomeLab -InputObject ‘The Plan’
  2. Get-Posh-Git | Test-Lab
  3. Get-DSCFramework | Test-Lab
  4. Invoke-DscBuild | Test-Lab
  5. Test-Lab | Update-GitHub

In the last post we forked a copy of the Powershell.org DSC tools and cloned a copy locally

Today I’m going to get the example configuration working.

Now looking at the README.md under examples seems straightforward, but I know for a fact that it’s missing a few steps.

This folder contains some very basic examples of what a DSC configurationData folder structure, script, and call to Invoke-DscBuild might look like.  If you want to execute SampleBuild.ps1, there are a few dependencies you need to set up ahead of time:

– You must install all of the DSC tooling modules from this repository into your PSModulePath (typically into C:\Program Files\WindowsPowerShell\Modules\)
– You must also copy the Tooling\Examples\SampleConfiguration folder to the PSModulePath.
– You must copy [Pester](https://github.com/pester/Pester) (version 3.0.0 or later) and [ProtectedData](https://github.com/dlwyatt/ProtectedData) (version 2.1 or later) into the PSModulePath.
– You should create a DSC_Resources folder in the same directory as SampleBuild.ps1 and DSC_Configuration.  Copy the following modules into that DSC_Resources folder:
– [StackExchangeResources](https://github.com/PowerShellOrg/StackExchangeResources)
– [cWebAdministration](https://github.com/PowerShellOrg/cWebAdministration)
– [cSmbShare](https://github.com/PowerShellOrg/cSmbShare)

Once these dependencies are set up, you can execute SampleBuild.ps1.  It will run tests against the 3 modules in your DSC_Resources folder, compile your configuration into MOF documents, produce zip files for the resource modules, generate checksums for everything and copy them into C:\Program Files\WindowsPowerShell\DscService\

Looks like we are going to need a few more DSC resources. I’m going to go and fork/clone them just like before. (don’t forget to update the URL’s to match your own fork. )

# Download required files via Git
#Next line not needed if you were following along with my last blog
git clone https://github.com/BladeFireLight/DSC.git c:\GitHub\PshOrgDSC --branch development
git clone https://github.com/BladeFireLight/StackExchangeResources.git c:\GitHub\StackExchangeResources
git clone https://github.com/BladeFireLight/cWebAdministration.git c:\GitHub\cWebAdministration
git clone https://github.com/BladeFireLight/cSmbShare.git c:\GitHub\cSmbShare 
git clone https://github.com/BladeFireLight/Pester c:\GitHub\Pester
git clone https://github.com/BladeFireLight/ProtectedData.git c:\GitHub\ProtectedData

Now I’m going to create some folder structure and place all the files where they need to go

#Create Folders
mkdir c:\DSC
mkdir C:\DSC\BuldOutput
mkdir C:\DSC\DSC_Configuration
mkdir C:\DSC\DSC_Resources
mkdir C:\DSC\DSC_Script
mkdir C:\DSC\DSC_Tooling

#Copy Files
copy C:\github\PshOrgDSC\Tooling\* C:\DSC\DSC_Tooling\ -Exclude 'examples', 'readme.md' -Recurse
copy C:\github\PshOrgDSC\Tooling\* 'C:\Program Files\WindowsPowerShell\Modules' -Exclude 'examples', 'readme.md' -Recurse
copy C:\github\* 'C:\Program Files\WindowsPowerShell\Modules' -Include 'Pester','ProtectedData' -Recurse 
copy C:\github\* C:\DSC\DSC_Tooling\ -Include 'Pester','ProtectedData' -Recurse 
copy C:\github\PshOrgDSC\Tooling\Examples\SampleBuild.ps1 c:\dsc\SampleBuild.ps1
copy c:\github\* C:\DSC\DSC_Resources -Include 'cSmbShare', 'cWebAdministration', 'StackExchangeResources' -Recurse
copy c:\github\* 'C:\Program Files\WindowsPowerShell\Modules' -Include 'cSmbShare', 'cWebAdministration', 'StackExchangeResources' -Recurse
copy C:\github\PshOrgDSC\Tooling\Examples\DSC_Configuration\* C:\DSC\DSC_Configuration -Recurse
copy C:\github\PshOrgDSC\Tooling\Examples\* C:\DSC\DSC_Script -Include 'SampleConfiguration' -Recurse

#delete unneeded folders. 
dir -Path c:\dsc -Include '.git' -Recurse | del -Recurse -Force #-Confirm:$false
dir -Path 'C:\Program Files\WindowsPowerShell\Modules' -Include '.git' -Recurse | del -Recurse -Force

Then result should give us a folder structure like this

C:\DSC
├───BuldOutput
├───DSC_Configuration
│   ├───AllNodes
│   ├───Services
│   └───SiteData
├───DSC_Resources
│   ├───cSmbShare
│   │   └───DscResources
│   │       └───PSHOrg_cSmbShare
│   ├───cWebAdministration
│   │   ├───DSCResources
│   │   │   ├───PSHOrg_cAppPool
│   │   │   └───PSHOrg_cWebsite
│   │   └───Examples
│   └───StackExchangeResources
│       ├───DSCResources
│       │   ├───StackExchange_CertificateStore
│       │   ├───StackExchange_FirewallRule
│       │   ├───StackExchange_NetworkAdapter
│       │   ├───StackExchange_Pagefile
│       │   │   ├───StackExchange_en-US
│       │   │   └───StackExchange_nl-NL
│       │   ├───StackExchange_PowerPlan
│       │   │   └───StackExchange_en-US
│       │   ├───StackExchange_ScheduledTask
│       │   ├───StackExchange_SetExecutionPolicy
│       │   │   └───StackExchange_en-US
│       │   └───StackExchange_Timezone
│       └───test
│           ├───integration
│           │   └───StackExchange_PageFile
│           │       └───pester
│           └───unit
│               └───StackExchange_Pagefile
│                   └───pester
├───DSC_Script
│   └───SampleConfiguration
└───DSC_Tooling
    ├───cDscDiagnostics
    ├───cDscResourceDesigner
    ├───dscbuild
    ├───DscConfiguration
    ├───DscDevelopment
    ├───DscOperations
    ├───Pester
    │   ├───bin
    │   ├───en-US
    │   ├───Examples
    │   │   ├───Calculator
    │   │   └───Validator
    │   ├───Functions
    │   │   └───Assertions
    │   ├───Snippets
    │   └───vendor
    │       └───tools
    │           ├───OneGet
    │           │   └───Etc
    │           └───PowerShellGet
    │               └───en-US
    └───ProtectedData
        └───en-US

C:\PROGRAM FILES\WINDOWSPOWERSHELL\MODULES
├───cDscDiagnostics
├───cDscResourceDesigner
├───cSmbShare
│   └───DscResources
│       └───PSHOrg_cSmbShare
├───cWebAdministration
│   ├───DSCResources
│   │   ├───PSHOrg_cAppPool
│   │   └───PSHOrg_cWebsite
│   └───Examples
├───dscbuild
├───DscConfiguration
├───DscDevelopment
├───DscOperations
├───Pester
│   ├───bin
│   ├───en-US
│   ├───Examples
│   │   ├───Calculator
│   │   └───Validator
│   ├───Functions
│   │   └───Assertions
│   ├───Snippets
│   └───vendor
│       └───tools
│           ├───OneGet
│           │   └───Etc
│           └───PowerShellGet
│               └───en-US
├───ProtectedData
│   └───en-US
└───StackExchangeResources
    ├───DSCResources
    │   ├───StackExchange_CertificateStore
    │   ├───StackExchange_FirewallRule
    │   ├───StackExchange_NetworkAdapter
    │   ├───StackExchange_Pagefile
    │   │   ├───StackExchange_en-US
    │   │   └───StackExchange_nl-NL
    │   ├───StackExchange_PowerPlan
    │   │   └───StackExchange_en-US
    │   ├───StackExchange_ScheduledTask
    │   ├───StackExchange_SetExecutionPolicy
    │   │   └───StackExchange_en-US
    │   └───StackExchange_Timezone
    └───test
        ├───integration
        │   └───StackExchange_PageFile
        │       └───pester
        └───unit
            └───StackExchange_Pagefile
                └───pester

now lets fix C:\DSC\SampleBuild.ps1

end
{
    Import-Module Pester -ErrorAction Stop
    Import-Module dscbuild -ErrorAction Stop
    Import-Module dscconfiguration -ErrorAction Stop

    $params = @{
        WorkingDirectory = (Get-TempDirectory).FullName
        SourceResourceDirectory = "$PSScriptRoot\DSC_Resources"
        SourceToolDirectory = "$PSScriptRoot\DSC_Tooling"
        DestinationRootDirectory = "$PSScriptRoot\BuldOutput"
        DestinationToolDirectory = $env:TEMP
        ConfigurationData = Get-DscConfigurationData -Path "$PSScriptRoot\DSC_Configuration" -Force -verbose
        ModulePath = "$PSScriptRoot\DSC_Script"  , "$PSScriptRoot\DSC_Tooling"
        ConfigurationModuleName = 'SampleConfiguration'
        ConfigurationName = 'SampleConfiguration'
        Configuration = $true
        Resource = $true
    }

    Invoke-DscBuild @params -verbose
}

begin
{
    function Get-TempDirectory
    {
        [CmdletBinding()]
        [OutputType([System.IO.DirectoryInfo])]
        param ( )

        do
        {
            $tempDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
        }
        until (-not (Test-Path -Path $tempDir -PathType Container))

        return New-Item -Path $tempDir -ItemType Directory -ErrorAction Stop
    }
}

So what is different?

  • DestinationRootDirectory = “$PSScriptRoot\BuldOutput”
    • Changed to point to a relative output. this is a test, so there is no need to place it into the common location for a pull server, although in production you may want to. I prefer to copy the files after a build, as it’s usually built on a machine other then the pull server
  •  ConfigurationData = Get-DscConfigurationData -Path “$PSScriptRoot\DSC_Configuration” -Force -verbose
    • added verbose so I can see how its progresses and help with troubleshooting
  • ModulePath = “$PSScriptRoot\DSC_Script”  , “$PSScriptRoot\DSC_Tooling”
      This is the big one. ModulePath and SourceResourceDirectory are going to be the only path’s in $psmodulepath when the configuration module (the module referenced by ConfigurationModuleName ) is loaded and when the configuration in ConfigurationName  is executed. This feature was added to resolve a problem when your modules used to build .mof files may be newer then the ones used to configure the machine running them. Something I ran into and Dave Wyatt was kind enough to solve
  • Invoke-DscBuild @params -verbose
    • Added -verbose. I’m a bit of a verbose junky.

With the updates to SampleBuild.ps1 we are ready to build our first sample set of .mof files

I do this in a clean environment so I’m opening PowerShell as an administrator and running C:\DSC\SampleBuild.ps1. After a long bit of scrolling if it works the last few lines should look like this.

VERBOSE: Moving 718aec80-e8fe-41b5-ac31-fbcd5d0186b1.mof to C:\DSC\BuldOutput\Configuration
VERBOSE: Moving b4519959-9724-40d5-ab62-5c4f82bbcd80.mof to C:\DSC\BuldOutput\Configuration
VERBOSE: Moving fc107c0b-1fc8-45fb-9991-a0a1f0fd6c21.mof to C:\DSC\BuldOutput\Configuration

Congratulations you have your first set of .mof files working with the PowerShell.org DSC tools.

Next up. Creating a pull request to update the readme.md and SampleBuild.ps1

Get-DSCFramework | Test-Lab

Posts in this series

  1. Test-HomeLab -InputObject ‘The Plan’
  2. Get-Posh-Git | Test-Lab
  3. Get-DSCFramework | Test-Lab
  4. Invoke-DscBuild | Test-Lab
  5. Test-Lab | Update-GitHub

In the last post we got the posh-git installed, Now we are going to fork the Powershell.org DSC tools development branch and clone that locally.

I already have an account with GitHub, You will need one to be able to contribute.

I had over the the repository and click the fork button

fork

with that done next is to get a copy of the clone URL.

cloneurl

I create a folder to store the repository in

C:\> mkdir github
    Directory: C:\
Mode                LastWriteTime     Length Name                                                                                                              
----                -------------     ------ ----                                                                                                              
d----          5/1/2015   5:25 PM            github                                                                                                            

C:\> cd github
C:\github> 

now i have everything I need to make a clone.

C:\github> git clone https://github.com/BladeFireLight/DSC.git PshOrgDSC --branch development
git : Cloning into 'PshOrgDSC'...
At line:1 char:1
+ git clone https://github.com/BladeFireLight/DSC.git PshOrgDSC --branch developme ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Cloning into 'PshOrgDSC'...:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 

C:\github> cd .\PshOrgDSC

C:\github\PshOrgDSC [development]> dir

    Directory: C:\github\PshOrgDSC

Mode                LastWriteTime     Length Name                                                                                                              
----                -------------     ------ ----                                                                                                              
d----          5/1/2015   5:38 PM            Tooling                                                                                                           
-a---          5/1/2015   5:38 PM        605 .gitattributes                                                                                                    
-a---          5/1/2015   5:38 PM        366 .gitignore                                                                                                        
-a---          5/1/2015   5:38 PM       1099 LICENSE.txt                                                                                                       
-a---          5/1/2015   5:38 PM       1231 README.md                                                                                                         
-a---          5/1/2015   5:38 PM       7305 README.old.md                                                                                                     

C:\github\PshOrgDSC [development]> 

I’m not sure why PowerShell thought it was an error but the clone worked.

Next up will be getting the example config to build.

C:\github\PshOrgDSC [development]> cd .\Tooling\Examples

C:\github\PshOrgDSC\Tooling\Examples [development]> dir

    Directory: C:\github\PshOrgDSC\Tooling\Examples

Mode                LastWriteTime     Length Name                                                                                                              
----                -------------     ------ ----                                                                                                              
d----          5/1/2015   5:38 PM            DSC_Configuration                                                                                                 
d----          5/1/2015   5:38 PM            SampleConfiguration                                                                                               
-a---          5/1/2015   5:38 PM       1770 README.md                                                                                                         
-a---          5/1/2015   5:38 PM       1203 SampleBuild.ps1       

Get-Posh-Git | Test-Lab

Posts in this series

  1. Test-HomeLab -InputObject ‘The Plan’
  2. Get-Posh-Git | Test-Lab
  3. Get-DSCFramework | Test-Lab
  4. Invoke-DscBuild | Test-Lab
  5. Test-Lab | Update-GitHub

My instal test lab starting to take shape.

I have a vyos router bridging my production environment and my isolated virtual switch similar to what Greg Altman talks about on PowerShell.org

And I have one Windows 8.1 Pro VM to setup the PowerShell.org DSC tools

Now I’m going to get the development branch, as it has some major fixes, including a re-working of how modules are tested and package, and how passwords are stored.  The part on password is the big item for me.

Now I could just download it via GitHub using IE.

DevBranch  downloadzip

But this blog is about giving back to the community so I’m going to using the GitHub client, and it’s included Posh-Git module for PowerShell.

The GitHub client is a nice simple GUI client, but I have one issue with it. It installs into a users profile. While all store apps and downloaded .net apps do this and it’s a good thing for isolation. In a secure environment like where i work it’s frowned apon. So ifyour in that boat, Posh-Git can be downloaded from github and works with other git clients.

Now I dont plan on using the GUI for the purpose of this blog so I’m going to change my PowerShell profile to load posh-git. GitHub client has to be ran once to create the files I use in my profile.

The commands for that are.

 #create/add posh-git to profile
 if (-not (test-path (split-path -Path $profile.CurrentUserAllHosts -Parent)))
 {
 mkdir (split-path -Path $profile.CurrentUserAllHosts -Parent)
 }
 '. (Resolve-Path "$env:LOCALAPPDATA\GitHub\shell.ps1")' | out-file -Path $profile.CurrentUserAllHosts -Append
 '. $env:github_posh_git\profile.example.ps1' | out-file -Path $profile.CurrentUserAllHosts -Append

restart powershell and I can run get-module to see what modules are loaded.


C:\> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     1.0.0.0    ISE                                 {Get-IseSnippet, Import-IseSnippet, New-IseSnippet}
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Content...}
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script     0.0        posh-git                            {Add-SshKey, Enable-GitColors, Get-AliasPattern, Get-GitDirectory...}

Now we are ready to fork the PowerShell.org DSC repository and clone it locally.