From 507d5983becdab30d443bb25800f607bcabfc283 Mon Sep 17 00:00:00 2001 From: Wong Hoi Sing Edison Date: Thu, 19 Sep 2024 07:17:12 +0800 Subject: [PATCH] Add VirtualBox 7.1.x Support Fixes https://github.com/hashicorp/vagrant/issues/13501 Signed-off-by: Wong Hoi Sing Edison --- plugins/providers/virtualbox/driver/meta.rb | 1 + .../virtualbox/driver/version_7_1.rb | 305 +++++++ plugins/providers/virtualbox/plugin.rb | 1 + .../virtualbox/driver/version_7_1_test.rb | 856 ++++++++++++++++++ .../docs/providers/virtualbox/index.mdx | 2 +- 5 files changed, 1164 insertions(+), 1 deletion(-) create mode 100644 plugins/providers/virtualbox/driver/version_7_1.rb create mode 100644 test/unit/plugins/providers/virtualbox/driver/version_7_1_test.rb diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 7c0b2600652..e29acab01d7 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -69,6 +69,7 @@ def initialize(uuid=nil) "6.0" => Version_6_0, "6.1" => Version_6_1, "7.0" => Version_7_0, + "7.1" => Version_7_1, } if @@version.start_with?("4.2.14") diff --git a/plugins/providers/virtualbox/driver/version_7_1.rb b/plugins/providers/virtualbox/driver/version_7_1.rb new file mode 100644 index 00000000000..f0a4a26efcf --- /dev/null +++ b/plugins/providers/virtualbox/driver/version_7_1.rb @@ -0,0 +1,305 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +require "rexml" +require File.expand_path("../version_7_0", __FILE__) + +module VagrantPlugins + module ProviderVirtualBox + module Driver + # Driver for VirtualBox 7.1.x + class Version_7_1 < Version_7_0 + # VirtualBox version requirement for using host only networks + # instead of host only interfaces + HOSTONLY_NET_REQUIREMENT=Gem::Requirement.new(">= 7") + # Prefix of name used for host only networks + HOSTONLY_NAME_PREFIX="vagrantnet-vbox" + DEFAULT_NETMASK="255.255.255.0" + + def initialize(uuid) + super + + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_7_1") + end + + def read_bridged_interfaces + ifaces = super + return ifaces if !use_host_only_nets? + + # Get a list of all subnets which are in use for hostonly networks + hostonly_ifaces = read_host_only_networks.map do |net| + IPAddr.new(net[:lowerip]).mask(net[:networkmask]) + end + + # Prune any hostonly interfaces in the list + ifaces.delete_if { |i| + addr = begin + IPAddr.new(i[:ip]).mask(i[:netmask]) + rescue IPAddr::Error => err + @logger.warn("skipping bridged interface due to parse error #{err} (#{i}) ") + nil + end + addr.nil? || + hostonly_ifaces.include?(addr) + } + + ifaces + end + + def delete_unused_host_only_networks + return super if !use_host_only_nets? + + # First get the list of existing host only network names + network_names = read_host_only_networks.map { |net| net[:name] } + # Prune the network names to only include ones we manage + network_names.delete_if { |name| !name.start_with?(HOSTONLY_NAME_PREFIX) } + + @logger.debug("managed host only network names: #{network_names}") + + return if network_names.empty? + + # Next get the list of host only networks currently in use + inuse_names = [] + execute("list", "vms", retryable: true).split("\n").each do |line| + match = line.match(/^".+?"\s+\{(?.+?)\}$/) + next if match.nil? + begin + info = execute("showvminfo", match[:vmid].to_s, "--machinereadable", retryable: true) + info.split("\n").each do |vmline| + if vmline.start_with?("hostonly-network") + net_name = vmline.split("=", 2).last.to_s.gsub('"', "") + inuse_names << net_name + end + end + rescue Vagrant::Errors::VBoxManageError => err + raise if !err.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + end + end + + @logger.debug("currently in use network names: #{inuse_names}") + + # Now remove all the networks not in use + (network_names - inuse_names).each do |name| + execute("hostonlynet", "remove", "--name", name, retryable: true) + end + end + + def enable_adapters(adapters) + return super if !use_host_only_nets? + + hostonly_adapters = adapters.find_all { |adapter| adapter[:hostonly] } + other_adapters = adapters - hostonly_adapters + super(other_adapters) if !other_adapters.empty? + + if !hostonly_adapters.empty? + args = [] + hostonly_adapters.each do |adapter| + args.concat(["--nic#{adapter[:adapter]}", "hostonlynet"]) + args.concat(["--host-only-net#{adapter[:adapter]}", adapter[:hostonly], + "--cableconnected#{adapter[:adapter]}", "on"]) + end + + execute("modifyvm", @uuid, *args, retryable: true) + end + end + + def create_host_only_network(options) + # If we are not on macOS, just setup the hostonly interface + return super if !use_host_only_nets? + + opts = { + netmask: options.fetch(:netmask, DEFAULT_NETMASK), + } + + if options[:type] == :dhcp + opts[:lower] = options[:dhcp_lower] + opts[:upper] = options[:dhcp_upper] + else + addr = IPAddr.new(options[:adapter_ip]) + opts[:upper] = opts[:lower] = addr.mask(opts[:netmask]).to_range.first.to_s + end + + name_idx = read_host_only_networks.map { |hn| + next if !hn[:name].start_with?(HOSTONLY_NAME_PREFIX) + hn[:name].sub(HOSTONLY_NAME_PREFIX, "").to_i + }.compact.max.to_i + 1 + opts[:name] = HOSTONLY_NAME_PREFIX + name_idx.to_s + + execute("hostonlynet", "add", + "--name", opts[:name], + "--netmask", opts[:netmask], + "--lower-ip", opts[:lower], + "--upper-ip", opts[:upper], + retryable: true) + + { + name: opts[:name], + ip: options[:adapter_ip], + netmask: opts[:netmask], + } + end + + # Disabled when host only nets are in use + def reconfig_host_only(options) + return super if !use_host_only_nets? + end + + # Disabled when host only nets are in use since + # the host only nets will provide the dhcp server + def remove_dhcp_server(*_, **_) + super if !use_host_only_nets? + end + + # Disabled when host only nets are in use since + # the host only nets will provide the dhcp server + def create_dhcp_server(*_, **_) + super if !use_host_only_nets? + end + + def read_host_only_interfaces + return super if !use_host_only_nets? + + # When host only nets are in use, read them and + # reformat the information to line up with how + # the interfaces is structured + read_host_only_networks.map do |net| + addr = begin + IPAddr.new(net[:lowerip]) + rescue IPAddr::Error => err + @logger.warn("invalid host only network lower IP encountered: #{err} (#{net})") + next + end + # Address of the interface will be the lower bound of the range or + # the first available address in the subnet + if addr == addr.mask(net[:networkmask]) + addr = addr.succ + end + + net[:netmask] = net[:networkmask] + if addr.ipv4? + net[:ip] = addr.to_s + net[:ipv6] = "" + else + net[:ip] = "" + net[:ipv6] = addr.to_s + net[:ipv6_prefix] = net[:netmask] + end + + net[:status] = net[:state] == "Enabled" ? "Up" : "Down" + + net + end.compact + end + + def read_network_interfaces + return super if !use_host_only_nets? + + {}.tap do |nics| + execute("showvminfo", @uuid, "--machinereadable", retryable: true).each_line do |line| + if m = line.match(/nic(?\d+)="(?.+?)"$/) + nics[m[:adapter].to_i] ||= {} + if m[:type] == "hostonlynetwork" + nics[m[:adapter].to_i][:type] = :hostonly + else + nics[m[:adapter].to_i][:type] = m[:type].to_sym + end + elsif m = line.match(/^bridgeadapter(?\d+)="(?.+?)"$/) + nics[m[:adapter].to_i] ||= {} + nics[m[:adapter].to_i][:bridge] = m[:network] + elsif m = line.match(/^hostonly-network(?\d+)="(?.+?)"$/) + nics[m[:adapter].to_i] ||= {} + nics[m[:adapter].to_i][:hostonly] = m[:network] + end + end + end + end + + # Generate list of host only networks + def read_host_only_networks + networks = [] + current = nil + execute("list", "hostonlynets", retryable: true).split("\n").each do |line| + line.chomp! + next if line.empty? + key, value = line.split(":", 2).map(&:strip) + key = key.downcase + if key == "name" + networks.push(current) if !current.nil? + current = Vagrant::Util::HashWithIndifferentAccess.new + end + current[key] = value + end + networks.push(current) if !current.nil? + + networks + end + + # The initial VirtualBox 7.1 release has an issue with displaying port + # forward information. When a single port forward is defined, the forwarding + # information can be found in the `showvminfo` output. Once more than a + # single port forward is defined, no forwarding information is provided + # in the `showvminfo` output. To work around this we grab the VM configuration + # file from the `showvminfo` output and extract the port forward information + # from there instead. + def read_forwarded_ports(uuid=nil, active_only=false) + # Only use this override for the 7.1.0 release. + return super if get_version.to_s != "7.1.0" + + uuid ||= @uuid + + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") + + results = [] + + info = execute("showvminfo", uuid, "--machinereadable", retryable: true) + result = info.match(/CfgFile="(?.+?)"/) + if result.nil? + raise Vagrant::Errors::VirtualBoxConfigNotFound, + uuid: uuid + end + + File.open(result[:path], "r") do |f| + doc = REXML::Document.new(f) + networks = REXML::XPath.each(doc.root, "Machine/Hardware/Network/Adapter") + networks.each do |net| + REXML::XPath.each(doc.root, net.xpath + "/NAT/Forwarding") do |fwd| + # Result Array values: + # [NIC Slot, Name, Host Port, Guest Port, Host IP] + result = [ + net.attribute("slot").value.to_i + 1, + fwd.attribute("name")&.value.to_s, + fwd.attribute("hostport")&.value.to_i, + fwd.attribute("guestport")&.value.to_i, + fwd.attribute("hostip")&.value.to_s + ] + @logger.debug(" - #{result.inspect}") + results << result + end + end + end + + results + end + + private + + # Returns if hostonlynets are enabled on the current + # host platform + # + # @return [Boolean] + def use_host_only_nets? + Vagrant::Util::Platform.darwin? && + HOSTONLY_NET_REQUIREMENT.satisfied_by?(get_version) + end + + # VirtualBox version in use + # + # @return [Gem::Version] + def get_version + return @version if @version + @version = Gem::Version.new(Meta.new.version) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 8c0ff08fb2c..25b49fd3160 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -104,6 +104,7 @@ module Driver autoload :Version_6_0, File.expand_path("../driver/version_6_0", __FILE__) autoload :Version_6_1, File.expand_path("../driver/version_6_1", __FILE__) autoload :Version_7_0, File.expand_path("../driver/version_7_0", __FILE__) + autoload :Version_7_1, File.expand_path("../driver/version_7_1", __FILE__) end module Model diff --git a/test/unit/plugins/providers/virtualbox/driver/version_7_1_test.rb b/test/unit/plugins/providers/virtualbox/driver/version_7_1_test.rb new file mode 100644 index 00000000000..d9241b65ef6 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/driver/version_7_1_test.rb @@ -0,0 +1,856 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +require "stringio" +require_relative "../base" + +describe VagrantPlugins::ProviderVirtualBox::Driver::Version_7_1 do + include_context "virtualbox" + + let(:vbox_version) { "7.1.0" } + + subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_7_1.new(uuid) } + + it_behaves_like "a version 5.x virtualbox driver" + it_behaves_like "a version 6.x virtualbox driver" + + describe "#read_forwarded_ports" do + let(:uuid) { "MACHINE-UUID" } + let(:cfg_path) { "MACHINE_CONFIG_PATH" } + let(:vm_info) { +%(name="vagrant-test_default_1665781960041_56631" +Encryption: disabled +groups="/" +ostype="Ubuntu (64-bit)" +UUID="#{uuid}" +CfgFile="#{cfg_path}" +SnapFldr="/VirtualBox VMs/vagrant-test_default_1665781960041_56631/Snapshots" +LogFldr="/VirtualBox VMs/vagrant-test_default_1665781960041_56631/Logs" +memory=1024) + } + let(:config_file) { + StringIO.new(VBOX_VMCONFIG_FILE) + } + + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:version).and_return(vbox_version) + end + + describe "VirtualBox version 7.1.0" do + let(:vbox_version) { "7.1.0" } + + before do + allow(subject).to receive(:execute).with("showvminfo", uuid, any_args).and_return(vm_info) + allow(File).to receive(:open).with(cfg_path, "r").and_yield(config_file) + end + + it "should return two port forward values" do + expect(subject.read_forwarded_ports.size).to eq(2) + end + + it "should have port forwards on slot one" do + subject.read_forwarded_ports.each do |fwd| + expect(fwd.first).to eq(1) + end + end + + it "should include host ip for ssh forward" do + fwd = subject.read_forwarded_ports.detect { |f| f[1] == "ssh" } + expect(fwd).not_to be_nil + expect(fwd.last).to eq("127.0.0.1") + end + + describe "when config file cannot be determine" do + let(:vm_info) { %(name="vagrant-test_default_1665781960041_56631") } + + it "should raise a custom error" do + expect(File).not_to receive(:open).with(cfg_path, "r") + + expect { subject.read_forwarded_ports }.to raise_error(Vagrant::Errors::VirtualBoxConfigNotFound) + end + end + end + + describe "VirtualBox version greater than 7.1.0" do + let(:vbox_version) { "7.1.1" } + + before do + allow(subject).to receive(:execute).with("showvminfo", uuid, any_args).and_return(vm_info) + end + + it "should not read configuration file" do + expect(File).not_to receive(:open).with(cfg_path, "r") + subject.read_forwarded_ports + end + end + + end + + describe "#use_host_only_nets?" do + context "when platform is darwin" do + before do + allow(Vagrant::Util::Platform).to receive(:darwin?).and_return(true) + end + + context "when virtualbox version is less than 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("6.0.28") + end + + it "should return false" do + expect(subject.send(:use_host_only_nets?)).to be(false) + end + end + + context "when virtualbox version is greater than 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("7.0.2") + end + + it "should return true" do + expect(subject.send(:use_host_only_nets?)).to be(true) + end + end + + context "when virtualbox version is equal to 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("7.0.0") + end + + it "should return true" do + expect(subject.send(:use_host_only_nets?)).to be(true) + end + end + end + + context "when platform is not darwin" do + before do + allow(Vagrant::Util::Platform).to receive(:darwin?).and_return(false) + end + + context "when virtualbox version is less than 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("6.0.28") + end + + it "should return false" do + expect(subject.send(:use_host_only_nets?)).to be(false) + end + end + + context "when virtualbox version is greater than 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("7.0.2") + end + + it "should return false" do + expect(subject.send(:use_host_only_nets?)).to be(false) + end + end + + context "when virtualbox version is equal to 7" do + before do + allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). + to receive(:version).and_return("7.0.0") + end + + it "should return false" do + expect(subject.send(:use_host_only_nets?)).to be(false) + end + end + end + end + + describe "#read_bridged_interfaces" do + let(:bridgedifs) { VBOX_BRIDGEDIFS } + + before do + allow(subject).to receive(:execute).and_call_original + expect(subject). + to receive(:execute). + with("list", "bridgedifs"). + and_return(bridgedifs) + end + + context "when hostonlynets are not enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should return all interfaces in list" do + expect(subject.read_bridged_interfaces.size).to eq(5) + end + + it "should not read host only networks" do + expect(subject).not_to receive(:read_host_only_networks) + subject.read_bridged_interfaces + end + end + + context "when hostonlynets are enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should return all interfaces in list" do + expect(subject).to receive(:read_host_only_networks).and_return([]) + expect(subject.read_bridged_interfaces.size).to eq(5) + end + + context "when hostonly networks are defined" do + before do + expect(subject). + to receive(:execute). + with("list", "hostonlynets", any_args). + and_return(VBOX_HOSTONLYNETS) + end + + it "should not return all interfaces in list" do + expect(subject.read_bridged_interfaces.size).to_not eq(5) + end + + it "should not include hostonly network devices" do + expect( + subject.read_bridged_interfaces.any? { |int| + int[:name].start_with?("bridge") + } + ).to be(false) + end + end + end + + context "when address is empty" do + let(:bridgedifs) { VBOX_BRIDGEDIFS.sub("0.0.0.0", "") } + + it "should not raise an error" do + expect { subject.read_bridged_interfaces }.to_not raise_error + end + end + end + + describe "#delete_unused_host_only_networks" do + context "when hostonlynets are not enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should remove host only interfaces" do + expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") + expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") + subject.delete_unused_host_only_networks + end + + it "should not read host only networks" do + expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") + expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") + expect(subject).not_to receive(:read_host_only_networks) + subject.delete_unused_host_only_networks + end + end + + context "when hostonlynets are enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + allow(subject).to receive(:read_host_only_networks).and_return([]) + allow(subject).to receive(:execute).with("list", "vms", any_args).and_return("") + end + + it "should not read host only interfaces" do + expect(subject).not_to receive(:execute).with("list", "hostonlyifs", any_args) + subject.delete_unused_host_only_networks + end + + context "when no host only networks are defined" do + before do + expect(subject).to receive(:read_host_only_networks).and_return([]) + end + + it "should not list vms" do + expect(subject).not_to receive(:execute).with("list", "vms", any_args) + subject.delete_unused_host_only_networks + end + end + + context "when host only networks are defined" do + before do + expect(subject). + to receive(:read_host_only_networks). + and_return([{name: "vagrantnet-vbox-1"}]) + + end + + context "when no vms are using the network" do + before do + expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") + end + + it "should delete the network" do + expect(subject). + to receive(:execute). + with("hostonlynet", "remove", "--name", "vagrantnet-vbox-1", any_args) + subject.delete_unused_host_only_networks + end + end + + context "when vms are using the network" do + before do + expect(subject). + to receive(:execute). + with("list", "vms", any_args). + and_return(%("VM_NAME" {VM_ID})) + expect(subject). + to receive(:execute). + with("showvminfo", "VM_ID", any_args). + and_return(%(hostonly-network="vagrantnet-vbox-1")) + end + + it "should not delete the network" do + expect(subject).not_to receive(:execute).with("hostonlynet", "remove", any_args) + subject.delete_unused_host_only_networks + end + end + end + end + end + + describe "#enable_adapters" do + let(:adapters) { + [{hostonly: "hostonlynetwork", adapter: 1}, + {bridge: "eth0", adapter: 2}] + } + + before do + allow(subject).to receive(:execute).with("modifyvm", any_args) + end + + context "when hostonlynets are not enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should only call modifyvm once" do + expect(subject).to receive(:execute).with("modifyvm", any_args).once + subject.enable_adapters(adapters) + end + + it "should configure host only network using hostonlyadapter" do + expect(subject).to receive(:execute) { |*args| + expect(args.first).to eq("modifyvm") + expect(args).to include("--hostonlyadapter1") + true + } + subject.enable_adapters(adapters) + end + end + + context "when hostonlynets are enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should call modifyvm twice" do + expect(subject).to receive(:execute).with("modifyvm", any_args).twice + subject.enable_adapters(adapters) + end + + it "should configure host only network using hostonlynet" do + expect(subject).to receive(:execute).once + expect(subject).to receive(:execute) { |*args| + expect(args.first).to eq("modifyvm") + expect(args).to include("--host-only-net1") + true + } + subject.enable_adapters(adapters) + end + end + end + + describe "#create_host_only_network" do + let(:options) { + { + adapter_ip: "127.0.0.1", + netmask: "255.255.255.0" + } + } + + context "when hostonlynets are disabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should create using hostonlyif" do + expect(subject). + to receive(:execute). + with("hostonlyif", "create", any_args). + and_return("Interface 'host_only' was successfully created") + expect(subject). + to receive(:execute). + with("hostonlyif", "ipconfig", "host_only", any_args) + subject.create_host_only_network(options) + end + end + + context "when hostonlynets are enabled" do + let(:prefix) { described_class.const_get(:HOSTONLY_NAME_PREFIX) } + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + allow(subject).to receive(:read_host_only_networks).and_return([]) + end + + it "should create using hostonlynet" do + expect(subject). + to receive(:execute). + with("hostonlynet", "add", "--name", prefix + "1", + "--netmask", options[:netmask], "--lower-ip", + "127.0.0.0", "--upper-ip", "127.0.0.0", any_args) + subject.create_host_only_network(options) + end + + context "when other host only networks exist" do + before do + expect(subject). + to receive(:read_host_only_networks). + and_return(["custom", prefix + "1", prefix + "20"].map { |n| {name: n} }) + end + + it "should create network with incremented name" do + expect(subject). + to receive(:execute). + with("hostonlynet", "add", "--name", prefix + "21", any_args) + subject.create_host_only_network(options) + end + end + + context "when dhcp information is included" do + let(:options) { + { + type: :dhcp, + dhcp_lower: "127.0.0.1", + dhcp_upper: "127.0.1.200", + netmask: "255.255.240.0" + } + } + + it "should set DHCP range" do + expect(subject). + to receive(:execute). + with("hostonlynet", "add", "--name", anything, "--netmask", options[:netmask], + "--lower-ip", options[:dhcp_lower], "--upper-ip", options[:dhcp_upper], + any_args) + subject.create_host_only_network(options) + end + end + end + end + + describe "#reconfig_host_only" do + let(:interface) { {name: "iname", ipv6: "VALUE"} } + + context "when hostonlynets are disabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should apply ipv6 update" do + expect(subject).to receive(:execute).with("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6], any_args) + subject.reconfig_host_only(interface) + end + end + + context "when hostonlynets are enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should do nothing" do + expect(subject).not_to receive(:execute) + subject.reconfig_host_only(interface) + end + end + end + + describe "#remove_dhcp_server" do + let(:dhcp_name) { double(:dhcp_name) } + + context "when hostonlynets are disabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should remove the dhcp server" do + expect(subject).to receive(:execute).with("dhcpserver", "remove", "--netname", + dhcp_name, any_args) + subject.remove_dhcp_server(dhcp_name) + end + end + + context "when hostonlynets are enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should do nothing" do + expect(subject).not_to receive(:execute) + subject.remove_dhcp_server(dhcp_name) + end + end + end + + describe "#create_dhcp_server" do + let(:network) { double("network") } + let(:options) { + { + dhcp_ip: "127.0.0.1", + netmask: "255.255.255.0", + dhcp_lower: "127.0.0.2", + dhcp_upper: "127.0.0.200" + } + } + + context "when hostonlynets is diabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should create a dhcp server" do + expect(subject).to receive(:execute).with("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], any_args) + subject.create_dhcp_server(network, options) + end + end + + context "when hostonlynets is enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should do nothing" do + expect(subject).not_to receive(:execute) + subject.create_dhcp_server(network, options) + end + end + end + + describe "#read_host_only_interfaces" do + context "when hostonlynets is diabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + allow(subject).to receive(:execute).and_return("") + end + + it "should list hostonlyifs" do + expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") + subject.read_host_only_interfaces + end + + it "should not call read_host_only_networks" do + expect(subject).not_to receive(:read_host_only_networks) + subject.read_host_only_interfaces + end + end + + context "when hostonlynets is enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). + and_return(VBOX_HOSTONLYNETS) + end + + it "should call read_host_only_networks" do + expect(subject).to receive(:read_host_only_networks).and_return([]) + subject.read_host_only_interfaces + end + + it "should return defined networks" do + expect(subject.read_host_only_interfaces.size).to eq(2) + end + + it "should add compat information to network entries" do + result = subject.read_host_only_interfaces + expect(result.first[:netmask]).to eq(result.first[:networkmask]) + expect(result.first[:status]).to eq("Up") + end + + it "should assign the address as the first in the subnet" do + result = subject.read_host_only_interfaces + expect(result.first[:ip]).to eq(IPAddr.new(result.first[:lowerip]).succ.to_s) + end + + context "when dhcp range is set" do + before do + allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). + and_return(VBOX_RANGE_HOSTONLYNETS) + end + + it "should assign the address as the first in the dhcp range" do + result = subject.read_host_only_interfaces + expect(result.first[:ip]).to eq(result.first[:lowerip]) + end + end + end + end + + describe "#read_host_only_networks" do + before do + allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). + and_return(VBOX_HOSTONLYNETS) + end + + it "should return defined networks" do + expect(subject.read_host_only_networks.size).to eq(2) + end + + it "should return expected network information" do + result = subject.read_host_only_networks + expect(result.first[:name]).to eq("vagrantnet-vbox1") + expect(result.first[:lowerip]).to eq("192.168.61.0") + expect(result.first[:networkmask]).to eq("255.255.255.0") + expect(result.last[:name]).to eq("vagrantnet-vbox2") + expect(result.last[:lowerip]).to eq("192.168.22.0") + expect(result.last[:networkmask]).to eq("255.255.255.0") + end + end + + describe "#read_network_interfaces" do + before do + allow(subject) + .to receive(:execute). + with("showvminfo", any_args). + and_return(VBOX_GUEST_HOSTONLYVNETS_INFO) + end + + context "when hostonlynets is disabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(false) + end + + it "should return two interfaces" do + valid_interfaces = subject.read_network_interfaces.find_all { |k, v| + v[:type] != :none + } + expect(valid_interfaces.size).to eq(2) + end + + it "should include a nat type" do + expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :nat }).to be + end + + it "should include a hostonlynetwork type with no information" do + expect(subject.read_network_interfaces[2]).to eq({type: :hostonlynetwork}) + end + end + + context "when hostonlynets is enabled" do + before do + allow(subject).to receive(:use_host_only_nets?).and_return(true) + end + + it "should return two interfaces" do + valid_interfaces = subject.read_network_interfaces.find_all { |k, v| + v[:type] != :none + } + expect(valid_interfaces.size).to eq(2) + end + + it "should include a nat type" do + expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :nat }).to be + end + + it "should include a hostonly type" do + expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :hostonly }).to be + end + + it "should not include a hostonlynetwork type" do + expect(subject.read_network_interfaces.detect { |_, v| + v[:type] == :hostonlynetwork + }).to_not be + end + + it "should include the hostonly network name" do + hostonly = subject.read_network_interfaces.values.detect { |v| + v[:type] == :hostonly + } + expect(hostonly).to be + expect(hostonly[:hostonly]).to eq("vagrantnet-vbox1") + end + end + end +end + +VBOX_VMCONFIG_FILE=%( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + + +VBOX_BRIDGEDIFS=%(Name: en1: Wi-Fi (AirPort) +GUID: 00000000-0000-0000-0000-000000000001 +DHCP: Disabled +IPAddress: 10.0.0.49 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: xx:xx:xx:xx:xx:01 +MediumType: Ethernet +Wireless: Yes +Status: Up +VBoxNetworkName: HostInterfaceNetworking-en1 + +Name: en0: Ethernet +GUID: 00000000-0000-0000-0000-000000000002 +DHCP: Disabled +IPAddress: 0.0.0.0 +NetworkMask: 0.0.0.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: xx:xx:xx:xx:xx:02 +MediumType: Ethernet +Wireless: No +Status: Up +VBoxNetworkName: HostInterfaceNetworking-en0 + +Name: bridge100 +GUID: 00000000-0000-0000-0000-000000000003 +DHCP: Disabled +IPAddress: 192.168.61.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: xx:xx:xx:xx:xx:03 +MediumType: Ethernet +Wireless: No +Status: Up +VBoxNetworkName: HostInterfaceNetworking-bridge100 + +Name: en2: Thunderbolt 1 +GUID: 00000000-0000-0000-0000-000000000004 +DHCP: Disabled +IPAddress: 0.0.0.0 +NetworkMask: 0.0.0.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: xx:xx:xx:xx:xx:04 +MediumType: Ethernet +Wireless: No +Status: Up +VBoxNetworkName: HostInterfaceNetworking-en2 + +Name: bridge101 +GUID: 00000000-0000-0000-0000-000000000005 +DHCP: Disabled +IPAddress: 192.168.22.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: xx:xx:xx:xx:xx:05 +MediumType: Ethernet +Wireless: No +Status: Up +VBoxNetworkName: HostInterfaceNetworking-bridge101) + +VBOX_HOSTONLYNETS=%(Name: vagrantnet-vbox1 +GUID: 10000000-0000-0000-0000-000000000000 + +State: Enabled +NetworkMask: 255.255.255.0 +LowerIP: 192.168.61.0 +UpperIP: 192.168.61.0 +VBoxNetworkName: hostonly-vagrantnet-vbox1 + +Name: vagrantnet-vbox2 +GUID: 20000000-0000-0000-0000-000000000000 + +State: Enabled +NetworkMask: 255.255.255.0 +LowerIP: 192.168.22.0 +UpperIP: 192.168.22.0 +VBoxNetworkName: hostonly-vagrantnet-vbox2) + +VBOX_RANGE_HOSTONLYNETS=%(Name: vagrantnet-vbox1 +GUID: 10000000-0000-0000-0000-000000000000 + +State: Enabled +NetworkMask: 255.255.255.0 +LowerIP: 192.168.61.10 +UpperIP: 192.168.61.100 +VBoxNetworkName: hostonly-vagrantnet-vbox1 + +Name: vagrantnet-vbox2 +GUID: 20000000-0000-0000-0000-000000000000 + +State: Enabled +NetworkMask: 255.255.255.0 +LowerIP: 192.168.22.0 +UpperIP: 192.168.22.0 +VBoxNetworkName: hostonly-vagrantnet-vbox2) + +VBOX_GUEST_HOSTONLYVNETS_INFO=%( +natnet1="nat" +macaddress1="080027BB1475" +cableconnected1="on" +nic1="nat" +nictype1="82540EM" +nicspeed1="0" +mtu="0" +sockSnd="64" +sockRcv="64" +tcpWndSnd="64" +tcpWndRcv="64" +Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" +hostonly-network2="vagrantnet-vbox1" +macaddress2="080027FBC15B" +cableconnected2="on" +nic2="hostonlynetwork" +nictype2="82540EM" +nicspeed2="0" +nic3="none" +nic4="none" +nic5="none" +nic6="none" +nic7="none" +nic8="none" +) diff --git a/website/content/docs/providers/virtualbox/index.mdx b/website/content/docs/providers/virtualbox/index.mdx index 96f47151c76..6c685971f0e 100644 --- a/website/content/docs/providers/virtualbox/index.mdx +++ b/website/content/docs/providers/virtualbox/index.mdx @@ -12,7 +12,7 @@ Vagrant comes with support out of the box for [VirtualBox](https://www.virtualbo a free, cross-platform consumer virtualization product. The VirtualBox provider is compatible with VirtualBox versions 4.0.x, 4.1.x, -4.2.x, 4.3.x, 5.0.x, 5.1.x, 5.2.x, 6.0.x, 6.1.x and 7.0.x. Other versions are unsupported and the provider +4.2.x, 4.3.x, 5.0.x, 5.1.x, 5.2.x, 6.0.x, 6.1.x, 7.0.x and 7.1.x. Other versions are unsupported and the provider will display an error message. Please note that beta and pre-release versions of VirtualBox are not supported and may not be well-behaved.