A Free PowerShell Script to Configure a Hyper-V Host

Table of contents

Occasionally, I mention that I use saved scripts to auto-configure the Hyper-V hosts in my lab. Small, homegrown tools like this are perfect for smaller installations that can’t justify enterprise tools like VMM and SCOM, which can handle a lot of situations like this automatically. However, I never shared any of those tools because they just weren’t very high quality. They did what I needed, but I understood them and could fix any problems easily enough that I never bothered to make any of the scripts particularly resilient.

Until now.

What follows is a thoroughly retooled and streamlined version of all the separate little things I’ve built for myself over the past few years to facilitate host rebuilds, and configure a Hyper-V Host. It’s not perfectly bullet-proof and I did not build in any recovery problems from errors. However, if set correctly, it should leave you with a perfectly functional host.

Script Discussion

Normally, I post the script first so that you can copy/paste it and be on your way. This one requires you to understand some things up front. First: it is not in a usable state. You will need to modify it before you can use it. When I sat down to work something up for you to use, I had a lot of choices on the way that it could have gone. What I decided was that it needed to be a one-stop experience — no digging around in other files, no organizing things, no re-tooling Windows images, no running one tool in order to run another tool, etc. That’s because I’ve been in a small business environment and I know that “automation” processes that take as much effort as just doing the work by hand are not going to be followed. So, if you’re looking at anything in here and wondering why I did it that way, the reason is almost certainly related to this philosophy.

Requirements

To use this script, you need:

  • A way to reinstall Windows Server 2012 R2 or Hyper-V Server 2012 R2 on the target host
  • A way to edit a PowerShell file. PowerShell ISE, Visual Studio 2015 Community Edition with PowerShell Tools, and Notepad are all freely available solutions that will suffice. The first two might serve you better because I seeded the beginning of the file with a table of contents with line numbers, and Notepad does not display line numbers. As you use the file, the line numbers will likely be changed anyway, so that might not help much. I used another technique that will work just fine with CRTL+F in all three tools. You’ll see that in the instructions.

Nice to have, but not required, is the raw files for any drivers that you want to install.

I choose to install Windows and Hyper-V Server my physical hosts by using an 8GB USB stick. This is because I can drop this file and anything else that I need right on it without modifying an ISO file. I can also tinker with it, or delete anything on it, without any effort. I found these directions once upon a time on a Stack Overflow site but can no longer find my way back to the exact answer for proper crediting, so apologies to whomever I stole this from. To prepare a USB disk to be bootable and hold Windows installation files, use DISKPART.EXE with the following commands:

