Vagrant HyperV Static IP Addresses
One of the features that kept me using the Vagrant VirtualBox provider for a long time on Windows rather than the native HyperV hypervisor was the ability to set static IP addresses for your VMs. But it turns out with a bit of extra effort you can get this working with the HyperV Vagrant provider too.
At a high level, the solution is create a second virtual switch of type “Internal”, attach that switch to the VM as a network adapter, and then script the setting up a static IP from the range defined for the internal switch.
The tricky part of this process is adding support for this to work on both Linux and Windows, as the triggers for running these customizations are different between these two platforms.
Triggers
The following sections outline the details of how I implemented these customizations to my HyperV VMs using Vagrant triggers. Triggers are life-cycle hooks in the provisioning of a VM.
Shared Triggers
Creating the Virtual Switch can be done at the same time regardless of platform:
config.trigger.before :up do |trigger|
trigger.info = "Creating Hyper-V switch if it does not exist..."
trigger.run = {privileged: "true", powershell_elevated_interactive: "true", path: "../scripts/create-nat-hyperv-switch.ps1"}
end
I followed the guide here to write this snippet of Powershell.
If ("NATSwitch" -in (Get-VMSwitch | Select-Object -ExpandProperty Name) -eq $FALSE) {
'Creating Internal-only switch named "NATSwitch" on Windows Hyper-V host...'
New-VMSwitch -SwitchName "NATSwitch" -SwitchType Internal
New-NetIPAddress -IPAddress 192.168.0.1 -PrefixLength 24 -InterfaceAlias "vEthernet (NATSwitch)"
New-NetNAT -Name "NATNetwork" -InternalIPInterfaceAddressPrefix 192.168.0.0/24
} else {
'"NATSwitch" for static IP configuration already exists; skipping'
}
If ("192.168.0.1" -in (Get-NetIPAddress | Select-Object -ExpandProperty IPAddress) -eq $FALSE) {
'Registering new IP address 192.168.0.1 on Windows Hyper-V host...'
New-NetIPAddress -IPAddress 192.168.0.1 -PrefixLength 24 -InterfaceAlias "vEthernet (NATSwitch)"
} else {
'"192.168.0.1" for static IP configuration already registered; skipping'
}
If ("192.168.0.0/24" -in (Get-NetNAT | Select-Object -ExpandProperty InternalIPInterfaceAddressPrefix) -eq $FALSE) {
'Registering new NAT adapter for 192.168.0.0/24 on Windows Hyper-V host...'
New-NetNAT -Name "NATNetwork" -InternalIPInterfaceAddressPrefix 192.168.0.0/24
} else {
'"192.168.0.0/24" for static IP configuration already registered; skipping'
}
Triggers for Linux (Rocky8)
Attaching the Switch to the VM
In this trigger and script you can see I use the current working directory name as an argument. I also use the current working directory name to prefix the VM name.
config.trigger.before :"VagrantPlugins::HyperV::Action::StartInstance" do |trigger|
trigger.info = "Setting Hyper-V switch to 'NATSwitch' to allow for static IP..."
trigger.run = {privileged: "true", powershell_elevated_interactive: "true", path: "../scripts/set-hyperv-switch.ps1", args: File.basename(Dir.getwd)}
end
Where the script contents looks like:
$netAdapter = Get-VM "$($args[0])*" | Get-VMNetworkAdapter -Name NAT -ErrorAction SilentlyContinue
if ($null -ne $netAdapter) {
Write-Host "The network adapter already exists."
} else {
Write-Host "The network adapter does not exist, creating..."
Get-VM "$($args[0])*" | Add-VMNetworkAdapter -Name NAT -SwitchName NATSwitch
}
Triggers for Windows (Windows 2019)
Note I first have to sleep to allow some initial boot up operations to finish before I can attach the switch.
config.trigger.before :"VagrantPlugins::HyperV::Action::WaitForIPAddress", type: :action do |t|
t.info = "Sleep to prevent issues where Windows hasn't completed Applying computer settings"
t.run = { inline: "Start-Sleep -Seconds 120" }
end
config.trigger.before :"VagrantPlugins::HyperV::Action::WaitForIPAddress", type: :action do |t|
t.name = "Add NATSwitch to VM"
t.run = {
path: "./../scripts/set-hyperv-switch.ps1",
args: [File.basename(Dir.getwd)]
}
end
Assign the Static IP for Linux (Rocky8)
config.vm.provision "shell", path: "./../scripts/configure-static-ip.sh", args: [IP_ADDRESS]
#!/bin/sh
echo "Setting static IP $1 address for Hyper-V..."
cat << EOF > /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1
BOOTPROTO=none
ONBOOT=yes
PREFIX=24
IPADDR=$1
GATEWAY=192.168.0.1
DNS1=8.8.8.8
EOF
Assign the Static IP for Windows (Windows 2019)
config.vm.provision "shell", path: "./../scripts/configure-static-ip.ps1", args: [IP_ADDRESS]
$adapter = Get-NetAdapter -Physical | Where-Object { $_.Name -eq "Ethernet 2" }
Set-NetIPInterface -InterfaceIndex $adapter.InterfaceIndex -Dhcp Disabled
New-NetIPAddress -InterfaceIndex $adapter.InterfaceIndex -IPAddress $($args[0]) -PrefixLength 24 -DefaultGateway "192.168.0.1"