Skip to content

Commit

Permalink
Add VirtualBox 7.1.x Support
Browse files Browse the repository at this point in the history
Fixes hashicorp#13501

Signed-off-by: Wong Hoi Sing Edison <[email protected]>
  • Loading branch information
hswong3i committed Sep 18, 2024
1 parent ae6a175 commit 507d598
Show file tree
Hide file tree
Showing 5 changed files with 1,164 additions and 1 deletion.
1 change: 1 addition & 0 deletions plugins/providers/virtualbox/driver/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
305 changes: 305 additions & 0 deletions plugins/providers/virtualbox/driver/version_7_1.rb
Original file line number Diff line number Diff line change
@@ -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+\{(?<vmid>.+?)\}$/)
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(?<adapter>\d+)="(?<type>.+?)"$/)
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(?<adapter>\d+)="(?<network>.+?)"$/)
nics[m[:adapter].to_i] ||= {}
nics[m[:adapter].to_i][:bridge] = m[:network]
elsif m = line.match(/^hostonly-network(?<adapter>\d+)="(?<network>.+?)"$/)
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="(?<path>.+?)"/)
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
1 change: 1 addition & 0 deletions plugins/providers/virtualbox/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 507d598

Please sign in to comment.