# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 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+\{(?.+?)\}$/) 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.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="(?.+?)"/) 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