UPDATE: This post is old and intended as a walk through on how my original script was put together for those wishing to learn PowerShell. For the finished, up-to-date script please check out this post and you can download the script from my Microsoft TechNet profile.
Recently I’ve been looking into automating my image build process further using PowerShell to create an Image Factory of sorts. There are other similar scripts that I’ve found online, but I like the relatively simple and straightforward method I’ve developed below. If like me you’re looking to automate your image build process so that you can have a fully up-to-date image library after patch Tuesday each month, then this may be for you. I’m moving the change log and updates to this page, as this post is more about the script itself and how I put it together.
Prerequisites
To use this script you’ll need:
- A Windows 10/Windows Server 2016/Windows 8.1/Windows Server 2012 R2 Hyper-V host
- Microsoft Deployment Toolkit installed
- Separate Build and Deployment shares
- Fully automated reference image build Task Sequences
It’s helpful to run the script on the same computer that has Microsoft Deployment Toolkit installed - although the shares do not have to be on this computer. For example, I run this script on my workstation which has MDT installed but the Build/Deployment shares are on a remote server. The Hyper-V host can be either a remote server or the computer that the script is being run on.
I use the current time and date to name VHDs, WIM files and so on to avoid naming conflicts and files getting overwritten. If you do not have separate Build/Deployment MDT shares, the script can be tweaked to run with this set up, but you will not be able to deploy devices whilst the script is running. Similarly, if you do have separate shares, this script effectively takes over the Build share for the duration of it’s run. This is to automate the running of the Task Sequences by making the necessary changes to the CustomSettings.ini file of the Build share. Your Build share should be configured as below so that the process is completely automated. If slightly different settings are needed for your environment then the script can be tweaked to make those changes, but it’s important to remember that the purpose of building an image using Hyper-V is so that the image is as “clean” as it can be, containing only the most general data needed. If drivers are required, they should be a part of the deployment process. If you have any queries about making changes, please leave a comment.
My Build share CustomSettings.ini. Please note: it’s important to have at least one blank line at the very end.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
[Settings]
Priority=Model, Default, SetOSD
Properties=OSDPrefix
[Virtual Machine]
DriverGroup001=Virtual Machine Driver
SelectionProfile=nothing
OSDPrefix=VM
[Default]
_SMSTSORGNAME=Mike Galvin | Build Share
_SMSTSPackageName=%TaskSequenceName%
UserDataLocation=NONE
DoCapture=YES
ComputerBackupLocation=\\wds01\buildshare$\Captures
BackupFile=%TaskSequenceID%_#year(date) & "-" & month(date) & "-" & day(date) & "-" & hour(time) & "-" & minute(time)#.wim
OSInstall=Y
TimeZoneName=GMT Standard Time
KeyboardLocale=0809:00000809
UILanguage=en-GB
UserLocale=en-GB
KeyboardLocale=en-GB
BitsPerPel=32
VRefresh=60
XResolution=1
YResolution=1
WSUSServer=http://wsus01:8530
SkipAdminPassword=YES
SkipCapture=YES
SkipRoles=YES
SkipProductKey=YES
SkipUserData=YES
SkipComputerBackup=YES
SkipBitLocker=YES
SkipLocaleSelection=YES
SkipTimeZone=YES
SkipDomainMembership=YES
SkipSummary=YES
SkipFinalSummary=YES
FinishAction=SHUTDOWN
EventService=http://wds01:9800
SLShare=\\wds01\buildshare$\Logs
OSDComputerName=%OSDPrefix%-%TaskSequenceID%
|
Once you have your Build share CustomSettings.ini configured, you’ll need to have an up to date LiteTouch_x64.iso generated and placed on the computer you are using for Hyper-V. The LiteTouch_x64.iso can be found under BuildShare\Boot\LiteTouchPE_x64.iso. You can also use a LiteTouch_x86.iso if needed. Of course we can’t forget about the BootStrap.ini settings either. For automated log in, it should look something like this:
1
2
3
4
5
6
7
8
9
|
[Settings]
Priority=Default
[Default]
DeployRoot=\\WDS01\BuildShare$
UserDomain=FQDN.domain.co.uk
UserID=mdt\admin
UserPassword=p@ssw0rd
SkipBDDWelcome=YES
|
The script below will require some configuration for your environment, but when configured it will take a list of Task Sequence IDs, generate VMs for them, boot the VMs into the Task Sequence, run the Task Sequence to completion, capture the WIM, then import the WIMs into the Deploy share, ready for you to test and deploy them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
# -------------------------------------------
# Script: image-factory_v2-1.ps1
# Version: 2.1
# Author: Mike Galvin
# Date: 24/04/2017
# -------------------------------------------
##Set MDT Custom Settings.ini location
$csini = "\\wds01\BuildShare$\Control"
##List Task Sequence IDs to run
$tsid = "REF-W101607","REF-WS2016-STD"
##Log file location
$log = "E:\imagefactory.log"
##Configure Hyper-V
$vmhost = "vs01"
$vhdpath = "G:\Hyper-V\VHD"
$vhdremotepath = "\\$vmhost\g$\Hyper-V\VHD"
$bootmedia = "E:\iso\LiteTouchPE_x64-build.iso"
$vmnic = "v-NIC"
##Configure MDT
$mdt = "C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"
$mdtpath = "\\wds01\DeploymentShare$"
$captures = "\\wds01\BuildShare$\Captures"
##Configure mail for log file
$toaddress = "[email protected]"
$fromaddress = "[email protected]"
$subject = "Image Factory Log"
$mailserver = "mail.contoso.com"
##Start Log Start-Transcript
$log
##Import old Hyper V Module for WS2012R2
Import-Module C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Hyper-V\1.1\Hyper-V.psd1 -Verbose
##Import MDT Module
Import-Module $mdt -Verbose
ForEach ($id in $tsid) {
##Setup MDT Custom Settings for VM
Copy-Item $csini\CustomSettings.ini $csini\CustomSettings-backup.ini -Verbose
Start-Sleep -s 5
Add-Content $csini\CustomSettings.ini "TaskSequenceID=$id" -Verbose
Add-Content $csini\CustomSettings.ini "SkipTaskSequence=YES" -Verbose
Add-Content $csini\CustomSettings.ini "SkipComputerName=YES" -Verbose
(Get-Content $csini\CustomSettings.ini).replace('OSDComputerName=%OSDPrefix%-%TaskSequenceID%', ';OSDComputerName=%OSDPrefix%-%TaskSequenceID%') | Set-Content $csini\CustomSettings.ini -Verbose
##Create VM
$vmname = ("build-{0:yyyy-MM-dd-HH-mm}" -f (get-date))
New-VM -name $vmname -MemoryStartupBytes 4096MB -BootDevice CD -Generation 1 -NewVHDPath $vhdpath\$vmname.vhdx -NewVHDSizeBytes 130048MB -SwitchName $vmnic -ComputerName $vmhost -Verbose
Set-VM $vmname -ProcessorCount 2 -StaticMemory -ComputerName $vmhost -Verbose
Set-VMDvdDrive -VMName $vmname -ControllerNumber 1 -ControllerLocation 0 -Path $bootmedia -ComputerName $vmhost -Verbose
Start-VM $vmname -ComputerName $vmhost -Verbose
##Wait for VM to stop
while ((get-vm -name $vmname -ComputerName $vmhost).state -ne 'Off') { start-sleep -s 5 }
##Remove VM
Remove-VM $vmname -ComputerName $vmhost -Force -Verbose
Remove-Item $vhdremotepath\$vmname.vhdx -Verbose
##Reset MDT Custom Settings
Remove-Item $csini\CustomSettings.ini -Verbose
Move-Item $csini\CustomSettings-backup.ini $csini\CustomSettings.ini -Verbose
Start-Sleep -s 5
}
##Connect to MDT Production
New-PSDrive -Name "DS002" -PSProvider MDTProvider -Root $mdtpath -Verbose
##Get files
$wims = Get-ChildItem $captures\*.wim
##Import the Captured REF Image into MDT Production
ForEach ($file in $wims) { Import-MDTOperatingSystem -path "DS002:\\Operating Systems" -SourceFile $file -DestinationFolder $file.Name -Verbose }
##Remove Captured WIMs
Remove-Item $captures\*.wim -Verbose
##Stop Log
Stop-Transcript
##Send Mail
$body = Get-Content -Path $log | Out-String Send-MailMessage -To $toaddress -From $fromaddress -Subject $subject -Body $body -SmtpServer $mailserver
##END
|
Let’s go through each section of the script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
##Set MDT Custom Settings.ini location
$csini = "\\wds01\BuildShare$\Control"
##List Task Sequence IDs to run
$tsid = "REF-W101607","REF-WS2016-STD"
##Log file location
$log = "E:\imagefactory.log"
##Configure Hyper-V
$vmhost = "vs01"
$vhdpath = "G:\Hyper-V\VHD"
$vhdremotepath = "\\$vmhost\g$\Hyper-V\VHD"
$bootmedia = "E:\iso\LiteTouchPE_x64-build.iso"
$vmnic = "v-NIC"
##Configure MDT
$mdt = "C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"
$mdtpath = "\\wds01\DeploymentShare$"
$captures = "\\wds01\BuildShare$\Captures"
##Configure mail for log file
$toaddress = "[email protected]"
$fromaddress = "[email protected]"
$subject = "Image Factory Log"
$mailserver = "mail.contoso.com"
|
This is where all the configuration of the script is. Here you set the locations of all the resources needed. First, set the location of the Custom Settings.ini file in the MDT share that is being used for the building of the images. Then list the Task Sequence IDs for all the Task Sequences that need to be run.
New for v2.1 of the script, you can set a log file for the script to output to, and optionally email it when the script completes. This does not replace MDT’s logging for the Task Sequences, that should still be enabled, this is logging just for the script.
Next up is the configuration of the resources for the VMs. The configuration of the Hyper-V $vhdpath and $bootmedia variables are the local paths that Hyper-V uses. For example, if you’re running the script on your admin PC and using a remote Hyper-V host, then the $vhdpath would be the local disk on the remote server. The $vhdremotepath is regardless of where the script is running from, the UNC path of the location of the VHD location.
Added in v2.1 of the script is a variable to configure the Virtual Switch for the Network Adaptor to use. Previously this had to be configured lower down in the script and was an oversight. Here, I’ve corrected this.
The MDT share locations are self explanatory, the $mdt variable is the location of the MDT PowerShell module needed later on in the script - MDT should be installed on the device that this script is running on.
Added in v2.1 is the configuration for emailing the log file when the script completes.
1
2
|
##Import old Hyper V Module for backquards compatibility
Import-Module C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Hyper-V\1.1\Hyper-V.psd1 -Verbose
|
This line is for backwards compatibility with previous versions of Hyper-V older than the device running the script. For example: if the device running the script is Windows 10 and the Hyper-V host is a remote server running Windows Server 2012 R2, then this line should be enabled. If the Hyper-V host is running the same version of Windows as the device running the script then you can leave this line commented out.
1
2
3
4
5
6
7
8
9
|
ForEach ($id in $tsid) {
##Setup MDT Custom Settings for VM
Copy-Item $csini\CustomSettings.ini
$csini\CustomSettings-backup.ini -Verbose
Start-Sleep -s 5
Add-Content $csini\CustomSettings.ini "TaskSequenceID=$id" -Verbose
Add-Content $csini\CustomSettings.ini "SkipTaskSequence=YES" -Verbose
Add-Content $csini\CustomSettings.ini "SkipComputerName=YES" -Verbose
(Get-Content $csini\\CustomSettings.ini).replace('OSDComputerName=%OSDPrefix%-%TaskSequenceID%', ';OSDComputerName=%OSDPrefix%-%TaskSequenceID%') | Set-Content $csini\CustomSettings.ini -Verbose
|
Now the script is going to go through each Task Sequence ID that has been configured and run the image build process, one at a time. Here the CustomSettings.ini file is backed up and edited so that the VMs will boot automatically into each Task Sequence.
1
2
3
4
5
6
7
8
9
10
11
12
|
##Create VM
$vmname = ("build-{0:yyyy-MM-dd-HH-mm}" -f (get-date))
New-VM -name $vmname -MemoryStartupBytes 4096MB -BootDevice CD -Generation 1 -NewVHDPath $vhdpath\$vmname.vhdx -NewVHDSizeBytes 130048MB -SwitchName $vmnic -ComputerName $vmhost -Verbose
Set-VM $vmname -ProcessorCount 2 -StaticMemory -ComputerName $vmhost -Verbose
Set-VMDvdDrive -VMName $vmname -ControllerNumber 1 -ControllerLocation 0 -Path $bootmedia -ComputerName $vmhost -Verbose
Start-VM $vmname -ComputerName $vmhost -Verbose
##Wait for VM to stop
while ((get-vm -name $vmname -ComputerName $vmhost).state -ne 'Off') { start-sleep -s 5 }
##Remove VM
Remove-VM $vmname -ComputerName $vmhost -Force -Verbose Remove-Item $vhdremotepath\$vmname.vhdx -Verbose
|
This is the creation, start up and deletion of the VMs. When the VM starts it will auto boot from the LiteTouch_x64-build.iso, auto login to the Build share and run the configured task sequence. It will use an automatically generated computer name, install the applications specified, run Windows Update from WSUS, then capture the image and finally shutdown, then the script removes the VM and it’s VHD file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
##Connect to MDT Production
New-PSDrive -Name "DS002" -PSProvider MDTProvider -Root $mdtpath -Verbose
##Get files
$wims = Get-ChildItem $captures\*.wim
##Import the Captured REF Image into MDT Production
ForEach ($file in $wims) { Import-MDTOperatingSystem -path "DS002:\\Operating Systems" -SourceFile $file -DestinationFolder $file.Name -Verbose }
##Remove Captured WIMs
Remove-Item $captures\*.wim -Verbose
##Stop Log
Stop-Transcript
##Send Mail
$body = Get-Content -Path $log | Out-String Send-MailMessage -To $toaddress -From $fromaddress -Subject $subject -Body $body -SmtpServer $mailserver
|
Here the script connects to MDT, imports all the WIMs in the Capture folder to the Operating Systems folder on the Deploy share then deletes the source capture files.
Added in v2.1 the logging is stopped and then emailed to an address of your choice.
After the script has run to completion, you should have all the specified Task Sequences' WIMs imported into your Deploy share ready to be tested and deployed - they will have a long name as per usual with importing custom images. I actually tried to automate this part of the process as well, so new Task Sequences would be created for the new images, but alas, I hit a few road blocks. I figured that it wasn’t the end of the world and I would still like to test the images before making them widely available.
This has been a big post and I know that this script is largely dependant on a lot of factors which could be very unique to your circumstances. It may be hard work to fully automate your process but I believe it’s worth it. My team and I have been enjoying coming into the office to fresh images in the morning, instead of having to remember to manually set them off.
I take great care to test my ideas and make sure my articles are accurate before posting, however mistakes do slip through sometimes.
If you have any questions or comments, please leave them below.
-Mike