diskpart
list disk (find the number of the disk that is the USB disk; it's probably the one measured in MB instead of GB)
select disk N (where N is the disk you want to format)
clean
create partition primary
active
format fs=ntfs quick
assign

Once the USB stick is prepared, copy the entire contents of a Windows/Hyper-V Server ISO to it.

Next, follow the directions in the script. If you do not follow the directions, I left a reminder for you. Place the edited file on the USB stick. If you have drivers, place them as well. The script as-written looks for its drivers in sub-folders of a “Drivers” folder but you can do whatever you want.

The script as you see it is for one of my systems. Your task will be to change it for one of your systems.

It performs these operations in order:

  1. Very basic host configuration, like a name change.
  2. Installs drivers. Feed it .INF files.
  3. Enables roles and features. Hyper-V and MPIO are handled automatically.
  4. Reboots. You must have supplied a valid local administrator account in the beginning of the file or the whole thing will stop here. Remember that enabling the Hyper-V role requires two restarts.
  5. Configures physical adapters. My hosts do not support Consistent Device Naming (CDN) so I configure them by MAC. Tinker with the Get-NetAdapter lines to suit your system.
  6. Configures network team(s). Comment this section out as necessary.
  7. Configures virtual switches.
  8. Configures virtual network adapters for the management operating system.
  9. Joins the domain, if one is specified along with valid credentials for a domain account that has permissions to add computers to the domain.
  10. Reboots if domain join was successful.
  11. iSCSI is configured. I placed the iSCSI section here because you might have configured your target to expect particular initiators, and initiators for a host change when it joins a domain.
  12. Some Hyper-V defaults are set.
  13. Any customizations that you make are performed.

Everything that this script does is recorded to C:WindowsLogsRebuildHost.log. Absolutely read this file after each use.

I tried to make it very clear where you need to make changes and how they need to be made. Just follow my lead. The toughest spot will probably be the iSCSI section. There are so many possible ways to configure iSCSI that it’s tough to make an all-purpose script.

WARNING/DISCLAIMER: The following script is intended to be run on a freshly installed Windows Server or Hyper-V Server host. It will cause catastrophe-level changes if run on a functioning host, including but not limited to: all teams being deleted, all network information being lost, all virtual switches being deleted, and release of those sensitive pictures you have on your phone (just kidding on that last part). Neither Altaro Software nor I are responsible for any bad things that happen because of this script, especially if they happen because you didn’t read the instructions.

<# Rebuild script for host SVHV2 #>
<# Last Modified February 7, 2016 by Eric Siron #>
#requires -Version 4
#requires -RunAsAdministrator
param
(
	[Parameter()][String][ValidateSet('Start', 'Main', 'Finish')]$Action = 'Start'
)

<############################################
 Instructions for use

 Save this file with the name of the host you want to rebuild

 Search this file for instances of the following and change items as necessary: BEGIN Modifiable lines

 Table of contents for the file as originally written:
 Set general host information starting on line 39
 Set Windows features on line 137
 Set driver information on line 140
 Set physical adapter parameters starting on line 335
 Set teaming information starting on line 386
 Set virtual switch parameters starting on line 413
 Set virtual adapter parameters starting on line 477
 Set iSCSI information on line 659
 Any other customizations begin on line 731

 Remove or comment out line 744
 ############################################>

 Set-StrictMode -Version Latest

<# Begin script variable definitions #>
$MinimalQoSWeight = 1
<# End script variable definitions #>

<# Begin general host information #>
## These are globals and will be available in all functions ##
#### BEGIN Modifiable lines ####
$ComputerName = 'svhv2'
$TimeZone = 'Central Standard Time'
$LocalUser = 'administrator' # this is a local user account that will restart the computer to apply changes. the account must exist before the script is run
$LocalPassword = 'P@ssw0rd'
# domain information -- if $DomainName is empty, the entire domain section is skipped
$DomainName = 'siron.int' # leave empty to skip domain configuration
$DomainUser = 'adduser' # this account only needs sufficient privileges to add computers to the domain
$DomainPassword = '2easy2guess!'
$DomainOU = 'Servers: Hyper-V Hosts' # make empty for default Computers OU or pre-staged account. ensure $DomainUser can add computer accounts here
# Hyper-V settings
$DefaultVMPath = 'C:LocalVMs' # make empty to leave at default
$DefaultVHDPath = 'C:LocalVMsVirtual Hard Disks' # make empty to leave at default
$TeamVSwitchName = 'vSwitchTeam' # to avoid breaking line numbering, append additional entries on this line like: ; $TeamDMZName = 'DMZTeam'; $TeamWhyName = 'UnneededTeam'
# storage settings
$UseMPIO = $true
#### END Modifiable lines ####
<# End general host information #>

<# Begin log file configuration #>
$LogFile = '{0}{1}{2}' -f $env:SystemRoot, 'Logs', 'RebuildHost.log' # change the final string to change the log file's name
if(-not (Test-Path -Path $LogFile -PathType Leaf))
{
	try
	{
		$HideOutput = New-Item -Path $LogFile -ItemType File -ErrorAction Stop
	}
	catch
	{
		Write-Warning -Message ('Log file "{0}" not created due to error: {1}' -f $LogFile, $_.Exception.Message)
	}
}

function New-LogLine
{
	param([Parameter(Mandatory=$true)][String]$Value)
	Add-Content -Path $LogFile -Value $Value
}

function New-LogWarning
{
	param([Parameter(Mandatory=$true)][String]$Value)
	Add-Content -Path $LogFile -Value ('WARNING: {0}' -f $Value)
	Write-Warning -Message $Value
}

function New-LogError
{
	param([Parameter(Mandatory=$true)][String]$Value)
	Add-Content -Path $LogFile -Value ('ERROR: {0}' -f $Value)
	Write-Warning -Message $Value
}

function New-LogSeparatorLine
{
	New-LogLine -Value '***********************************************************'
}

function Restart-PartiallyRebuiltHost
{
	param(
		[Parameter(Mandatory=$true)][String]$NextAction
	)

	$JobCredential = New-Object -TypeName pscredential -ArgumentList ($LocalUser, (ConvertTo-SecureString -String $LocalPassword -AsPlainText -Force))
	$JobTrigger = New-JobTrigger -AtStartup -RandomDelay (New-TimeSpan -Seconds 30)
	$JobOptions = New-ScheduledJobOption -RunElevated -MultipleInstancePolicy StopExisting
	try
	{
		$IgnoreOutput = Register-ScheduledJob -Name 'RebuildHost' -Credential $JobCredential -FilePath $PSCommandPath -Trigger $JobTrigger -ScheduledJobOption $JobOptions -ArgumentList @($NextAction) -ErrorAction Stop
		New-LogLine -Value 'Built scheduled job to restart host and automatically resume script.'
		New-LogLine -Value 'Restarting computer...'
		Restart-Computer
	}
	catch
	{
		New-LogError -Value ('Unable to schedule the script continuation task: {0}' -f $_ex.Exception.Message)
		New-LogWarning -Value ('Manually restart the computer. After it restarts, open an elevated PowerShell prompt and run: {0} -{1}' -f $PSCommandPath, $NextAction)
	}
}

function Start-HostRebuild
{
	New-LogSeparatorLine
	New-LogLine -Value ('Configuration began on {0}' -f (Get-Date))
	New-LogSeparatorLine
	$RestartRequired = $false

	<# Begin basic host configuration #>
	tzutil /s $TimeZone
	New-LogLine -Value ('Time zone set to {0}' -f $TimeZone)

	$FeaturesToAdd = @('Hyper-V', 'RSAT-Hyper-V-Tools')
	if($UseMPIO)
	{
		$FeaturesToAdd += 'Multipath-IO'
	}

	#### BEGIN Modifiable lines ####
	# add other Windows features (Hyper-V and MPIO already added - use Get-WindowsFeature to discover exact names)
	$FeaturesToAdd += 'SNMP-Service'
	# drivers
	pnputil -i -a $PSScriptRootDriversBroadcomb57nd60a.inf
	pnputil -i -a $PSScriptRootDriversRealtekrt630x64.inf
	#### END Modifiable lines ####

	New-LogSeparatorLine
	New-LogLine -Value 'Beginning basic host configuration'

	if($env:COMPUTERNAME -ne $ComputerName)
	{
		Rename-Computer -NewName $ComputerName
		New-LogLine -Value ('Computer renamed to "{0}"' -f $ComputerName)
		$RestartRequired = $true
	}

	$InstallResult = Install-WindowsFeature -Name $FeaturesToAdd -IncludeAllSubFeature -IncludeManagementTools
	if($InstallResult.RestartNeeded -eq 'Yes')
	{
		New-LogLine -Value 'Hyper-V installed.'
		$RestartRequired = $true
	}

	<# End basic host configuration #>

	if($RestartRequired)
	{
		Restart-PartiallyRebuiltHost -NextAction 'Main'
	}
	else
	{
		Resume-HostRebuild
	}
}

function Resume-HostRebuild
{
	if(Get-ScheduledJob | Where-Object -Property Name -Value 'RebuildHost' -EQ)
	{
		Unregister-ScheduledTask -TaskName 'RebuildHost' -Confirm:$false
		New-LogLine -Value 'Restart successful. Deleting scheduled job.'
	}
	<# Begin clearing configuration #>
	Get-VMSwitch | Remove-VMSwitch -Force -Confirm:$false
	New-LogLine -Value 'Any existing virtual switches removed.'

	Get-NetLbfoTeam | Remove-NetLbfoTeam
	New-LogLine -Value 'Any existing teams cleared.'

	Get-NetIPAddress | where -Property InterfaceAlias -NotMatch 'Loopback' -Confirm:$false
	New-LogLine -Value 'Any existing IP information cleared.'

	Get-NetAdapter | Set-DnsClientServerAddress -ResetServerAddresses -ErrorAction SilentlyContinue
	New-LogLine -Value 'Any existing DNS server address information cleared.'

	try
	{
		Get-NetRoute -DestinationPrefix 0.0.0.0/0 -ErrorAction Stop | Remove-NetRoute -Confirm:$false -ErrorAction Stop
		New-LogLine -Value 'Any existing default gateway information cleared.'
	}
	catch
	{
		New-LogWarning -Value ('Error while detecting or removing gateways. Errors are expected when no gateways exist. Message: ' -f $_.Exception.Message)
	}


	<# End clearing configuration #>

	<# Begin adapter configuration utility functions; no user serviceable parts inside #>
	function Configure-Adapter
	{
		param(
			[Parameter(Mandatory=$true)][CimInstance]$NetAdapter,
			[Parameter()][String]$NewName = '',
			[Parameter()][String]$IPAddress = '',
			[Parameter()][Int32]$PrefixLength = '',
			[Parameter()][String]$DefaultGateway = '',
			[Parameter()][String[]]$DNSServers = @(),
			[Parameter()][Switch]$RegisterInDns,
			[Parameter()][Switch]$EnableVmq
		)
		if($NetAdapter.CimClass.CimClassName -eq 'MSFT_NetAdapter')
		{
			if($NewName)
			{
				Rename-NetAdapter -InputObject $NetAdapter -NewName $PhysicalAdapterConfiguration.Name
				New-LogLine -Value ('Renamed to {0}' -f $PhysicalAdapterConfiguration.Name)
			}

			if($IPAddress)
			{
				$IPErrored = $false
				try
				{
					$IgnoreOutput = New-NetIPAddress -InterfaceIndex $NetAdapter.InterfaceIndex -IPAddress $IPAddress -PrefixLength $PrefixLength -ErrorAction Stop
				}
				catch
				{
					if($_.Exception.Message.Contains('Inconsistent parameters'))
					{
						# this message appears sometimes when IP information has already been set on an object. it does not affect IP assignment
						New-LogWarning -Value ('Error while assigning IP {0}. Usually this message can be ignored: {1}' -f $IPAddress, $_.Exception.Message)
					}
					else
					{
						New-LogError -Value ('Error while assigning IP {0}: {1}' -f $IPAddress, $_.Exception.Message)
						$IPErrored = $true
					}
				}
				if(-not $IPErrored)
				{
					New-LogLine -Value ('IP address set to {0} with a prefix length of {1}' -f $IPAddress, $PrefixLength)
				}
			}

			if($DefaultGateway)
			{
				$IgnoreOutput = New-NetRoute -InterfaceIndex $NetAdapter.InterfaceIndex -DestinationPrefix 0.0.0.0/0 -NextHop $DefaultGateway
				New-LogLine -Value ('Default gateway set to {0}' -f $DefaultGateway)
			}

			if($DNSServers.Count)
			{
				Set-DnsClientServerAddress -InterfaceIndex $NetAdapter.InterfaceIndex -ServerAddresses $DNSServers
				New-LogLine -Value ('DNS Servers set to {0}' -f ([String]::Join(', ', $PhysicalAdapterConfiguration.DNSServers)))
			}

			try
			{
				Set-DnsClient -InterfaceIndex $NetAdapter.InterfaceIndex -RegisterThisConnectionsAddress $RegisterInDns -ErrorAction Stop
				New-LogLine -Value ('Adapter will register in DNS: {0}' -f $PhysicalAdapterConfiguration.RegisterInDns)
			}
			catch
			{
				New-LogWarning -Value ('Error generated while setting adapter to not register in DNS. These errors are typically benign: {0}' -f $_.Exception.Message)
			}

			New-LogLine -Value 'Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.'
			$NewVMQValue = 0
			if($EnableVmq)
			{
				$NewVMQValue = 1
			}
			$VMQValues = Get-NetAdapterAdvancedProperty -InterfaceDescription $NetAdapter.InterfaceDescription | Where-Object -Property 'DisplayName' -Value 'v(irtual)W*m(achine)W*q(ueue)?' -Match
			foreach ($VMQRegistryValue in $VMQValues)
			{
				try
				{
					Set-NetAdapterAdvancedProperty -InputObject $VMQRegistryValue -RegistryValue $NewVMQValue -ErrorAction Stop
					New-LogLine -Value ('Set property "{0}" to {1}' -f $VMQRegistryValue.DisplayName, $NewVMQValue)
				}
				catch
				{
					New-LogError -Value ('Set property "{0}" to {1}. Error message: {2}' -f $VMQRegistryValue.DisplayName, $NewVMQValue, $_.Exception.Message)
				}
			}
		}
		else
		{
			New-LogWarning -Value 'Object supplied for network configuration is not a network adapter'
		}
	}
	<# End adapter configuration utility functions #>

	<# Begin physical adapter configuration #>
	New-LogSeparatorLine
	New-LogLine -Value 'Beginning configuration of physical adapters.'

	function New-PhysicalAdapterDefinition
	{
		param(
			[Parameter(Mandatory=$true)][String]$Name,
			[Parameter(Mandatory=$true)][String]$MacAddress,
			[Parameter()][String]$IPAddress = '',
			[Parameter()][Int32]$PrefixLength = 24,
			[Parameter()][String]$DefaultGateway = '',
			[Parameter()][String[]]$DNSServers = @(),
			[Parameter()][Switch]$RegisterInDns = $false,
			[Parameter()][String]$TeamName = '',
			[Parameter()][Switch]$EnableVmq
		)
		$AdapterObject = New-Object PSObject
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'Name' -Value $Name
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'MacAddress' -Value $MacAddress
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'IPAddress' -Value $IPAddress
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'PrefixLength' -Value $PrefixLength
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'DefaultGateway' -Value $DefaultGateway
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'DNSServers' -Value $DNSServers
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'RegisterInDns' -Value $RegisterInDns
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'TeamName' -Value $TeamName
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'EnableVmq' -Value $EnableVmq
		$AdapterObject
	}

	$PhysicalAdapterConfigurations = @()

	#### BEGIN Modifiable lines ####
	# Use the following lines to configure adapters. To leave an adapter unconfigured, do not include an entry. Add or remove entries as necessary.
	$PhysicalAdapterConfigurations += New-PhysicalAdapterDefinition `
		-Name 'Onboard' `
		-MacAddress 'A0-B3-CC-E4-F5-5D' `
		-IPAddress '192.168.25.11' `
		-PrefixLength 24 `
		-DefaultGateway '192.168.25.1' `
		-DNSServers @('192.168.25.5', '192.168.25.6') `
		-RegisterInDns `
		-TeamName '' `
		-EnableVmq:$false # this entry is only present to show that it exists. If not specified, VMQ is disabled. To enable, just use -EnableVmq

	$PhysicalAdapterConfigurations += New-PhysicalAdapterDefinition -Name 'PBR' -MacAddress '00-0A-CD-20-DB-0C' -TeamName $TeamVSwitchName
	$PhysicalAdapterConfigurations += New-PhysicalAdapterDefinition -Name 'PBL' -MacAddress '00-0A-CD-20-DA-F6' -TeamName $TeamVSwitchName
	$PhysicalAdapterConfigurations += New-PhysicalAdapterDefinition -Name 'PTL' -MacAddress '00-0A-CD-20-DB-0D' -IPAddress '192.168.50.11' -PrefixLength 24
	$PhysicalAdapterConfigurations += New-PhysicalAdapterDefinition -Name 'PTR' -MacAddress '00-0A-CD-20-DA-F7' -IPAddress '192.168.51.11' # if not specified, a PrefixLength of 24 is assumed
	#### END Modifiable lines ####

	$PhysicalAdapterList = Get-NetAdapter
	foreach($PhysicalAdapterConfiguration in $PhysicalAdapterConfigurations)
	{
		New-LogSeparatorLine
		New-LogLine -Value ('Configuring physical adapter with MAC address {0}' -f $PhysicalAdapterConfiguration.MacAddress)
		$PhysicalAdapter = $PhysicalAdapterList | Where-Object -Property 'MacAddress' -Value $PhysicalAdapterConfiguration.MacAddress -EQ
		if($PhysicalAdapter)
		{
			if(-not([String]::IsNullOrEmpty($PhysicalAdapterConfiguration.IPAddress)) -and $PhysicalAdapterConfiguration.TeamName)
			{
				New-LogWarning -Value ('Adapter "{0}" has IP information and is marked to be joined to team "{1}". IP information will be lost.' -f $PhysicalAdapterConfiguration.Name, $PhysicalAdapterConfiguration.TeamName)
			}
			Configure-Adapter -NetAdapter $PhysicalAdapter -NewName $PhysicalAdapterConfiguration.Name -IPAddress $PhysicalAdapterConfiguration.IPAddress -PrefixLength $PhysicalAdapterConfiguration.PrefixLength -DefaultGateway $PhysicalAdapterConfiguration.DefaultGateway -DNSServers $PhysicalAdapterConfiguration.DNSServers -RegisterInDns:$PhysicalAdapterConfiguration.RegisterInDns -EnableVmq:$PhysicalAdapterConfiguration.EnableVmq
		}
		else
		{
			New-LogError -Value ('Adapter not found with MAC address {0}' -f $PhysicalAdapterConfiguration.MacAddress)
		}
	}
	<# End physical adapter configuration #>

	<# Begin teaming configuration #>
	New-LogSeparatorLine
	New-LogLine -Value 'Beginning network teaming configuration.'
	function Get-TeamMembers
	{
		param(
			[Parameter(Mandatory=$true)][String]$TeamName
		)
		($PhysicalAdapterConfigurations | Where-Object -Property 'TeamName' -Value $TeamName -EQ | Select-Object -Property 'Name').Name
	}

	#### BEGIN Modifiable lines ####
	# Add/remove lines as necessary to create new teams. Match with the team name strings from the beginning of the file
	$IgnoreOutput = New-NetLbfoTeam -Confirm:$false -TeamMembers (Get-TeamMembers -TeamName $TeamVSwitchName) -Name $TeamVSwitchName -TeamNicName $TeamVSwitchName -TeamingMode Lacp -LoadBalancingAlgorithm Dynamic 
	Configure-Adapter -NetAdapter (Get-NetAdapter $TeamVSwitchName) # as-is, assumes tNIC name equals team name and clears any IP, DNS, and VMQ settings. for teams that won't hold vswitches, configure IP info

	# Add/remove lines to create team NICs other than the default
	#Add-NetLbfoTeamNic -Team $TeamNotAVSwitchName -VlanID 42 -Name 'ThisAdditionalTeamNicName' -Confirm:$false
	#ConfigureAdapter -NetAdapter 'ThisAdditionalTeamNicName' -IPAddress...

	#### END Modifiable lines ####
	$Teams = Get-NetLbfoTeam
	$TeamNICs = Get-NetLbfoTeamNic
	foreach ($Team in $Teams)
	{
		$ThisTeamsMembers = [String]::Join((", ", $Team.Members))
		New-LogLine -Value ('Team {0} created from members {1}' -f $Team.Name, $ThisTeamsMembers)
	}
	foreach ($TeamNIC in $TeamNICs)
	{
		New-LogLine -Value ('Team NIC "{0}" created on team "{1}"' -f $TeamNIC.Name, $TeamNIC.Team)
	}
	<# End teaming configuration #>

	<# Begin virtual switch configuration #>
	New-LogSeparatorLine
	New-LogLine -Value 'Beginning virtual switch configuration.'

	#### BEGIN Modifiable lines ####
	# modify, add, or remove following lines to configure virtual switch(es)
	$VirtualSwitchName = 'vSwitch' # use different variable names for additional virtual switches; variables used again in the virtual adapters segment
	$IgnoreOutput = New-VMSwitch -Name $VirtualSwitchName -Confirm:$false -AllowManagementOS $false -NetAdapterName $TeamVSwitchName -MinimumBandwidthMode Weight -EnableIov $false
	#### END Modifiable lines ####

	$Switches = Get-VMSwitch
	foreach($VMSwitch in $Switches)
	{
		New-LogLine -Value ('Virtual switch "{0}" created.' -f $VMSwitch.Name)
		if($VMSwitch.SwitchType -eq 'External')
		{
			foreach ($TeamNIC in $TeamNICs)
			{
				if(($TeamNIC.InterfaceDescription -eq $VMSwitch.NetAdapterInterfaceDescription) -and $TeamNIC.Primary -eq $false)
				{
					# KNOWN ISSUE: can trigger multiple times; more work to prevent than the duplication justifies
					New-LogWarning -Value ('Team {0} hosts a virtual switch and multiple team NICs. This configuration is not supported by Microsoft and may lead to unpredictable QoS behavior.' -f $TeamNIC.Team)
				}
			}
		}
	}
	<# End virtual switch configuration #>

	<# Begin virtual adapter configuration #>
	New-LogSeparatorLine
	New-LogLine -Value 'Beginning management operating system virtual adapter configuration.'

	function New-VirtualAdapterDefinition
	{
		param(
			[Parameter(Mandatory=$true)][String]$Name,
			[Parameter()][String]$SwitchName,
			[Parameter()][String]$MacAddress,
			[Parameter()][String]$IPAddress = '',
			[Parameter()][Int32]$PrefixLength = 24,
			[Parameter()][String]$DefaultGateway = '',
			[Parameter()][String[]]$DNSServers = @(),
			[Parameter()][Switch]$RegisterInDns = $false,
			[Parameter()][Int32]$VlanId = 0,
			[Parameter()][UInt32]$VmqWeight = 100,
			[Parameter()][UInt32]$IovWeight = 0,
			[Parameter()][Int64]$MinimumBandwidth = 0,
			[Parameter()][Int64]$MaximumBandwidth = 0
		)
		$AdapterObject = New-Object PSObject
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'Name' -Value $Name
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'SwitchName' -Value $SwitchName
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'MacAddress' -Value $MacAddress
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'IPAddress' -Value $IPAddress
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'PrefixLength' -Value $PrefixLength
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'DefaultGateway' -Value $DefaultGateway
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'DNSServers' -Value $DNSServers
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'RegisterInDns' -Value $RegisterInDns
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'VlanId' -Value $VlanId
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'VmqWeight' -Value $VmqWeight
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'IovWeight' -Value $IovWeight
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'MinimumBandwidth' -Value $MinimumBandwidth
		Add-Member -InputObject $AdapterObject -MemberType NoteProperty -Name 'MaximumBandwidth' -Value $MaximumBandwidth
		$AdapterObject
	}

	$VirtualAdapterConfigurations = @()

	#### BEGIN Modifiable lines ####
	# Add/remove line groups to create and configure management operating system virtual adapters
	$VirtualAdapterConfigurations += New-VirtualAdapterDefinition -Name 'Cluster' -SwitchName $VirtualSwitchName -IPAddress '192.168.10.11' -VlanId 10 -MinimumBandwidth 5
	$VirtualAdapterConfigurations += New-VirtualAdapterDefinition -Name 'LiveMigration' -SwitchName $VirtualSwitchName -IPAddress '192.168.15.11' -VlanId 15 -MinimumBandwidth 10
	#Example with more parameters: $VirtualAdapters += New-VirtualAdapterDefinition -Name 'Management' -IPAddress '192.168.25.10' -DefaultGateway '192.168.25.1' -DNSServers @('192.168.25.5', '192.168.25.6')
	#### END Modifiable lines ####

	foreach($VirtualAdapterConfiguration in $VirtualAdapterConfigurations)
	{
		New-LogSeparatorLine
		New-LogLine -Value ('Configuring virtual adapter "{0}"' -f $VirtualAdapterConfiguration.Name)
		$AdapterParameters = @{}
		$AdapterParameters.Add('ManagementOS', $true)
		$AdapterParameters.Add('Name', $VirtualAdapterConfiguration.Name)
		$AdapterParameters.Add('SwitchName', $VirtualAdapterConfiguration.SwitchName)
		New-LogLine -Value ('Created on switch {0}' -f $VirtualAdapterConfiguration.SwitchName)
		if([String]::IsNullOrEmpty($VirtualAdapterConfiguration.MacAddress))
		{
			$AdapterParameters.Add('DynamicMacAddress', $true)
			New-LogLine -Value 'Using dynamic MAC Address'
		}
		else
		{
			$AdapterParameters.Add('StaticMacAddress', $VirtualAdapterConfiguration.MacAddress)
			New-LogLine -Value ('Using static MAC Address' -f $VirtualAdapterConfiguration.MacAddress)
		}
		$AdapterParameters.Add('PassThru', $true)

		$AdditionalParameters = @{}
		foreach($Switch in $Switches)
		{
			if($Switch.Name -eq $VirtualAdapterConfiguration.SwitchName)
			{
				switch ($Switch.BandwidthReservationMode) {
					Weight {
						$QoSWeight = $VirtualAdapterConfiguration.MinimumBandwidth

						$AdditionalParameters.Add('MaximumBandwidth', $VirtualAdapterConfiguration.MaximumBandwidth)
						if($VirtualAdapterConfiguration.MinimumBandwidth -gt 100)
						{
							New-LogError -Value ('QoS weight cannot exceed 100%. Changing {0} to {1}' -f $QoSWeight, $MinimalQoSWeight)
							$QoSWeight = [UInt32]$MinimalQoSWeight
						}
						$AdditionalParameters.Add('MinimumBandwidthWeight', $QoSWeight)
					}
					Absolute {
						$AdditionalParameters.Add('MinimumBandwidthAbsolute', $VirtualAdapterConfiguration.MinimumBandwidth)
					}
					#default { } # everything is ignored otherwise
				}
				$AdditionalParameters.Add('VmqWeight', $VirtualAdapterConfiguration.VmqWeight)
				$AdditionalParameters.Add('IovWeight', $VirtualAdapterConfiguration.IovWeight)
			}
		}

		$VlanParameters = @{}
		$VlanParameters.Add('Access', $true)
		if($VirtualAdapterConfiguration.VlanId)
		{
			$VlanParameters.Add('VlanId', $VirtualAdapterConfiguration.VlanId)
			New-LogLine -Value ('Assigning to VLAN {0}' -f $VirtualAdapterConfiguration.VlanId)
		}
		else
		{
			$VlanParameters.Add('Untagged', $true)
			New-LogLine -Value 'Assigned to the default VLAN.'
		}
	
		try
		{
			$VirtualAdapter = Add-VMNetworkAdapter @AdapterParameters -ErrorAction Stop
		}
		catch
		{
			New-LogError -Value ('Unable to create virtual adapter. Message: ' -f $_.Exception.Message)
			continue
		}
		Set-VMNetworkAdapter -VMNetworkAdapter $VirtualAdapter @AdditionalParameters
		Set-VMNetworkAdapterVlan -VMNetworkAdapter $VirtualAdapter @VlanParameters
		$ManagementOSVNetAdapter = Get-NetAdapter | Where-Object -Property DeviceID -EQ $VirtualAdapter.DeviceId
		Configure-Adapter -NetAdapter $ManagementOSVNetAdapter -IPAddress $VirtualAdapterConfiguration.IPAddress -PrefixLength $VirtualAdapterConfiguration.PrefixLength -DefaultGateway $VirtualAdapterConfiguration.DefaultGateway -DNSServers $VirtualAdapterConfiguration.DNSServers -RegisterInDns $VirtualAdapterConfiguration.RegisterInDns
	}
	<# End virtual adapter configuration #>

	<# Begin final host configuration; no user serviceable parts inside #>
	if($DomainName)
	{
		$CredentialUser = ''
		if($DomainName.Contains("."))
		{
			$CredentialUser = '{0}@{1}' -f $DomainUser, $DomainName
		}
		else
		{
			$CredentialUser = '{0}{1}' -f $DomainName, $DomainUser
		}
	
		$CredentialPassword = ConvertTo-SecureString -String $DomainPassword -AsPlainText -Force
		$DomainJoinCredentials = New-Object -TypeName pscredential -ArgumentList ($CredentialUser, $CredentialPassword)

		$DomainJoinParameters = @{}
		$DomainJoinParameters.Add('DomainName', $DomainName)
		$DomainJoinParameters.Add('Credential', $DomainJoinCredentials)
		if(-not([String]::IsNullOrEmpty($DomainOU)))
		{
			$DomainJoinParameters.Add('OUPath', $DomainOU)
		}

		$DomainJoinSucceeded = $false
		try
		{
			Add-Computer @DomainJoinParameters -Force -ErrorAction Stop
			$DomainJoinSucceeded = $true
		}
		catch [InvalidOperationException]
		{
			if($_.FullyQualifiedErrorId -match 'FailToJoinDomainFromWorkgroup')
			{
				try
				{
					# if the account already exists then OUPath cannot be specified. unfortunately, there is no way to check in advance without loading the AD cmdlets
					$DomainJoinParameters.Remove('OUPath')
					Add-Computer @DomainJoinParameters -Force -ErrorAction Stop
					$DomainJoinSucceeded = $true
				}
				catch
				{
					New-LogError -Value ('Unable to join domain {0}: {1}' -f $DomainName, $_.Exception.Message)
				}
			}
			if(-not $DomainJoinSucceeded)
			{
				New-LogError -Value ('Unable to join domain {0}: {1}' -f $DomainName, $_.Exception.Message)
			}
		}
		catch
		{
			New-LogError -Value ('Unable to join domain {0}: {1}' -f $DomainName, $_.Exception.Message)
		}
		if($DomainJoinSucceeded)
		{
			New-LogLine -Value ('Joined domain {0}. Restarting to complete.' -f $DomainName)
			Restart-PartiallyRebuiltHost -NextAction Finish
		}
	}
	Complete-HostRebuild # this is a fall-through in case the domain join does not occur
}

function Complete-HostRebuild
{
	if(Get-ScheduledJob | Where-Object -Property Name -Value 'RebuildHost' -EQ)
	{
		Unregister-ScheduledTask -TaskName 'RebuildHost' -Confirm:$false
		New-LogLine -Value 'Restart successful. Deleting scheduled job.'
	}
	@($DefaultVMPath, $DefaultVHDPath) | foreach {
		if(-not([String]::IsNullOrEmpty($_)))
		{
			if(-not(Test-Path $_))
			{
				$IgnoreOutput = New-Item -Path $_ -ItemType Directory
				New-LogLine -Value ('Folder {0} created.' -f $_)
			}
		}
	}

	if($DefaultVMPath)
	{
		Set-VMHost -VirtualMachinePath $DefaultVMPath
		New-LogLine -Value ('Default virtual machine path set to "{0}"' -f $DefaultVMPath)
	}

	if($DefaultVHDPath)
	{
		Set-VMHost -VirtualHardDiskPath $DefaultVHDPath
		New-LogLine -Value ('Default virtual hard disk path set to "{0}"' -f $DefaultVHDPath)
	}

	<# Begin iSCSI configuration #>
	Set-Service -Name MSiSCSI -StartupType Automatic
	Start-Service -Name MSiSCSI
	$iSCSIError = $false
	#### BEGIN Modifiable lines ####
	$UseMPIOForiSCSI = $true
	# duplicate and/or reconfigure following lines as necessary
	# use caution if attempting to shorten script! authentication methods etc. may not duplicate well!

	## set up all portals first; for multiple portals, will need to use variables other than $Portal as the connections will operate on individual portal objects
	try
	{
		$Portal = New-IscsiTargetPortal -TargetPortalAddress '192.168.25.12' -InitiatorPortalAddress '192.168.25.11'
	}
	catch
	{
		New-LogError -Value ('Unable to establish a connection to portal: {0}' -f $_.Exception.Message)
		$iSCSIError = $true
	}

	# leave the following 'if' block alone; skip past for connection configuration
	if($UseMPIO -and $UseMPIOForiSCSI -and -not $iSCSIError)
	{
		$Script:RestartRequired = Enable-MSDSMAutomaticClaim -BusType iSCSI
	}

	$DiscoveredTargets = @() # do not remove; this ensures that the foreach doesn't fail
	if(-not ($iSCSIError))
	{
		try
		{
			$DiscoveredTargets = Get-IscsiTarget -IscsiTargetPortal $Portal
		}
		catch
		{
			New-LogError -Value ('Unable to retrieve iSCSI target(s) from portal {0}: {1}' -f $Portal.TargetPortalAddress)
			$iSCSIError = $true
		}
	}
	## duplicate the following per portal, changing IPs and adding authentication information as necessary; will need to use variable names other than $Portal
	# TODO: this COULD be collapsed further by using custom PS objects to aggregate settings for splatting and looping, but with the vast number of iSCSI options, the outcome would not be significantly less script except in environments with lots of iSCSI portals
	foreach($DiscoveredTarget in $DiscoveredTargets)
	{
		# WARNING: any pre-existing session data is NOT cleared, due to the complexity of ensuring that it is done correctly
		## set target and initiator IPs as necessary
		$Session1TargetIP = '192.168.50.100'
		$Session1InitiatorIP = '192.168.50.11'
		$Session2TargetIP = '192.168.51.100'
		$Session2InitiatorIP = '192.168.51.11'
		# if MPIO wasn't enabled, errors will be logged if more than one session is pointed to the same target
		try
		{
			$Session = Connect-IscsiTarget -NodeAddress $DiscoveredTarget.NodeAddress -TargetPortalAddress $Session1TargetIP -InitiatorPortalAddress $Session1InitiatorIP -IsMultipathEnabled ($UseMPIO -band $UseMPIOForiSCSI) -ErrorAction Stop
			$IgnoreOutput = Register-IscsiSession -InputObject $Session
			New-LogLine -Value ('iSCSI connection created to {0}' -f $DiscoveredTarget.NodeAddress)
		}
		catch
		{
			New-LogError -Value ('Cannot establish iSCSI connection to "{0}: {1}"' -f $DiscoveredTarget.NodeAddress, $_.Exception.Message)
		}
	
		try
		{
			$Session = Connect-IscsiTarget -NodeAddress $DiscoveredTarget.NodeAddress -TargetPortalAddress $Session2TargetIP -InitiatorPortalAddress $Session2InitiatorIP -IsMultipathEnabled ($UseMPIO -band $UseMPIOForiSCSI) -ErrorAction Stop
			$IgnoreOutput = Register-IscsiSession -InputObject $Session
			New-LogLine -Value ('iSCSI connection created to {0}' -f $DiscoveredTarget.NodeAddress)
		}
		catch
		{
			New-LogError -Value ('Cannot establish iSCSI connection to "{0}: {1}"' -f $DiscoveredTarget.NodeAddress, $_.Exception.Message)
		}
	}

	#### END Modifiable lines ####
	<# End iSCSI configuration #>

	#### BEGIN Modifiable lines ####
	# any other host customizations can go here
	#### END Modifiable lines ####

	<# End final host configuration #>
	New-LogLine -Value 'Configuration complete!'
	if($RestartRequired)
	{
		New-LogLine -Value 'Restarting for final cleanup.'
		Restart-Computer
	}
}

 throw ("You didn't read the instructions, did you?")

switch($Action)
{
	'Start' { Start-HostRebuild }
	'Main' { Resume-HostRebuild }
	'Finish' { Complete-HostRebuild }
}

This is a copy of the RebuildHost.log file from my testing:

***********************************************************
Configuration began on 2/13/2016 12:58:43 PM
***********************************************************
Time zone set to Central Standard Time
***********************************************************
Beginning basic host configuration
Computer renamed to "svhv2"
Hyper-V installed.
Built scheduled job to restart host and automatically resume script.
Restarting computer...
Restart successful. Deleting scheduled job.
Any existing virtual switches removed.
Any existing teams cleared.
Any existing IP information cleared.
Any existing DNS server address information cleared.
Any existing default gateway information cleared.
***********************************************************
Beginning configuration of physical adapters.
***********************************************************
Configuring physical adapter with MAC address A0-B3-CC-E4-F5-5D
Renamed to Onboard
IP address set to 192.168.25.11 with a prefix length of 24
Default gateway set to 192.168.25.1
DNS Servers set to 192.168.25.5, 192.168.25.6
Adapter will register in DNS: True
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Configuring physical adapter with MAC address 00-0A-CD-20-DB-0C
Renamed to PBR
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Configuring physical adapter with MAC address 00-0A-CD-20-DA-F6
Renamed to PBL
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Configuring physical adapter with MAC address 00-0A-CD-20-DB-0D
Renamed to PTL
IP address set to 192.168.50.11 with a prefix length of 24
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Configuring physical adapter with MAC address 00-0A-CD-20-DA-F7
Renamed to PTR
IP address set to 192.168.51.11 with a prefix length of 24
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Beginning network teaming configuration.
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
Set property "Virtual Machine Queues" to 0
Set property "Virtual Machine Queues - Shared Memory" to 0
Set property "Virtual Machine Queues - VLAN Id Filtering" to 0
Team vSwitchTeam created from members 
Team NIC "vSwitchTeam" created on team "vSwitchTeam"
***********************************************************
Beginning virtual switch configuration.
Virtual switch "vSwitch" created.
***********************************************************
Beginning management operating system virtual adapter configuration.
***********************************************************
Configuring virtual adapter "Cluster"
Created on switch vSwitch
Using dynamic MAC Address
Assigning to VLAN 10
Renamed to PTR
IP address set to 192.168.10.11 with a prefix length of 24
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
***********************************************************
Configuring virtual adapter "LiveMigration"
Created on switch vSwitch
Using dynamic MAC Address
Assigning to VLAN 15
Renamed to PTR
IP address set to 192.168.15.11 with a prefix length of 24
Adapter will register in DNS: False
Preparing to set VMQ values. If no VMQ-related entries appear below, no VMQ-related settings were found on this adapter.
Joined domain siron.int. Restarting to complete.
Folder C:LocalVMs created.
Folder C:LocalVMsVirtual Hard Disks created.
Default virtual machine path set to "C:LocalVMs"
Default virtual hard disk path set to "C:LocalVMsVirtual Hard Disks"
iSCSI connection created to iqn.1991-05.com.microsoft:svstore-csvs-target
iSCSI connection created to iqn.1991-05.com.microsoft:svstore-csvs-target
iSCSI connection created to iqn.1991-05.com.microsoft:svstore-quorum-target
iSCSI connection created to iqn.1991-05.com.microsoft:svstore-quorum-target
Configuration complete!
Restarting for final cleanup.

Feedback

I’d like to know what you think of this. If you find it useful, please let me know. If there’s something that would be good to add for the community’s use, suggest it in the comments.

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

10 thoughts on "A Free PowerShell Script to Configure a Hyper-V Host"

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.