Automate the Process of Building and Capturing a Windows 10 1703 Reference Image: Automating using PowerShell

So far in this series we’ve populated our Deployment Share, created a Build and Capture Task Sequence and configured the CustomSettings.ini rules to skip the MDT Deployment Wizard to run our task sequence.

At this point, the CustomSettings.ini rules helps us to automate the process of running the task sequence and capturing the image but there’s still a few manual tasks of having to:

  • Create a Virtual Machine
  • Giving the VM’s network adapter the specific MAC address that we specified in the rules
  • Attaching the MDT Boot Image to the VM’s DVD drive
  • Turning on the VM

Only then does the MDT Deployment Wizard look up the MAC address in the VM and then processes the rules under a matching MAC address section in the CustomSettings.ini file.

So here we need a bit of help from PowerShell and also XML.

Download the zip file containing the PowerShell module and XML file

One of my goals of automating this process using PowerShell was that it should be extensible without having to change any PowerShell code. When Windows 10 1709 comes along later this year I don’t want to have to change anything in the code to account for that. Also, I don’t just create reference images for Windows 10 – I have other reference images to create for Windows 7 and 8.1 (with matching task sequences and rules in MDT). Taking inspiration from Mikael Nystrom’s image factory, I decided to have an XML file to hold data about the Windows reference images I want to create.

The XML file holds global data relating to each VM to create, such as the number of processors, RAM, Hyper-V Switch, ISO path, etc. It also holds data relating to every reference image I want to create, such as the name to give the VM and, most importantly, the Mac address to assign the VM.

This will, I hope, become clearer with an example of how to run the PowerShell function:

New-ReferenceImage –Image windows10-1703 –DestroyVM

Update: Click on the GIF for the high quality image. I didn’t realise WordPress will compress the image so much in the post that the text would be unreadable. Clicking on the image however displays the original high quality GIF.  Powershell 3

The value you provide in the –Image switch has to match a tag in the XML file (which is where the module gets its data from). PowerShell will match the value provided in the –Image switch with an <Image> tag in the XML file and then proceed to create the VM with the name and Mac address within the matching tag.

In the above example, this is how things work:

  1. PowerShell matches the value provided in –Image switch (windows10-1703) with the tag highlighted yellow below
  2. It creates a VM called “Reference Image – Windows 10 1703”
  3. Give the network adapter the MAC address 00:15:5D:00:0B:04. This is the same MAC address we have in our CustomSettings.ini rules.
  4. The MDT boot image is attached to the VM
  5. The VM fires up to boot straight into the MDT Deployment Wizard.
  6. The Deployment Wizard then looks up the VM’s MAC address and matches it with the MAC address provided in the CustomSettings.ini rules.
  7. The rules underneath the MAC address tells the Deployment Wizard to run our Build and Capture Task Sequence and create a captured WIM file at the end

The Settings XML FIle

               

                                4

                                8

                                40

                                External

                                E:\RefVMs

                                E:\MDT Boot Image\LiteTouchPE_x64_Build.iso

               

               

                                name=“windows10-1703”>

                                                <VMName>Reference Image – Windows 10 1703

                                                00:15:5D:00:0B:05

                                name=“windows10-1607”>

                                                Reference Image – Windows 10 1607

                                                00:15:5D:00:0B:04

                               

                                name=“windows10-1511”>

                                                Reference Image – Windwos 10 1511

                                                00:15:5D:01:FB:32

                               

                                name=“windows7”>

                                                Reference Image – Windwos 7

                                                00:15:5D:01:FB:37

                               

                                name=“windows8.1”>

                                                Reference Image – Windwos 8.1

                                                00:15:5D:01:FB:38

                                                                           

               

               

                                E:\RefVMs

                                ReferenceImageAutomation.log

               

Notes about the XML:

  • Make any changes to the XML to suit your requirements, such the path to the ISO, Hyper-V switch, etc.
  • MAC addresses must be in the format 00:15:5D:00:0B:04 (with colons, not dashes)

The PowerShell Module

