2014-01-06

In the world of deployment a reference image is where all begins, basically we take the ISO/WIM from the vendor, install it, tweak it, patch it and on the other side we now a have a customized reference image that is more suitable for our needs. This applies to both clients and servers. There are two ways of doing this, the correct way and the incorrect way. The correct way is repeatable and as automated as possible. This blog is about the correct way and how to automate it just a bit more then usual. You can of coarse modify the script so it will do more, for example upload the images to the correct destination.

Acknowledgement goes to:

Michael Niehaus for the post: http://blogs.technet.com/b/mniehaus/archive/2009/09/23/setting-up-an-image-factory-part-1-using-mdt-2010-lite-touch-with-scvmm-2008-r2.aspx

Johan Arwidmark for http://www.deploymentresearch.com/Research/tabid/62/EntryId/60/MMS-2012-Running-Hyper-V-Data-Exchange-Service-in-WinPE-3-0.aspx

Keith Garner for http://keithga.wordpress.com/2013/07/23/running-hyper-v-data-exchange-service-in-winpe-4-0-adk/

What do I need to get this working?

Step 1: Set up Microsoft Deployment Toolkit 2013 on a server

Step 2: Import Operating Systems, applications and create your task sequences

Step 3: Install Windows Server 2012 R2 (or Windows 8.1) with Hyper-V as your “creator”

Step 4: Download the script and enjoy the show

Note:

The script is designed to run on the Hyper-V machine, but it is possible to run this remote either using Remote-Sessions or defining –Computer on every Commands that relates to Hyper-V

The Solution is tested on Windows Server 2012 R2 with MDT 2013 and ADK 8.1 BUT should work just fine on Windows 8.1

How does the script work?



The PowerShell module has a couple of functions

New-AutoGenRefImages

This function will connect to your MDT server, grab the boot ISO, read all TaskSequences from Task Sequences\REF



And for each of them build a VM with the same name as the task sequence. When they are all built it will power them on one by one, booting from the ISO and then run the task sequence that corresponds to the name of the the VM. So if you have a TS with the ID of RW81.x86, it will build a VM with that name and run the TS with that ID on that VM.

You might ask your self how that is done?. I’m not using the database, not using GUID number or Mac’s. The main reason is that I want a solution with as little dependencies as possible, so it will work anytime at any customer (almost). Instead I’ loading the drivers for KVP (Key Value Pair Exchange) in WinPE, I grab the name of the VM before the customsettings.ini is read using a userexit script in bootstrap.ini so I have a Property called VMNameAlias wich then is used to control the deployment process, a neat solution IMHO (more on that later)

Remove-AutoGenRefImages

This function will destroy and remove all VMs, it does this by getting all task sequence ID’s from the MDT server, and find corresponding VM’s and then destroy and clean up.

New-RefImage

This is a manual process, you now need to KNOW the Task Sequence ID, since that is a validation in the CMDLET

Remove-RefImage

Also a manual process, it will destroy/delete/cleanup whatever VMname you type in, not NO questions, it just deletes them, so don’t type in DC01 or something like that.

Get-RefTaskSequence

A simple CMDLET that lists all task sequences that are in the \REF folder in MDT

The Details

Download the Scripts from here: http://sdrv.ms/1hqoJIh

Download devcon.exe from here: http://support.microsoft.com/kb/311272/en-us

Modify ImageFactoryV2.xml

This is my sample XML file and you need to modify it so it will work in your environment.

<Settings>
    <DeploymentShare>\\SRVHOST555\MDTBuildLab$</DeploymentShare>
    <HyperVHost>SRVHOST555</HyperVHost>
    <HyperVHostNetwork>UplinkSwitch</HyperVHostNetwork>
    <HyperVStorage>E:\VMs</HyperVStorage>
    <HyperVISOFolder>E:\ISO</HyperVISOFolder>
    <HardwareProfile>MDT 2010 Profile</HardwareProfile>
    <TaskSequenceFolder>MDT:\Task Sequences\REF</TaskSequenceFolder>
    <StartUpRAM>2</StartUpRAM>
    <VLANID>0</VLANID>
    <VHDSize>60</VHDSize>
</Settings>

Modify bootstrap.ini

Bootstrap.ini needs some modification (besides being silent). You need to add the following:



In text:

[Settings]
Priority=Default

