|
| 1 | +# Copyright (c) HashiCorp, Inc. |
| 2 | +# SPDX-License-Identifier: BUSL-1.1 |
| 3 | + |
| 4 | +require "rexml" |
| 5 | +require File.expand_path("../version_7_0", __FILE__) |
| 6 | + |
| 7 | +module VagrantPlugins |
| 8 | + module ProviderVirtualBox |
| 9 | + module Driver |
| 10 | + # Driver for VirtualBox 7.1.x |
| 11 | + class Version_7_1 < Version_7_0 |
| 12 | + # VirtualBox version requirement for using host only networks |
| 13 | + # instead of host only interfaces |
| 14 | + HOSTONLY_NET_REQUIREMENT=Gem::Requirement.new(">= 7") |
| 15 | + # Prefix of name used for host only networks |
| 16 | + HOSTONLY_NAME_PREFIX="vagrantnet-vbox" |
| 17 | + DEFAULT_NETMASK="255.255.255.0" |
| 18 | + |
| 19 | + def initialize(uuid) |
| 20 | + super |
| 21 | + |
| 22 | + @logger = Log4r::Logger.new("vagrant::provider::virtualbox_7_1") |
| 23 | + end |
| 24 | + |
| 25 | + def read_bridged_interfaces |
| 26 | + ifaces = super |
| 27 | + return ifaces if !use_host_only_nets? |
| 28 | + |
| 29 | + # Get a list of all subnets which are in use for hostonly networks |
| 30 | + hostonly_ifaces = read_host_only_networks.map do |net| |
| 31 | + IPAddr.new(net[:lowerip]).mask(net[:networkmask]) |
| 32 | + end |
| 33 | + |
| 34 | + # Prune any hostonly interfaces in the list |
| 35 | + ifaces.delete_if { |i| |
| 36 | + addr = begin |
| 37 | + IPAddr.new(i[:ip]).mask(i[:netmask]) |
| 38 | + rescue IPAddr::Error => err |
| 39 | + @logger.warn("skipping bridged interface due to parse error #{err} (#{i}) ") |
| 40 | + nil |
| 41 | + end |
| 42 | + addr.nil? || |
| 43 | + hostonly_ifaces.include?(addr) |
| 44 | + } |
| 45 | + |
| 46 | + ifaces |
| 47 | + end |
| 48 | + |
| 49 | + def delete_unused_host_only_networks |
| 50 | + return super if !use_host_only_nets? |
| 51 | + |
| 52 | + # First get the list of existing host only network names |
| 53 | + network_names = read_host_only_networks.map { |net| net[:name] } |
| 54 | + # Prune the network names to only include ones we manage |
| 55 | + network_names.delete_if { |name| !name.start_with?(HOSTONLY_NAME_PREFIX) } |
| 56 | + |
| 57 | + @logger.debug("managed host only network names: #{network_names}") |
| 58 | + |
| 59 | + return if network_names.empty? |
| 60 | + |
| 61 | + # Next get the list of host only networks currently in use |
| 62 | + inuse_names = [] |
| 63 | + execute("list", "vms", retryable: true).split("\n").each do |line| |
| 64 | + match = line.match(/^".+?"\s+\{(?<vmid>.+?)\}$/) |
| 65 | + next if match.nil? |
| 66 | + begin |
| 67 | + info = execute("showvminfo", match[:vmid].to_s, "--machinereadable", retryable: true) |
| 68 | + info.split("\n").each do |vmline| |
| 69 | + if vmline.start_with?("hostonly-network") |
| 70 | + net_name = vmline.split("=", 2).last.to_s.gsub('"', "") |
| 71 | + inuse_names << net_name |
| 72 | + end |
| 73 | + end |
| 74 | + rescue Vagrant::Errors::VBoxManageError => err |
| 75 | + raise if !err.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") |
| 76 | + end |
| 77 | + end |
| 78 | + |
| 79 | + @logger.debug("currently in use network names: #{inuse_names}") |
| 80 | + |
| 81 | + # Now remove all the networks not in use |
| 82 | + (network_names - inuse_names).each do |name| |
| 83 | + execute("hostonlynet", "remove", "--name", name, retryable: true) |
| 84 | + end |
| 85 | + end |
| 86 | + |
| 87 | + def enable_adapters(adapters) |
| 88 | + return super if !use_host_only_nets? |
| 89 | + |
| 90 | + hostonly_adapters = adapters.find_all { |adapter| adapter[:hostonly] } |
| 91 | + other_adapters = adapters - hostonly_adapters |
| 92 | + super(other_adapters) if !other_adapters.empty? |
| 93 | + |
| 94 | + if !hostonly_adapters.empty? |
| 95 | + args = [] |
| 96 | + hostonly_adapters.each do |adapter| |
| 97 | + args.concat(["--nic#{adapter[:adapter]}", "hostonlynet"]) |
| 98 | + args.concat(["--host-only-net#{adapter[:adapter]}", adapter[:hostonly], |
| 99 | + "--cableconnected#{adapter[:adapter]}", "on"]) |
| 100 | + end |
| 101 | + |
| 102 | + execute("modifyvm", @uuid, *args, retryable: true) |
| 103 | + end |
| 104 | + end |
| 105 | + |
| 106 | + def create_host_only_network(options) |
| 107 | + # If we are not on macOS, just setup the hostonly interface |
| 108 | + return super if !use_host_only_nets? |
| 109 | + |
| 110 | + opts = { |
| 111 | + netmask: options.fetch(:netmask, DEFAULT_NETMASK), |
| 112 | + } |
| 113 | + |
| 114 | + if options[:type] == :dhcp |
| 115 | + opts[:lower] = options[:dhcp_lower] |
| 116 | + opts[:upper] = options[:dhcp_upper] |
| 117 | + else |
| 118 | + addr = IPAddr.new(options[:adapter_ip]) |
| 119 | + opts[:upper] = opts[:lower] = addr.mask(opts[:netmask]).to_range.first.to_s |
| 120 | + end |
| 121 | + |
| 122 | + name_idx = read_host_only_networks.map { |hn| |
| 123 | + next if !hn[:name].start_with?(HOSTONLY_NAME_PREFIX) |
| 124 | + hn[:name].sub(HOSTONLY_NAME_PREFIX, "").to_i |
| 125 | + }.compact.max.to_i + 1 |
| 126 | + opts[:name] = HOSTONLY_NAME_PREFIX + name_idx.to_s |
| 127 | + |
| 128 | + execute("hostonlynet", "add", |
| 129 | + "--name", opts[:name], |
| 130 | + "--netmask", opts[:netmask], |
| 131 | + "--lower-ip", opts[:lower], |
| 132 | + "--upper-ip", opts[:upper], |
| 133 | + retryable: true) |
| 134 | + |
| 135 | + { |
| 136 | + name: opts[:name], |
| 137 | + ip: options[:adapter_ip], |
| 138 | + netmask: opts[:netmask], |
| 139 | + } |
| 140 | + end |
| 141 | + |
| 142 | + # Disabled when host only nets are in use |
| 143 | + def reconfig_host_only(options) |
| 144 | + return super if !use_host_only_nets? |
| 145 | + end |
| 146 | + |
| 147 | + # Disabled when host only nets are in use since |
| 148 | + # the host only nets will provide the dhcp server |
| 149 | + def remove_dhcp_server(*_, **_) |
| 150 | + super if !use_host_only_nets? |
| 151 | + end |
| 152 | + |
| 153 | + # Disabled when host only nets are in use since |
| 154 | + # the host only nets will provide the dhcp server |
| 155 | + def create_dhcp_server(*_, **_) |
| 156 | + super if !use_host_only_nets? |
| 157 | + end |
| 158 | + |
| 159 | + def read_host_only_interfaces |
| 160 | + return super if !use_host_only_nets? |
| 161 | + |
| 162 | + # When host only nets are in use, read them and |
| 163 | + # reformat the information to line up with how |
| 164 | + # the interfaces is structured |
| 165 | + read_host_only_networks.map do |net| |
| 166 | + addr = begin |
| 167 | + IPAddr.new(net[:lowerip]) |
| 168 | + rescue IPAddr::Error => err |
| 169 | + @logger.warn("invalid host only network lower IP encountered: #{err} (#{net})") |
| 170 | + next |
| 171 | + end |
| 172 | + # Address of the interface will be the lower bound of the range or |
| 173 | + # the first available address in the subnet |
| 174 | + if addr == addr.mask(net[:networkmask]) |
| 175 | + addr = addr.succ |
| 176 | + end |
| 177 | + |
| 178 | + net[:netmask] = net[:networkmask] |
| 179 | + if addr.ipv4? |
| 180 | + net[:ip] = addr.to_s |
| 181 | + net[:ipv6] = "" |
| 182 | + else |
| 183 | + net[:ip] = "" |
| 184 | + net[:ipv6] = addr.to_s |
| 185 | + net[:ipv6_prefix] = net[:netmask] |
| 186 | + end |
| 187 | + |
| 188 | + net[:status] = net[:state] == "Enabled" ? "Up" : "Down" |
| 189 | + |
| 190 | + net |
| 191 | + end.compact |
| 192 | + end |
| 193 | + |
| 194 | + def read_network_interfaces |
| 195 | + return super if !use_host_only_nets? |
| 196 | + |
| 197 | + {}.tap do |nics| |
| 198 | + execute("showvminfo", @uuid, "--machinereadable", retryable: true).each_line do |line| |
| 199 | + if m = line.match(/nic(?<adapter>\d+)="(?<type>.+?)"$/) |
| 200 | + nics[m[:adapter].to_i] ||= {} |
| 201 | + if m[:type] == "hostonlynetwork" |
| 202 | + nics[m[:adapter].to_i][:type] = :hostonly |
| 203 | + else |
| 204 | + nics[m[:adapter].to_i][:type] = m[:type].to_sym |
| 205 | + end |
| 206 | + elsif m = line.match(/^bridgeadapter(?<adapter>\d+)="(?<network>.+?)"$/) |
| 207 | + nics[m[:adapter].to_i] ||= {} |
| 208 | + nics[m[:adapter].to_i][:bridge] = m[:network] |
| 209 | + elsif m = line.match(/^hostonly-network(?<adapter>\d+)="(?<network>.+?)"$/) |
| 210 | + nics[m[:adapter].to_i] ||= {} |
| 211 | + nics[m[:adapter].to_i][:hostonly] = m[:network] |
| 212 | + end |
| 213 | + end |
| 214 | + end |
| 215 | + end |
| 216 | + |
| 217 | + # Generate list of host only networks |
| 218 | + def read_host_only_networks |
| 219 | + networks = [] |
| 220 | + current = nil |
| 221 | + execute("list", "hostonlynets", retryable: true).split("\n").each do |line| |
| 222 | + line.chomp! |
| 223 | + next if line.empty? |
| 224 | + key, value = line.split(":", 2).map(&:strip) |
| 225 | + key = key.downcase |
| 226 | + if key == "name" |
| 227 | + networks.push(current) if !current.nil? |
| 228 | + current = Vagrant::Util::HashWithIndifferentAccess.new |
| 229 | + end |
| 230 | + current[key] = value |
| 231 | + end |
| 232 | + networks.push(current) if !current.nil? |
| 233 | + |
| 234 | + networks |
| 235 | + end |
| 236 | + |
| 237 | + # The initial VirtualBox 7.1 release has an issue with displaying port |
| 238 | + # forward information. When a single port forward is defined, the forwarding |
| 239 | + # information can be found in the `showvminfo` output. Once more than a |
| 240 | + # single port forward is defined, no forwarding information is provided |
| 241 | + # in the `showvminfo` output. To work around this we grab the VM configuration |
| 242 | + # file from the `showvminfo` output and extract the port forward information |
| 243 | + # from there instead. |
| 244 | + def read_forwarded_ports(uuid=nil, active_only=false) |
| 245 | + # Only use this override for the 7.1.0 release. |
| 246 | + return super if get_version.to_s != "7.1.0" |
| 247 | + |
| 248 | + uuid ||= @uuid |
| 249 | + |
| 250 | + @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") |
| 251 | + |
| 252 | + results = [] |
| 253 | + |
| 254 | + info = execute("showvminfo", uuid, "--machinereadable", retryable: true) |
| 255 | + result = info.match(/CfgFile="(?<path>.+?)"/) |
| 256 | + if result.nil? |
| 257 | + raise Vagrant::Errors::VirtualBoxConfigNotFound, |
| 258 | + uuid: uuid |
| 259 | + end |
| 260 | + |
| 261 | + File.open(result[:path], "r") do |f| |
| 262 | + doc = REXML::Document.new(f) |
| 263 | + networks = REXML::XPath.each(doc.root, "Machine/Hardware/Network/Adapter") |
| 264 | + networks.each do |net| |
| 265 | + REXML::XPath.each(doc.root, net.xpath + "/NAT/Forwarding") do |fwd| |
| 266 | + # Result Array values: |
| 267 | + # [NIC Slot, Name, Host Port, Guest Port, Host IP] |
| 268 | + result = [ |
| 269 | + net.attribute("slot").value.to_i + 1, |
| 270 | + fwd.attribute("name")&.value.to_s, |
| 271 | + fwd.attribute("hostport")&.value.to_i, |
| 272 | + fwd.attribute("guestport")&.value.to_i, |
| 273 | + fwd.attribute("hostip")&.value.to_s |
| 274 | + ] |
| 275 | + @logger.debug(" - #{result.inspect}") |
| 276 | + results << result |
| 277 | + end |
| 278 | + end |
| 279 | + end |
| 280 | + |
| 281 | + results |
| 282 | + end |
| 283 | + |
| 284 | + private |
| 285 | + |
| 286 | + # Returns if hostonlynets are enabled on the current |
| 287 | + # host platform |
| 288 | + # |
| 289 | + # @return [Boolean] |
| 290 | + def use_host_only_nets? |
| 291 | + Vagrant::Util::Platform.darwin? && |
| 292 | + HOSTONLY_NET_REQUIREMENT.satisfied_by?(get_version) |
| 293 | + end |
| 294 | + |
| 295 | + # VirtualBox version in use |
| 296 | + # |
| 297 | + # @return [Gem::Version] |
| 298 | + def get_version |
| 299 | + return @version if @version |
| 300 | + @version = Gem::Version.new(Meta.new.version) |
| 301 | + end |
| 302 | + end |
| 303 | + end |
| 304 | + end |
| 305 | +end |
0 commit comments