A few notes about the PowerShell module:

  • It needs to run in an elevated PowerShell session. If not then it will warn you to do so and no action will be taken
  • Every action taken since running the function will be logged to a log file as specified in the XML
  • It will check the value you provide in the –Image switch against the XML. This is a dynamic parameter, so you can cycle through the available values by hitting tab
  • The –DestroyVM switch is optional. If included in the function then the VM will be removed from console and disk after the image has been created
  • If the–DestroyVM is not provided, then the next time the same reference image is to be created a VM with the same name will already exist (unless it was removed manually). In this case the script warns you that a VM already exists, beeps at you to alert you to the warning and then pauses for 30 seconds to allow you to close or end the session to stop the VM from being removed. If no action is taken within the 30 seconds then the existing VM will be forcefully stopped and removed before proceeding
  • If all the error checking goes well then the function calls a second function to create the VM
  • If the –DestroyVM switch was provided then it waits for the VM to shutdown (which it should do since your MDT rules tells it to shut down once complete) and then removes the VM

The module is available to download, but I’ve copied it below anyway. (Sorry, the formatting options available in WordPress is quite limited.)

# ** Shout out to  Mikael Nystrom, aka the Deployment Bunny, for inspiration ** #

 

# Location of XML file

$XMLFile = $env:SystemDrive\Users\$env:USERNAME\Documents\WindowsPowerShell\Modules\ReferenceImageAutomationSettings.xml”

 

# Read settings from XML

[xml]$Global:Settings = Get-Content $XMLFile

$Global:Processors = $Global:Settings.Root.GlobalVMSettings.Processors

$Global:StartUpRAM = [int]$Global:Settings.Root.GlobalVMSettings.StartUpRAM

$Global:VHDSize = [int]$Global:Settings.Root.GlobalVMSettings.VHDSize

$Global:HypervSwitch = $Global:Settings.Root.GlobalVMSettings.HypervSwitch

$Global:VMSavePath = $Global:Settings.Root.GlobalVMSettings.VMSavePath

$Global:ISOPath = $Global:Settings.Root.GlobalVMSettings.ISOPath

$Global:LogSavePath = $Global:Settings.Root.Logging.LogSavePath

$Global:LogFileName = $Global:Settings.Root.Logging.LogFileName

 

#Convert RAM and VHDSize to Bytes

$Global:StartUpRAM = ($Global:StartUpRAM * 1024*1024*1024)

$Global:VHDSize = ($Global:VHDSize * 1024*1024*1024)

 

# Function to write to the log

Function WriteToLog {

    [CmdletBinding()]

    Param

    (

        [Parameter(Mandatory=$true)]

        [string]$LogEntry=[switch]::Present

    )

 

    Process

 

    {

        # Write to log file

        Add-Content $Global:LogSavePath\$Global:LogFileName $(Get-Date)$LogEntry

    }

}

 

# Function to get list of available reference images to build, which is used to build the array set for the dynamic parameters

Function Get-AvailableImages {

    # Get list of valid images from the XML

    $Global:AvailableImages = $Global:Settings.GetElementsByTagName(“Image”) | ForEach {$_.name}

 

    Return $Global:AvailableImages

}

 

# The primary function of this module to create new referece images

