2022-12-08 11:14:46 -08:00

303 lines
11 KiB
Ruby

require "rexml"
require File.expand_path("../version_6_1", __FILE__)
module VagrantPlugins
module ProviderVirtualBox
module Driver
# Driver for VirtualBox 7.0.x
class Version_7_0 < Version_6_1
# 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_0")
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.0 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.0.0 release.
return super if get_version.to_s != "7.0.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, "//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