Save to My DOJO
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:
- Very basic host configuration, like a name change.
- Installs drivers. Feed it .INF files.
- Enables roles and features. Hyper-V and MPIO are handled automatically.
- 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.
- 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.
- Configures network team(s). Comment this section out as necessary.
- Configures virtual switches.
- Configures virtual network adapters for the management operating system.
- Joins the domain, if one is specified along with valid credentials for a domain account that has permissions to add computers to the domain.
- Reboots if domain join was successful.
- 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.
- Some Hyper-V defaults are set.
- 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.
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"
Why is your avatar black when you’re a white guy?
Doesn’t look black on any of my screens or devices.
does this script works for Windows server 2019 too ?
kind regards, and keep the awesome work !
It doesn’t incorporate any new features, but yes it works.