[Default]
DeployRoot=\\SRVHOST555\MDTBuildLab$
UserDomain=NETWORK
UserID=Administrator
UserPassword=Password02
SkipBDDWelcome=YES
SubSection=ISVM-%IsVM%

[ISVM-True]
UserExit=LoadKVPInWinPE.vbs

This will load a userexit script that installs the driver for KVP and create a fake services that will allow it to load.

Modify customsettings.ini

You need to modify customsettings.ini so it will read in the values from the KVP correctly

In text:

[Settings]
Priority=Init,Default
Properties=VMNameAlias

[Init]
UserExit=ReadKVPData.vbs
VMNameAlias=#SetVMNameAlias()#

[Default]
TaskSequenceID=%VMNameAlias%

You need to add the Property VMNameAlias under the Settings Section

You need to create a new Section called Init that runs the UserExit

You need to set the TaskSequenceID to match the returned value from the UserExit script

Modify the boot image:

You need to make sure that those two scripts are added correctly and we also need the driver for KVP. One easy way is to use the “Extra” folder feature in MDT, whatever you put in that folder will end up on the boot image.

My deployment share is located in

E:\MDTBuildLab

so I created a folder called

E:\MDTBuildLab\Extra

In that Folder I two folders, Deploy and KVP. In the folder Deploy I have a subfolder called Scripts.

Like this:

Content of the KVP folder:

In the KVP folder you need to add the driver for KVP, and that you can do by searching for VMGuest.iso on your Hyper-V host, it is in C:\Windows\System32, mount that is and extract the drivers.

The driver you need is in the support\x86\Windows6.x-HyperVIntegrationServices-x86.cab and the folder is namned x86_wvmic.inf_31bf3856ad364e35_6.3.9600.16385_none_968283a5680527fe

(read Keith Garners blog for details, Note: In Keith’s blog it is 64 bit, I’m using the 32 bit driver, since I boot from the x86 boot image. Therefore the name is different of course.)

You also need to store devcon.exe in that folder

When you got them, it should look like this in the folder

Content of the Deploy\Scripts folder

In this folder you store the LoadKVPinWinPE.vbs script

Like this:

Update the BootImage:

In the Boot Image setting for WinPE, for the setting called Extra directory to add, browse to your folder, apply, update boot image.

The Scripts:

LoadKVPInWinPE.vbs

Mainly “borrowed” from Keith’s blog, just modified to use devcon.exe instead the method suggested by Keith

option explicit

function UserExit(sType,Swhen,Sdetail,bSkip)
    oLogging.CreateEntry "USEREXIT:ModelAliasExit.vbs started: " & sType & " " & sWhen & " " & sDetail, LogTypeInfo

    If ucase(oEnvironment.Item("ISVM")) = "TRUE" and oEnvironment.Item("OSVERSION") = "WinPE" then

        oLogging.CreateEntry "Load the Integration Components", LogTypeInfo
        oUtility.RunWithConsoleLogging "\KVP\devcon.exe /r install \KVP\wvmic.inf vmbus\{242ff919-07db-4180-9c2e-b86cb68c8c55}"
        CreateFakeService "TermService", "Remote Desktop Service", "FakeService.exe"
        GetObject("winmgmts:root\cimv2").Get("Win32_Service.Name=’vmickvpexchange’").StartService()
        oUtility.SafeSleep 10000
        LoadVariablesFromHyperV

    End if

    ‘ Write back an unique number to the host to let them know that the variables have been loaded.
    oUtility.RegWrite "HKLM\SOFTWARE\Microsoft\Virtual Machine\Auto\HydrationGuestStatus","eb471002-58aa-473a-850f-7b626613055f"

    Userexit=success

end function

‘ ———————————————————

Function CreateFakeService (sName,sDisplayName,sPathName )

    Dim objService
    Dim objInParam

    ‘ Obtain the definition of the Win32_Service class.
    Set objService = GetObject("winmgmts:root\cimv2").Get("Win32_Service")

    ‘ Obtain an InParameters object specific to the Win32_Service.Create method.
    Set objInParam = objService.Methods_("Create").inParameters.SpawnInstance_()

    ‘ Add the input parameters.
    objInParam.Properties_.item("Name") = sName
    objInParam.Properties_.item("DisplayName") = sDisplayName
    objInParam.Properties_.item("PathName") = sPathName
    objInParam.Properties_.item("ServiceType") = 16
    objInParam.Properties_.item("ErrorControl") = 0
    objInParam.Properties_.item("StartMode") = "Manual"
    objInParam.Properties_.item("DesktopInteract") = False

    ‘ Execute the method and obtain the return status.
    CreateFakeService = objService.ExecMethod_("Create", objInParam).ReturnValue