Function New-ReferenceImage {

<#

.Synopsis

    Run “Get-TrueHelp New-ReferenceImageFactory” to get the correct syntax including all parameter options and extended help

 

    Ignore the syntax below as it doesn’t show the dynamic -Image parameter (a limitation in the Get-Help cmdlet)

#>

    [CmdletBinding()]

    Param

    (

        [Parameter(Mandatory=$false,Position=1,HelpMessage=“Would you like to destroy the VM after the reference image has been created”)]

        [switch]$DestroyVM

    )

 

    DynamicParam {

 

        # Thanks to Stephen Owen, aka @FoxDeploy, for the tip on Dynamic parameters

   

        # Set the dynamic parameters’ name

        $ParameterName = “Image”

           

        # Create the dictionary

        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

 

        # Create the collection of attributes

        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

           

        # Create and set the parameters’ attributes

        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

        $ParameterAttribute.Mandatory = $true

        $ParameterAttribute.Position = 0

        $ParameterAttribute.HelpMessage = “Which reference image would you like to build?”

 

        # Add the attributes to the attributes collection

        $AttributeCollection.Add($ParameterAttribute)

 

        # Generate and set the ValidateSet

        $ArraySet = Get-AvailableImages

        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ArraySet)

 

        # Add the ValidateSet to the attributes collection

        $AttributeCollection.Add($ValidateSetAttribute)

 

        # Create and return the dynamic parameter

        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)

        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

        return $RuntimeParameterDictionary

    }

 

    Begin {

        # Bind the parameter to a friendly variable

        $Image = $PsBoundParameters[$ParameterName]

    }

 

    Process

    {

        # Create directory to save log file if it does not exist, along with the log file

        If (-not (Test-Path $Global:LogSavePath)) {

            New-Item $Global:LogSavePath -ItemType Directory | Out-Null -ErrorAction SilentlyContinue

            New-Item $Global:LogSavePath\$Global:LogFileName -ItemType File | Out-Null -ErrorAction SilentlyContinue

        }

        Else {

            # Directory already exists

            # Create the log file if it does not exist

            If (-not (Test-Path $Global:LogSavePath\$Global:LogFileName)) {

                New-Item $Global:LogSavePath\$Global:LogFileName -ItemType File -ErrorAction SilentlyContinue

            }

        }

 

        # Log the request received to build image in the log file

        WriteToLog “Request received: New-ReferenceImage -Image $($Image) -DestroyVM $($DestroyVM) (initiated by $($env:USERNAME))”

 

        # Check to make sure the function is run as administrator

        $CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent())

        If (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator )) {

            Write-Host “`nWarning: You need to run this function as administrator” -ForegroundColor “Red”

            WriteToLog “Warning: Request was made without admin privileges”

            WriteToLog “No action taken”

            Return

        }

       

        # Check if image provided in the switch is valid (it should be found in the XML)

        If ((($Global:Settings).Root.VMSettingsPerImage.Image | Where-Object {$_.name -eq $Image})) {

 

            # Check if ISO and HypervSwitch exists

            If (-not (Test-Path $Global:ISOPath)) {

                Write-Output “`nWarning: ISO was not found where it was expectied to be ($($Global:ISOPath))”

                WriteToLog “Warning: ISO was not found where it was expectied to be ($($Global:ISOPath))”

                Return

            }

            If (-not (Get-VMSwitch $Global:HypervSwitch)) {

                Write-Output “`nVM switch not found ($($Global:HypervSwitch))”

                WriteToLog “VM switch not found ($($Global:HypervSwitch))”

                Return

            }

           

            # If path to save VM does not exist then create the directory before proceeding

            If (-not (Test-Path $Global:VMSavePath)) {

                Write-Output “`nPath to save the VM ($($Global:VMSavePath)) does not exist, it will be created now”

                WriteToLog “Path to save the VM ($($Global:VMSavePath)) does not exist, it will be created now”

                Try {

                    New-Item $Global:VMSavePath -ItemType Directory

                }

                Catch {

                    Write-Output “`nCould not create the directory ($($Global:VMSavePath))”

                    WriteToLog “Could not create the directory ($($Global:VMSavePath))”

                    Write-Output “`nExiting script”

                    WriteToLog “Exiting script”

                    Return

                }

 

            }

 

            # Get VM Name and VM Mac from XML, based on the Image entered by user

            $VMName =$Global:Settings.Root.VMSettingsPerImage.Image | Where-Object {$_.name -eq $Image} | Select-Object VMName | ForEach {$_.VMName}

            $VMMac = $Global:Settings.Root.VMSettingsPerImage.Image | Where-Object {$_.name -eq $Image} | Select-Object VMMac | ForEach {$_.VMMac}

 

            # Check if VM already exists, if so then try to remove the VM

            If (Get-VM -Name $VMName -ErrorAction SilentlyContinue) {

                Write-Host “`nWarning: A virtual machine with the name $($VMName) already exists” -ForegroundColor Magenta

                WriteToLog “A virtual machine with the name $($VMName) already exists”

                Write-Host “`nWarning: The existing VM will be STOPPED if running and REMOVED before proceeding” -ForegroundColor Magenta

                WriteToLog “The existing VM will be STOPPED if running and REMOVED before proceeding”

                Sleep 2

 

                # Make beeping sounds to alert to this warning

                [console]::beep(2000,500)

                [console]::beep(2000,500)

                [console]::beep(500,300)

                [console]::beep(500,300)

                [console]::beep(500,300)

                [console]::beep(2000,500)

                [console]::beep(2000,500)

                [console]::beep(2000,500)

                [console]::beep(2000,500)

 

                Write-Host “`nPausing for 30 seconds to allow you to close this console to prevent the VM from being stopped and removed” -ForegroundColor Red

                WriteToLog “Pausing for 30 seconds to allow you to close this console to prevent the VM from being stopped and removed”

 

                Sleep 30

 

                # 30 seconds is over, now stop the VM if it’s running

                If ((Get-VM -Name $VMName).State -eq “Running”) {

                   

                    Try {

                        Write-Output “`nStopping the VM ‘$($VMName)‘”

                        WriteToLog “Stopping the VM ‘$($VMName)‘”

                        Stop-VM -Name $VMName -TurnOff -Force -ErrorAction SilentlyContinue

                    }

                    Catch {

                        Write-Output “`nCould not stop VM”

                        WriteToLog “Could not stop VM”

                        Write-Output “`nExiting the script”

                        WriteToLog “Exiting the script”

                        Exit

                    }

                }

                   

                # Delete the VM from console and disk

                Try {

                    Write-Output “`nRemoving the VM ‘$($VMName)‘ from disk”

                    WriteToLog “Removing the VM ‘$($VMName)‘ from console and disk”

                    Get-VM -Name $VMName | Remove-VM -Force

                    Remove-Item $Global:VMSavePath\$VMName -Recurse -Force

                }

                Catch {

                    Write-Output “`nCould not remove VM”

                    WriteToLog “Could not remove VM”

                    Write-Output “`nExiting the script”

                    WriteToLog “Exiting the script”

                    Return

                }

            }

            Else {

                If (Test-Path $Global:VMSavePath\$VMName) {

                    Write-Warning “`nA folder with the same name of ‘$($VMName)‘ is found in $($Global:VMSavePath). It will be deleted in 30 seconds”

                    WriteToLog “A folder with the same name of $($VMName) is found in $($Global:VMSavePath). It will be deleted in 30 seconds”

                    Write-Host “`nPausing for 30 seconds to allow you to close this console to prevent the folder from being deleted” -ForegroundColor Red

                    WriteToLog “Pausing for 30 seconds to allow you to close this console to prevent the folder from being deleted”

 

                    # Make beeping sounds to alert to this warning

                    [console]::beep(2000,500)

                    [console]::beep(2000,500)

                    [console]::beep(500,300)

                    [console]::beep(500,300)

                    [console]::beep(500,300)

                    [console]::beep(2000,500)

                    [console]::beep(2000,500)

                    [console]::beep(2000,500)

                    [console]::beep(2000,500)

 

                    Sleep 30

 

                    # 30 seconds is over, remove the folder

                    WriteToLog “Removing the folder now”

                    Remove-Item $Global:VMSavePath\$VMName -Recurse -Force

                }

            }

 

            # Check if -DestroyVM switch has been provided

            If ($DestroyVM.IsPresent) {

                #Write-Output “`nThe VM will be destroyed once the reference image has been created”

                $Global:DestroyVM = $true

            }

            Else {

                $Global:DestroyVM = $false

            }

           

            # Proceed to building the reference image

            # Call the New-ReferenceVM function and pass on the VMName and VMMac switches

            New-ReferenceVM $VMName $VMMac

 

        }

        Else {

            # Image not valid

            Write-Warning “`nThe image you entered ‘$($Image)‘ is not valid”

            WriteToLog “Warning: The image you entered ‘$($Image)‘ is not valid”

           

            # Get list of valid images from the XML

            $ValidImages = $Global:Settings.GetElementsByTagName(“Image”) | ForEach {$_.name}

           

            Write-Output “`nReference images available to build are:”

           

            # Dish out list of acceptable images

            ForEach ($ImageItem in $ValidImages) {

                Write-Output $ImageItem

            }

        }

    }

}

 

