When you have few Hyper-V hosts and when you have not the System Center products, you have to deploy your nodes from a USB stick or from PXE services such as WDS. These deployment methods often imply manual steps to configure the operating system. However, it is possible to automate most of the steps thanks to deployment tools (part of ADK) and some PowerShell scripts. This topic shows you how I deploy Hyper-V from USB stick with unattended file and scripts.
Requirements
To follow this topic, you need Windows Assessment and Deployment Kit (ADK). To make this topic, I have deployed the following feature:
You need also the ISO of Windows Server 2016. I have copied the install.wim (folder Sources of the ISO) on my local drive.
Prepare the USB stick
To prepare the USB stick, I use Rufus. Once you have run the tool, just select your ISO file, the USB Stick and the boot mode (Bios or UEFI). This topic is based on an UEFI configuration.
Prepare the unattended file
Once you have installed deployment tools, you can open it from start menu. Then click file | select Windows Image.
Next, browse your local drive to select install.wim.
Select the edition you want. For Hyper-V, I choose Windows Server 2016 Datacenter in Core edition.
Next create an answer file as below.
Select amd64_Microsoft-Windows-International-Core-WinPE (documentation here), and place it in 1. WindowsPE. Then specify language settings:
- InputLocale: Keyboard layout
- SystemLocale: system locale language
- UILanguage: User Interface language
- UserLocale: per-user settings for currency, time, numbers and so on
Next navigate to SetupUILanguage to specify the language used during Windows Setup.
Next, select amd64_Microsoft-Windows-Setup (documentation here) and place it in 1. WindowsPE.
Navigate to DiskConfiguration and right click on it and select add disk.
To create a partition, right click on CreatePartitions and select insert New CreatePartition.
To make the below configuration, I have followed this guide of Microsoft (because I have an UEFI):
Microsoft-Windows-Setup\DiskConfiguration\Disk | DiskID = 0 WillWipeDisk = true |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ CreatePartitions\CreatePartition | Order = 1 Size = 300 Type = Primary |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ CreatePartitions\CreatePartition | Order = 2 Size = 260 Type = EFI |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ CreatePartitions\CreatePartition | Order = 3 Size = 128 Type = MSR |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ CreatePartitions\CreatePartition | Extend = true Order = 4 Type = Primary |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ ModifyPartitions\ModifyPartition | Format = NTFS Label = WINRE Order = 1 PartitionID = 1 |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ ModifyPartitions\ModifyPartition | Format = FAT32 Label = System Order = 2 PartitionID = 2 |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ ModifyPartitions\ModifyPartition | Order = 3 PartitionID = 3 |
Microsoft-Windows-Setup\DiskConfiguration\Disk\ ModifyPartitions\ModifyPartition | Format = NTFS Label = HostOS Letter = C Order = 4 PartitionID = 4 |
Microsoft-Windows-Setup\ImageInstall\OSImage\InstallTo | DiskID = 0 PartitionID = 4 |
In a single install.wim, there are multiple Windows images. So, we have to specify from which image Windows will be deployed. It is a little tricky but we can retrieve this information with the following dism command:
Dism /Get-ImageInfo /ImageFile:<Path/to/install.wim>
Thanks to this command, we can list images in install.wim and the index. In the below screenshot, the Windows Server 2016 Datacenter in Core edition is the index 3.
So, in InstallFrom node, I add the following metadata:
- Key: /IMAGE/INDEX
- Value: 3
Next navigate to ProductKey node (in UserData) and specify a product key.
In UserData node, set AcceptEula to true and provide FullName and Organization name.
Next add amd64_Microsoft-Windows-Shell-Setup (documentation here) to specialize and oobeSystem section.
In Specialize section, specify the timezone and the computer name. You can find here the allowed timezone format.
Move to oobeSystem section. In UserAccounts | AdministratorPassword node, specify an Administrator password as below
In the above screenshot, I configure an Auto Logon that will run 5 times. I make this configuration because later, I’ll run a PowerShell script with some reboots. We will see that in the next section.
In FirstLogonCommands, I add a SynchronousCommand. I specify the script that will customize my operating system. This cmd script calls a PowerShell script. We will see that in the next section.
Next add amd64_Microsoft-Windows-TerminalServices-LocalSessionManager (documentation here). I set fDenyTSConnections to false to enable the RDP connection.
Once you have finished to edit the answer file, you can save it on USB stick. You must name it autounattend.xml.
Post deployment scripts
In the USB Stick, I create a folder called Deploy. In this folder I have also created a folder called Binaries and Agent. In binaries, I have copied the drivers (as Dell, Mellanox and so on). In Deploy folder I have the following script:
- ConfigureOS.cmd
- Autodeploy.ps1
- NodeConfiguration.xml
ConfigureOS.cmd
This script creates c:\temp\Deploy folder and copy the content of Deploy from USB stick to c:\temp\Deploy. Then the script AutoDeploy.ps1 is run.
mkdir c:\temp\Deploy xcopy D:\Deploy\* C:\temp\Deploy /H /F /E PowerShell -file c:\temp\deploy\AutoDeploy.ps1
NodeConfiguration.xml
This XML file contains network configurations (IP Address, netmask, Active Directory and so on). This XML is called in AutoDeploy.ps1.
<?xml version="1.0" encoding="UTF-8" /> <NodeConfiguration> <Network vSwitchName="SW-1G"> <Management name="MGMT-22" ipaddr="10.138.22.12" netmask="24" gateway="10.138.22.1" dns="10.139.16.15,10.138.23.15" type="Untagged" vlanid=""/> <Cluster name="Cluster-100" ipaddr="192.168.100.12" netmask="24" type="Access" vlanid="100"/> </Network> <ActiveDirectory name="homecloud.net" /> </NodeConfiguration>
AutoDeploy.ps1
The AutoDeploy.ps1 script install drivers, features, agent, configure networks, MPIO and join Active Directory. A scheduled task is created to run the script after each reboot. The script knows where it is after each reboot thanks to step file. The script reads the step file and regarding the value, it runs a part of the script.
# Variables $DeployPkg = "C:\temp\Deploy" $ScriptDir = Split-Path $script:MyInvocation.MyCommand.Path $StepFile = $($DeployPkg + "\step.cfg") $DellPkg = $DeployPkg + "\Binaries\Dell\suu.cmd" $XMLPath = $DeployPkg + "\NodeConfiguration.xml" if ((Test-Path $XMLPath) -like $False){ Write-Host "Can't find the XML file located to $XMLPath. Exiting" -ForegroundColor Red Exit } # Get XML content $XML = Get-Content $XMLPath Write-Host "The AutoDeploy script is located in $ScriptDir" -ForegroundColor Green Write-Host "The step file is $StepFile" -ForegroundColor Green #### INITIALIZATION OF STEP FILE #### if ((Test-Path $StepFile) -like $False){ Write-Host "The step file doesn't exist. Creating it with 0 value" -ForegroundColor Green Set-Content -LiteralPath $StepFile -Value 0 } # Get Step $Step = Get-Content $StepFile Write-Host "Current Step: 0. Deploying Dell Drivers and firmware" -ForegroundColor Green #### DELL DRIVERS INSTALLATION R630 + FIRMWARE #### if ($Step -like 0){ # Set a schedule task to run this script on each OS start $action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-command "& C:\temp\Deploy\Autodeploy.ps1"' $trigger = New-ScheduledTaskTrigger -Atlogon Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "AutoDeploy" -Description "Server Deployment" # Test if dell drivers are found if ((Test-Path $DellPkg) -like $False){ Write-Host "Can't find Dell package. Please update the variable DellPkg. Exiting." -ForegroundColor Red Exit } #Change the Power schema to high performance Write-Host "Change power schema to high performance" -ForegroundColor Green POWERCFG.EXE /S SCHEME_MIN #Change the value of step file Set-Content -LiteralPath $StepFile -Value 1 # Driver installation cmd /c "$DellPkg -e" Restart-Computer } #### ROLE AND FEATURE INSTALLATION #### if ($Step -like 1){ Write-Host "Installing the current Windows Roles and Features:" -ForegroundColor Green Write-Host " - Hyper-V + PowerShell cmdlets" -ForegroundColor Blue Write-Host " - Failover Clustering + PowerShell cmdlets" -ForegroundColor Blue Write-Host " - MPIO" -ForegroundColor Blue Write-Host " - Active Directory PowerSHell cmdlets" -ForegroundColor Blue Install-WindowsFeature Hyper-V, Failover-Clustering, MultiPath-IO, RSAT-CLustering-Powershell, Hyper-V-PowerShell, RSAT-AD-PowerShell Set-Content -LiteralPath $StepFile -Value 2 Restart-Computer -Force } #### NETWORK CONFIGURATION, MPIO AND AD #### if ($Step -like 2){ $SwitchName = $XML.NodeConfiguration.Network.vSwitchName $MGMTNicName = $XML.NodeConfiguration.Network.Management.Name $MGMTNicIP = $XML.NodeConfiguration.Network.Management.ipaddr $MGMTNicMask = $XML.NodeConfiguration.Network.Management.netmask $MGMTNicGW = $XML.NodeConfiguration.Network.Management.gateway $MGMTNicDNS = $XML.NodeConfiguration.Network.Management.dns $MGMTNicvType = $XML.NodeConfiguration.Network.Management.type $MGMTNicVlan = $XML.NodeConfiguration.Network.Management.vlanid $ClusNicName = $XML.NodeConfiguration.Network.Cluster.Name $ClusNicIP = $XML.NodeConfiguration.Network.Cluster.ipaddr $ClusNicMask = $XML.NodeConfiguration.Network.Cluster.netmask $ClusNicvType = $XML.NodeConfiguration.Network.Cluster.type $ClusNicVlan = $XML.NodeConfiguration.Network.Cluster.vlanid # Creating vSwitch (SET) Write-Host "Creating Switch Embedded Teaming (name: $SwitchName) vSwitch with all physical NIC" -ForegroundColor Green New-VMSwitch -Name $SwitchName -NetAdapterName NIC1, NIC2, NIC3, NIC4 -EnableEmbeddedTeaming $True -AllowManagementOS $False > $Null # Creating vNIC Management Write-Host "Creating Management NIC (NIC name: $MGMTNicName)" -ForegroundColor Green Add-VMNetworkAdapter -SwitchName $SwitchName -ManagementOS -Name $MGMTNicName if ($MGMTNicvType -like "Untagged"){ Write-Host "Configure vNIC $MGMTNicName to untagged" -ForegroundColor Green Set-VMNetworkAdapterVLAN -ManagementOS -VMNetworkAdapterName $MGMTNicName -Untagged } Elseif ($ClusNicvType -like "Access"){ Write-Host "Configure vNIC $MGMTNicName to tagged (VID: $MGMTNicVlan)" -ForegroundColor Green Set-VMNetworkAdapterVLAN -ManagementOS -VMNetworkAdapterName $MGMTNicName -Access -VlanId $MGMTNicVlan } # Creating vNIC Cluster (Hearbeat + Live-Migration) Write-Host "Creating Cluster NIC (NIC name: $ClusNicName)" -ForegroundColor Green Add-VMNetworkAdapter -SwitchName $SwitchName -ManagementOS -Name $ClusNicName Write-Host "Tagging the $ClusNicName NIC" -ForegroundColor Green if ($ClusNicvType -like "Untagged"){ Write-Host "Configure vNIC $ClusNicName to untagged" -ForegroundColor Green Set-VMNetworkAdapterVLAN -ManagementOS -VMNetworkAdapterName $ClusNicName -Untagged } Elseif ($ClusNicvType -like "Access"){ Write-Host "Configure vNIC $ClusNicName to tagged (VID: $ClusNicVlan)" -ForegroundColor Green Set-VMNetworkAdapterVLAN -ManagementOS -VMNetworkAdapterName $ClusNicName -Access -VlanId $ClusNicVlan } # Disable VMQ because 1GB NIC Write-Host "Disabling VMQ on all NICs" -ForegroundColor Green Disable-NetAdapterVMQ -Name * # Enable Jumbo Frame on all NICs Write-Host "Enabling JumboFrame on all vNICs" -ForegroundColor Green Get-NetAdapterAdvancedProperty -Name * -RegistryKeyword "*jumbopacket" | Set-NetAdapterAdvancedProperty -RegistryValue 9014 # Set IP addresses Write-Host "Set IP Address on vNICs $MGMTNicName ($MGMTNicIP/$MGMTNicMask, GW: $MGMTNicGW)" -ForegroundColor Green New-NetIPAddress -InterfaceAlias "vEthernet ($MGMTNicName)" -IPAddress $MGMTNicIP -PrefixLength $MGMTNicMask -Type Unicast -DefaultGateway $MGMTNicGW | Out-Null Write-Host "Set DNS on $MGMTNicName (DNS: $MGMTNicDNS)" -ForegroundColor Green Set-DnsClientServerAddress -InterfaceAlias "vEthernet ($MGMTNicName)" -ServerAddresses $MGMTNicDNS | Out-Null Write-Host "Set IP Address on vNICs $ClusNicName ($ClusNicIP/$ClusNicMask)" -ForegroundColor Green New-NetIPAddress -InterfaceAlias "vEthernet ($ClusNicName)" -IPAddress $ClusNicIP -PrefixLength $ClusNicMask -Type Unicast | Out-Null #Disable DNS registration of Storage and Cluster network adapter Write-Host "Disabling register in DNS for $ClusNicName" -ForegroundColor Green Set-DNSClient -InterfaceAlias *$ClusNicName* -RegisterThisConnectionsAddress $False #### CONFIGURE MPIO #### Write-Host "Activate SAS claim for MPIO" -ForegroundColor Green Enable-MSDSMAutomaticClaim -BusType SAS #### ADD TO DOMAIN #### Write-Host "Add computer to the domain $($XML.NodeConfiguration.ActiveDirectory.name)" -ForegroundColor Green New-MSDSMSupportedHW -allApplicable $Credential = Get-Credential -Message "Credential for $($XML.NodeConfiguration.ActiveDirectory.name)" Add-Computer -DomainName $($XML.NodeConfiguration.ActiveDirectory.name) -Credential $Credential Set-Content -LiteralPath $StepFile -Value 3 Restart-Computer } #### INSTALLATION AGENT #### if ($Step -like 3) { # Add here the Agent installation ################################ Set-Content -LiteralPath $StepFile -Value 4 Restart-Computer } #### REMOVE AUTOLOGON , CHANGE ADMIN PWD #### if ($Step -like 4){ Write-Host "Delete automatic run script at startup" -ForegroundColor Green Unregister-ScheduledTask AutoDeploy Restart-Computer }
Conclusion
This topic shows you an example of how we can automate the Hyper-V host’s deployment with free tools. If you have not System Center, you can use this method. With automation, you can get a consistent deployment more easily than manual deployment.