HyperV - Static Ip with Vagrant
Background
The Asker, and anyone with a similar use-case, is likely operating on a workstation (not Windows Server edition), for local development purposes, and prefers not to run a DHCP server merely to prevent Hyper-V from changing IP address assignments on every host machine reboot.
The need to hard-code a static IP address within the VM's network configuration "manually", after provisioning, is cumbersome and stymies automation. Wouldn't it be far more preferable to automate the entire process, in keeping with the spirit of "disposable" development VMs and a hands-free provisioning workflow? Why, yes it would!
All of this hoop-jumping is necessary because Vagrant cannot (yet) set a static IP address for Hyper-V machines. See:
- https://www.vagrantup.com/docs/hyperv/limitations.html
- https://github.com/hashicorp/vagrant/issues/8384
Overview
In short, the sequence of events to make this work is as follows:
- Create NAT switch for Hyper-V. (This is an Internal-only switch behind which any number of VMs with static IP addresses can sit and talk to each other, as well as the Hyper-V host. Guests behind this switch have outbound access to any network resources to which the host has access, such as the Internet or a LAN.)
- Configure a Vagrant Trigger that leverages the
vagrant-reload
plugin within the before-reload event to change a given VM's network switch on-demand. -
vagrant up
, but chooseDefault Switch
(not the newNATSwitch
) on initial provision. - At the beginning of the provisioning process, configure a static IP within the VM's operating system that is within the
NATSwitch
's range (this step is OS-specific). - Call
config.vm.provision :reload
, which will a) fire the trigger defined in step 2, thereby changing the VM's network switch to the newNATSwitch
; and b) issuevagrant reload
and continue the provisioning process after the VM reboots. - When the VM reboots, it will acquire the static IP address from the
NATSwitch
and use it indefinitely.
1. Create NAT Switch
While this step can certainly be done once, manually, it's even more powerful to bake the commands into a script that can be called from the Vagrantfile
during provisioning. Such a script might look like this:
./scripts/create-nat-hyperv-switch.ps1
:
# See: https://www.petri.com/using-nat-virtual-switch-hyper-v
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'
}
Then, add an appropriate trigger to the top of the Vagrantfile
config section, which will ensure that the configuration is always correct upon vagrant up
:
config.trigger.before :up do |trigger|
trigger.info = "Creating 'NATSwitch' Hyper-V switch if it does not exist..."
trigger.run = {privileged: "true", powershell_elevated_interactive: "true", path: "./scripts/create-nat-hyperv-switch.ps1"}
end
2. Configure Vagrant Reload Trigger
Rebooting (reloading) the VM in the middle of provisioning, in order to change over to a static IP address, requires the https://github.com/aidanns/vagrant-reload plugin, so install that first:
vagrant plugin install vagrant-reload
The script that the trigger will call is very simply:
./scripts/set-hyperv-switch.ps1
:
# See: https://www.thomasmaurer.ch/2016/01/change-hyper-v-vm-switch-of-virtual-machines-using-powershell/
Get-VM "homestead" | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName "NATSwitch"
Next, add an appropriate trigger to the Vagrantfile
config section (i.e., just below the trigger added in the previous step):
config.trigger.before :reload 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"}
end
3. Configure Static IP Within Guest VM
Configuring a static IP is an OS-specific task, so this procedure should be adjusted to suit the specific guest OS. Two examples follow.
NOTE: If on a corporate network, be sure to use the company's DNS servers, as Google's (shown in these examples) may not be reachable.
Ubuntu 18.04 LTS Example
On Ubuntu 18.04 LTS, this works well:
./scripts/configure-static-ip.sh
:
#!/bin/sh
echo 'Setting static IP address for Hyper-V...'
cat << EOF > /etc/netplan/01-netcfg.yaml
network:
version: 2
ethernets:
eth0:
dhcp4: no
addresses: [192.168.0.2/24]
gateway4: 192.168.0.1
nameservers:
addresses: [8.8.8.8,8.8.4.4]
EOF
# Be sure NOT to execute "netplan apply" here, so the changes take effect on
# reboot instead of immediately, which would disconnect the provisioner.
RedHat, CentOS, and Oracle Linux Example
./scripts/configure-static-ip.sh
:
#!/bin/sh
echo 'Setting static IP address for Hyper-V...'
cat << EOF > /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
BOOTPROTO=none
ONBOOT=yes
PREFIX=24
IPADDR=192.168.0.2
GATEWAY=192.168.0.1
DNS1=8.8.8.8
EOF
# Be sure NOT to execute "systemctl restart network" here, so the changes take
# effect on reboot instead of immediately, which would disconnect the provisioner.
With the above script in place, add something like this just below the trigger definitions that we added earlier:
config.vm.provision "shell", path: "./scripts/configure-static-ip.sh"
config.vm.provision :reload
4. Provision the VM
vagrant up
and choose Default Switch
when prompted; this causes the VM to obtain a dynamic IPv4 address that is sufficient for Vagrant to connect to the VM via SSH, mount Shared Folders, and begin provisioning.
Now, the static IP address will be set and the VM reloaded before provisioning continues.
Sample output:
homestead: Setting static IP address for Hyper-V...
==> homestead: Running provisioner: reload...
==> homestead: Running action triggers before reload ...
==> homestead: Running trigger...
==> homestead: Setting Hyper-V switch to 'NATSwitch' to allow for static IP...
homestead: Running local script: ./scripts/set-hyperv-switch.ps1
==> homestead: Attempting graceful shutdown of VM...
==> homestead: Stopping the machine...
homestead: Configuring the VM...
==> homestead: Starting the machine...
==> homestead: Waiting for the machine to report its IP address...
homestead: Timeout: 120 seconds
homestead: IP: 192.168.0.2
==> homestead: Waiting for machine to boot. This may take a few minutes...
==> homestead: Machine booted and ready!
5. Other Thoughts
- Manual input is required to choose the
Default Switch
after the initialvagrant up
; it would be ideal to find a way around this. - The vagrant-reload provisioner cannot shutdown the machine gracefully and must halt it forcibly; not a significant concern, given that it happens only once (during initial provisioning). This happens due to the fact that changing the VM's Hyper-V switch from
Default Switch
toNATSwitch
must be done during the before-reload event, which is, in effect, akin to pulling the Ethernet cord out of a physical jack and connecting it to a different switch.
It seems that the external DHCP server with the scope of 172.20.23.X will provide IP addresses for your virtual machines. You can use filters in the DHCP console to prevent the DHCP server from issuing addresses to these machines by entering the mac address of the virtual machine. As shown below:
Then you can use other DHCP to set reserved addresses for your virtual machine (my understanding is that you don't want to get the address in the 172.20.83.X scope, so use other DHCP) or manually configure static addresses so that their information is the same as the contents of your host file.
There is a workaround if you want to set switch:
config.vm.network "public_network", bridge: "VirtualSwitchName"
enhancement: hyper-v provider vswitch customization parameter #7915