Function New-ReferenceVM {

    [CmdletBinding()]

    Param

    (

        [Parameter(Mandatory=$true)]

        [string]$VMNameInput=[switch]::Present,

        [Parameter(Mandatory=$true)]

        [string]$VMMacInput=[switch]::Present

    )

   

    Process

    {

        # Check to make sure the MAC address provided is in the correct format

        # RegEx to satisfy MAC address validaiton

        $MacValidation = “^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$”

       

        # If MAc address is not valid then exit script

        If (-not $VMMacInput -cmatch $MacValidation) {

            Write-Output “`nMac address is not valid. Please make sure the MAC address is in the format 00:15:5D:00:0B:04”

            WriteToLog “Mac address is not valid. Please make sure the MAC address is in the format 00:15:5D:00:0B:04”

            Return

        }

 

        Write-Output “`nBuilding reference image: $($VMNameInput)

        WriteToLog “Building reference image: $($VMNameInput)

 

        # Create Virtual Machines

        WriteToLog “Creating the VM ‘$($VMNameInput)‘”

        Try {

            $VM = New-VM –Name $($VMnameInput) –MemoryStartupBytes $Global:StartUpRAM -SwitchName $Global:HypervSwitch -NewVHDPath $($Global:VMSavePath)\$($VMNameInput)\Virtual Hard Disks\$($VMNameInput).vhdx” -NewVHDSizeBytes $Global:VHDSize -Path $Global:VMSavePath # -Generation 2

            Set-VMProcessor -VMName $VMNameInput -Count $Global:Processors

            Set-VMNetworkAdapter -VMName $VMNameInput -StaticMacAddress $VMMacInput

            Add-VMDvdDrive -VM $VM -Path $Global:ISOPath

            Set-VMMemory -VM $VM -DynamicMemoryEnabled $false

        }

        Catch {

            Write-Output “`nCould not create the VM”

            Write-Output “`nExiting the script”

            WriteToLog “Could not create the VM”

            WriteToLog “Exiting the script”

            Return

        }

       

        #Start the VMs

        WriteToLog “Starting the VM”

        Try {

            Start-VM $VM -ErrorAction SilentlyContinue

        }

        Catch {

            Write-Output “`nCould not start the VM”

            Write-Output “`nExiting script”

            WriteToLog “Could not start the VM”

            WriteToLog “Exiting script”

            Return

        }

 

        # Wait until the VM powers off

        While ($VM.State -eq “Running”) {

            Sleep 60

            Write-Output “`nStill running: ‘$($VMNameInput)‘”

        }       

       

        # VM is stopped, which means the image has been created

        Write-Host “`nVM has powered off” -ForegroundColor Green

        Write-Host “Assume the reference image has been created (unless VM was forcefully turned off)” -ForegroundColor Green

        WriteToLog “VM has powered off, assume the reference image has been created (unless VM was forcefully turned off)”

       

        # Destroy the VM and delete the VHD if the user opted for it

        If ($Global:DestroyVM -eq $true) {

 

            # Destroy the VM

            Write-Output “`nDestroying VM: $($VMNameInput)

            WriteToLog “Sleeping for 10 seconds to allow the VM to shut down properly”

            Sleep 10 # To allow time for the VM to properly shut down

           

            # Proceed to stopping the VM

            WriteToLog “Destroying VM: $($VMNameInput) as requested”

            If (-not ($VM.State -eq “Off”)) {

                Try {

                    Stop-VM -Name $VMNameInput -TurnOff -Force -ErrorAction SilentlyContinue

                }

                Catch {

                    Write-Output “`nCould not stop the VM”

                    Write-Output “`nWill attempt to remove the VM”

                    WriteToLog “Could not stop the VM”

                    WriteToLog “Will attempt to remove the VM”

                    #Return

                }               

            }

 

            # Now remove the VM

            Try {

                Remove-VM -Name $VMNameInput -Force -ErrorAction SilentlyContinue

            }

            Catch {

                Write-Output “`nCould not remove the VM”

                Write-Output “`nExiting script”

                WriteToLog “Could not remove the VM”

                WriteToLog “Exiting script”

                Return

            }

           

            # Now remove VM files from disk

            Remove-Item $Global:VMSavePath\$VMNameInput\* -Recurse -Force

            Remove-Item $Global:VMSavePath\$VMNameInput -Recurse -Force

            Write-Output “`nSuccess”

            WriteToLog “Success”

        }

    }

}

 

Function Get-TrueHelp {

    [CmdletBinding()]

    Param

    (

        [Parameter(Mandatory=$true)]

        [string]$FunctionName=[switch]::Present

    )

 

    Process

    {

        If ($FunctionName = “New-ReferenceImage”) {

 

            # Get array of available options for the -Image switch

            $ImageOptions = Get-AvailableImages

 

            # Convert list of array into the correct syntax

            $Count = 0

            ForEach ($Option in $ImageOptions) {

 

                $Count = $Count + 1

                If ($Count -eq $ImageOptions.Length) {

                    $ImageSyntax = ($($ImageSyntax) $($Option))

                }

                Else {

                    $ImageSyntax = ($($ImageSyntax) $($Option) |”)

                }

            }

              

            Write-Output “`nNAME”

            Write-Output ”    New-ReferenceImage”

           

            Write-Output “`nSYNOPSIS”

            Write-Output ”    Creates a Virtual Machine to build Windows reference images.”

           

            Write-Output “`nSYNTAX”

            Write-Output ”    New-ReferenceImage [-Image] {$ImageSyntax} [-DestroyVM]”

 

            Write-Output “`nDESCRIPTION”

            Write-Output ”    Looks up the value provided in the -Image switch against the XML file and retrieves the VM Name and Mac address.

   It then attaches Lite Touch ISO to the VM and starts the VM which then builds the reference image”

 

            Write-Output “`nEXAMPLE”

            Write-Output ”    New-ReferenceImage -Image windows10-1703 -DestroyVM”

 

            Write-Output “`nEXAMPLE”

            Write-Output ”    New-ReferenceImage -Image windows10-1703″

 

            Write-Output “`nRELATED LINKS”

            Write-Output ”    Me, Myself and IT, emeneye.wordpress.com”

        }

    }

}

 

#Export-ModuleMember -Function Get-AvailableImages, WriteToLog, New-ReferenceVM

Export-ModuleMember -Function New-ReferenceImage, Get-TrueHelp

Advertisements

One thought on “Automate the Process of Building and Capturing a Windows 10 1703 Reference Image: Automating using PowerShell

  1. NICE JOB,
    When i boot the VM with LiteTouchPE_x64_Build.iso the first time the task sequence, during the installation the VM reboot and still boot to the ISO, with WINPE MDT

    I prefered to boot to ISO? could you help me

    Work find with PXE, with this change
    -Generation 2
    and comment # Add-VMDvdDrive -VM $VM -Path $Global:ISOPath

    $VM = New-VM –Name “$($VMnameInput)” –MemoryStartupBytes $Global:StartUpRAM -SwitchName $Global:HypervSwitch -NewVHDPath “$($Global:VMSavePath)\$($VMNameInput)\Virtual Hard Disks\$($VMNameInput).vhdx” -NewVHDSizeBytes $Global:VHDSize -Path $Global:VMSavePath -Generation 2
    Set-VMProcessor -VMName $VMNameInput -Count $Global:Processors
    Set-VMNetworkAdapter -VMName $VMNameInput -StaticMacAddress $VMMacInput
    # Add-VMDvdDrive -VM $VM -Path $Global:ISOPath
    Set-VMMemory -VM $VM -DynamicMemoryEnabled $false

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s