End function

Function LoadVariablesFromHyperV

    Dim objReg
    Dim aSubValues
    Dim aValues
    Dim SubVal

    oLogging.CreateEntry "Has MDT Environment Variables in Integration Components.", LogTypeInfo
    Set objReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
    objReg.EnumValues &H80000002, "SOFTWARE\Microsoft\Virtual Machine\External", aSubValues, aValues

    If isArray(aSubValues) then
        For Each SubVal in aSubValues
            oLogging.CreateEntry "Found Key: [" & SubVal & "]", LogTypeInfo
            oEnvironment.Item(SubVal) = oUtility.RegRead("HKLM\SOFTWARE\Microsoft\Virtual Machine\External\" & SubVal)
        Next
    End if

End Function

ReadKVPData.vbs

‘//—————————————————————————-
‘// Version: 1.0 – Jan 04, 2013 – Mikael Nystrom
‘// Twitter @mikael_nystrom
‘// Blog http://deploymentbunny.com
‘// This script is provided "AS IS" with no warranties, confers no rights and
‘// is not supported by the author
‘//—————————————————————————-
‘ //           
‘ // Usage:     Modify CustomSettings.ini similar to this:
‘ //              [Settings]
‘ //              Priority=SetAlias, Default
‘ //              Properties=VMNameAlias
‘ //
‘ //              [SetAlias]
‘ //              VMNameAlias=#SetVMNameAlias()#
‘ // ***** End Header *****

Function UserExit(sType, sWhen, sDetail, bSkip)

    oLogging.CreateEntry "UserExit: started: " & sType & " " & sWhen & " " & sDetail, LogTypeInfo
    UserExit = Success

End Function

Function SetVMNameAlias()
    oLogging.CreateEntry "UserExit: Running function SetVMNameAlias ", LogTypeInfo
    SetVMNameAlias = ""
    SetVMNameAlias = oShell.RegRead("HKLM\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters\VirtualMachineName")
    oLogging.CreateEntry "UserExit: SetVMNameAlias has been set to " & SetVMNameAlias, LogTypeInfo
    oLogging.CreateEntry "UserExit: Departing…", LogTypeInfo
End Function

ImageFactoryV2.psm1

<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
.INPUTS
   Inputs to this cmdlet (if any)
.OUTPUTS
   Output from this cmdlet (if any)
.NOTES
   General notes
.COMPONENT
   The component this cmdlet belongs to
.ROLE
   The role this cmdlet belongs to
.FUNCTIONALITY
   The functionality that best describes this cmdlet
#>
<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
#>
# Read Settings from XML
[xml]$Global:Settings = Get-Content .\ImageFactoryV2.xml
$Global:DeploymentShare = $Global:Settings.Settings.DeploymentShare
$Global:StartUpRAM = 1024*1024*1024*$Global:Settings.Settings.StartUpRAM
$Global:VHDSize = 1024*1024*1024*$Global:Settings.Settings.VHDSize
$Global:SWitchName = $Global:Settings.Settings.HyperVHostNetwork
$Global:VMLocation = $Global:Settings.Settings.HyperVStorage
$Global:HyperVISOFolder = $Global:Settings.Settings.HyperVISOFolder
$Global:TaskSequenceFolder = $Global:Settings.Settings.TaskSequenceFolder
$Global:VLANID = $Global:Settings.Settings.VLANID

function New-AutoGenRefImages{
    Process
    {

    #Connect to MDT:
    Add-PSSnapIn Microsoft.BDD.PSSnapIn -ErrorAction Stop
    Remove-PSDrive MDT -ErrorAction SilentlyContinue
    $Null = New-PSDrive -Name MDT -PSProvider MDTProvider -Root $Global:DeploymentShare -Force
    $MDTSettings = Get-ItemProperty MDT:
    $ISO = $MDTSettings.’Boot.x86.LiteTouchISOName’

    #Check if ISO exists
    $ISOFileExist = Test-Path "$($Global:DeploymentShare)\Boot\$($ISO)"  -ErrorAction SilentlyContinue
        If($ISOFileExist -like ‘False’){
        Write-Host "Unable to find ISO, exit"
        BREAK
        }

    #Create Folders
    $Null = New-Item -Path $HyperVISOFolder -ItemType Directory -Force -ErrorAction SilentlyContinue

    #Copy the BootImage from MDTServer to Hyper-VHost
    Copy-Item "$($Global:DeploymentShare)\Boot\$($ISO)" $Global:HyperVISOFolder\ -Container -Force -ErrorAction Stop

    #Check if ISO exists
    $ISOFileExist = Test-Path ($Global:HyperVISOFolder + "\" + $ISO) -ErrorAction Stop
        If($ISOFileExist -like ‘False’){
        Write-Host "Unable to find ISO, exit"
        BREAK
        }

    #Check if VMSwitch on host exists
    $VMSwitchExist = Get-VMSwitch -Name $Global:SWitchName -ErrorAction SilentlyContinue
        If($VMSwitchExist.Name -ne $Global:SWitchName){
        Write-Host "Unable to find VMSwitch, exit"
        BREAK
        }

    #Create the VMs
    $RefTS = Get-ChildItem $Global:TaskSequenceFolder -Recurse
        Foreach($TS in $RefTS){

        #Set VMName to ID
        $VMName = $TS.ID

        #Check if VM exists
        $VMexist = Get-VM -Name $VMName -ErrorAction SilentlyContinue
            If($VMexist.Name -like $VMName){
            Write-Host "VM already exist, exit"
            BREAK
            }

        $VM = New-VM –Name $VMname –MemoryStartupBytes $Global:StartUpRAM -SwitchName $Global:SWitchName -NewVHDPath "$Global:VMLocation\$VMName\Virtual Hard Disks\$VMName.vhdx" -NewVHDSizeBytes $Global:VHDSize -Path $Global:VMLocation
        Add-VMDvdDrive -VM $VM -Path ($Global:HyperVISOFolder + "\" + $ISO)

        #Set
        Get-VM -Name $VMName | Set-VMProcessor -CompatibilityForMigrationEnabled $True
        Get-VM -Name $VMName | Set-VMProcessor -Count 2
            If($Global:VLANID -notlike "0"){
            Get-VM -Name $VMName | Get-VMNetworkAdapter | Set-VMNetworkAdapterVlan -Access -VlanId $VLANID
            }
        }
        Foreach($TS in $RefTS){
        #Set VMName to ID
        $VMName = $TS.ID
        $VM = Get-VM -Name $VMName
        $VM | Start-VM
        Start-Sleep "10"
        while($VM.State -like "Running"){
        Write-Output "Waiting for $VMName to finish."
        Start-Sleep "120"}
        Write-Output "$VMName is done, checking for more"
        }
    }
}
function Remove-AutoGenRefImages{
    Process
    {

    #Connect to MDT:
    $Null = Add-PSSnapIn Microsoft.BDD.PSSnapIn -ErrorAction SilentlyContinue
    Remove-PSDrive MDT -ErrorAction SilentlyContinue
    $Null = New-PSDrive -Name MDT -PSProvider MDTProvider -Root $Global:DeploymentShare -Force
    $MDTSettings = Get-ItemProperty MDT:
    $ISO = $MDTSettings.’Boot.x86.LiteTouchISOName’

        #Remove the VMs
        $RefTS = Get-ChildItem $Global:TaskSequenceFolder -Recurse
        Foreach($TS in $RefTS){

            #Set VMName to ID
            $VMName = $TS.ID

            #Check if VM exists
            $VMexist = Get-VM -Name $VMName -ErrorAction SilentlyContinue
            If($VMexist.Name -like $VMName){
            Write-Host "Removing $VMName"
            $VMToRemove = Get-VM -Name $VMName
            $FolderPath = $VMToRemove.path
            if($VMToRemove.state -like "Running"){Stop-VM $VMToRemove -Force}
            $VMToRemove | Remove-VM -Force
            $FolderPath | Remove-Item -Force -Recurse
            }
        }
    }
}
function New-RefImage{
    [CmdletBinding()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=0)]
        [String]
        $TaskSequenceID
    )
   
    Process
    {

    #Connect to MDT:
    Add-PSSnapIn Microsoft.BDD.PSSnapIn -ErrorAction Stop
    Remove-PSDrive MDT -ErrorAction SilentlyContinue
    $Null = New-PSDrive -Name MDT -PSProvider MDTProvider -Root $Global:DeploymentShare -Force
    $MDTSettings = Get-ItemProperty MDT:
    $ISO = $MDTSettings.’Boot.x86.LiteTouchISOName’

    #Check if ISO exists
    $ISOFileExist = Test-Path "$($Global:DeploymentShare)\Boot\$($ISO)"  -ErrorAction SilentlyContinue
        If($ISOFileExist -like ‘False’){
        Write-Host "Unable to find ISO, exit"
        BREAK
        }

    #Create Folders
    $null = New-Item -Path $HyperVISOFolder -ItemType Directory -Force -ErrorAction SilentlyContinue

    #Copy the BootImage from MDTServer to Hyper-VHost
    Copy-Item "$($Global:DeploymentShare)\Boot\$($ISO)" $Global:HyperVISOFolder\ -Container -Force -ErrorAction Stop

    #Check if ISO exists
    $ISOFileExist = Test-Path ($Global:HyperVISOFolder + "\" + $ISO) -ErrorAction Stop
        If($ISOFileExist -like ‘False’){
        Write-Host "Unable to find ISO, exit"
        BREAK
        }

    #Check if VMSwitch on host exists
    $VMSwitchExist = Get-VMSwitch -Name $Global:SWitchName -ErrorAction SilentlyContinue
        If($VMSwitchExist.Name -ne $Global:SWitchName){
        Write-Host "Unable to find VMSwitch, exit"
        BREAK
        }

    #Check if VM exists
    $VMexist = Get-VM -Name $VMName -ErrorAction SilentlyContinue
        If($VMexist.Name -like $VMName){
        Write-Host "VM already exist, exit"
        BREAK
        }

    $VM = New-VM –Name $TaskSequenceID –MemoryStartupBytes $Global:StartUpRAM -SwitchName $Global:SWitchName -NewVHDPath "$Global:VMLocation\$TaskSequenceID\Virtual Hard Disks\$TaskSequenceID.vhdx" -NewVHDSizeBytes $Global:VHDSize -Path $Global:VMLocation
    Add-VMDvdDrive -VM $VM -Path ($Global:HyperVISOFolder + "\" + $ISO)

    #Set
    Get-VM -Name $TaskSequenceID | Set-VMProcessor -CompatibilityForMigrationEnabled $True -VM $VM

    #Set
    Get-VM -Name $TaskSequenceID | Set-VMProcessor -Count 2

    #Set
        If($VLANID -notlike "0"){
        Get-VM -Name $TaskSequenceID | Get-VMNetworkAdapter | Set-VMNetworkAdapterVlan -Access -VlanId $VLANID
        }

    }

}
function Remove-RefImage{
    [CmdletBinding()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=0)]
        [String]
        $VMName
    )
    Process
    {

    #Check if VM exists
    $VMexist = Get-VM -Name $VMName -ErrorAction SilentlyContinue
        If($VMexist.Name -like $VMName){
        Write-Host "Removing $VMName"
        $VMToRemove = Get-VM -Name $VMName
        $FolderPath = $VMToRemove.path
        if($VMToRemove.state -like "Running"){Stop-VM $VMToRemove -Force}
        $VMToRemove | Remove-VM -Force
        $FolderPath | Remove-Item -Force -Recurse
        }
    }
}
function Get-RefTaskSequence{
    Process
    {

    #Connect to MDT:
    Add-PSSnapIn Microsoft.BDD.PSSnapIn -ErrorAction Stop
    Remove-PSDrive MDT -ErrorAction SilentlyContinue
    $Null = New-PSDrive -Name MDT -PSProvider MDTProvider -Root $Global:DeploymentShare -Force
    $MDTSettings = Get-ItemProperty MDT:
    $ISO = $MDTSettings.’Boot.x86.LiteTouchISOName’

        #Get TS
        $RefTS = Get-ChildItem $Global:TaskSequenceFolder -Recurse
        Foreach($TS in $RefTS){
            New-Object PSObject -Property @{
            TaskSequenceID = $TS.ID
            Name = $TS.Name
            Comments = $TS.Comments
            Version = $TS.Version
            Enabled = $TS.enable
            LastModified = $TS.LastModifiedTime
            }
        }
    }
}

Export-ModuleMember -function *

/mike

Show more