require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 5.0.x class Version_5_0 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_5_0") @uuid = uuid end def clear_forwarded_ports retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end end def clear_shared_folders retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) end end end end def clonevm(master_id, snapshot_name) machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" args = ["--register", "--name", machine_name] if snapshot_name args += ["--snapshot", snapshot_name, "--options", "link"] end execute("clonevm", master_id, *args, retryable: true) return get_machine_id(machine_name) end def create_dhcp_server(network, options) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") rescue Vagrant::Errors::VBoxManageError => e return if e.extra_data[:stderr] == 'VBoxManage: error: DHCP server already exists' raise end end end def create_host_only_network(options) # Create the interface execute("hostonlyif", "create", retryable: true) =~ /^Interface '(.+?)' was successfully created$/ name = $1.to_s # Get the IP so we can determine v4 vs v6 ip = IPAddr.new(options[:adapter_ip]) # Configure if ip.ipv4? execute("hostonlyif", "ipconfig", name, "--ip", options[:adapter_ip], "--netmask", options[:netmask], retryable: true) elsif ip.ipv6? execute("hostonlyif", "ipconfig", name, "--ipv6", options[:adapter_ip], "--netmasklengthv6", options[:netmask].to_s, retryable: true) else raise "BUG: Unknown IP type: #{ip.inspect}" end # Return the details return { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } end def create_snapshot(machine_id, snapshot_name) execute("snapshot", machine_id, "take", snapshot_name, retryable: true) end def delete_snapshot(machine_id, snapshot_name) # Start with 0% last = 0 total = "" yield 0 if block_given? # Snapshot and report the % progress execute("snapshot", machine_id, "delete", snapshot_name, retryable: true) do |type, data| if type == :stderr # Append the data so we can see the full view total << data.gsub("\r", "") # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. match = /.+(\d{2})%/.match(lines.last) if match current = match[1].to_i if current > last last = current yield current if block_given? end end end end end def list_snapshots(machine_id) output = execute( "snapshot", machine_id, "list", "--machinereadable", retryable: true) result = [] output.split("\n").each do |line| if line =~ /^SnapshotName.*?="(.+?)"$/i result << $1.to_s end end result.sort rescue Vagrant::Errors::VBoxManageError => e d = e.extra_data return [] if d[:stderr].include?("does not have") || d[:stdout].include?("does not have") raise end def restore_snapshot(machine_id, snapshot_name) # Start with 0% last = 0 total = "" yield 0 if block_given? execute("snapshot", machine_id, "restore", snapshot_name, retryable: true) do |type, data| if type == :stderr # Append the data so we can see the full view total << data.gsub("\r", "") # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. match = /.+(\d{2})%/.match(lines.last) if match current = match[1].to_i if current > last last = current yield current if block_given? end end end end end def delete execute("unregistervm", @uuid, "--delete", retryable: true) end def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs", retryable: true).split("\n").each do |line| networks << $1.to_s if line =~ /^Name:\s+(.+?)$/ end execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ begin info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) info.split("\n").each do |inner_line| if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ networks.delete($1.to_s) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name, retryable: true) end end def discard_saved_state execute("discardstate", @uuid, retryable: true) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:intnet] args.concat(["--intnet#{adapter[:adapter]}", adapter[:intnet], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args, retryable: true) end def execute_command(command) execute(*command) end def export(path) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("export", @uuid, "--output", path.to_s) rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VERR_E_FILE_ERROR") # If the file already exists we'll throw a custom error raise Vagrant::Errors::VirtualBoxFileExists, stderr: e.extra_data[:stderr] end end end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end execute("modifyvm", @uuid, *args, retryable: true) if !args.empty? end def get_machine_id(machine_name) output = execute("list", "vms", retryable: true) match = /^"#{Regexp.escape(machine_name)}" \{(.+?)\}$/.match(output) return match[1].to_s if match nil end def halt execute("controlvm", @uuid, "poweroff", retryable: true) end def import(ovf) ovf = Vagrant::Util::Platform.windows_path(ovf) output = "" total = "" last = 0 # Dry-run the import to get the suggested name and path @logger.debug("Doing dry-run import to determine parallel-safe name...") output = execute("import", "-n", ovf) result = /Suggested VM name "(.+?)"/.match(output) if !result raise Vagrant::Errors::VirtualBoxNoName, output: output end suggested_name = result[1].to_s # Append millisecond plus a random to the path in case we're # importing the same box elsewhere. specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" @logger.debug("-- Parallel safe name: #{specified_name}") # Build the specified name param list name_params = [ "--vsys", "0", "--vmname", specified_name, ] # Extract the disks list and build the disk target params disk_params = [] disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/) disks.each do |unit_num, path| disk_params << "--vsys" disk_params << "0" disk_params << "--unit" disk_params << unit_num disk_params << "--disk" if Vagrant::Util::Platform.windows? # we use the block form of sub here to ensure that if the specified_name happens to end with a number (which is fairly likely) then # we won't end up having the character sequence of a \ followed by a number be interpreted as a back reference. For example, if # specified_name were "abc123", then "\\abc123\\".reverse would be "\\321cba\\", and the \3 would be treated as a back reference by the sub disk_params << path.reverse.sub("\\#{suggested_name}\\".reverse) { "\\#{specified_name}\\".reverse }.reverse # Replace only last occurrence else disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end end execute("import", ovf , *name_params, *disk_params, retryable: true) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif type == :stderr # Append the data so we can see the full view total << data.gsub("\r", "") # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") if lines.include?("OK.") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. match = /.+(\d{2})%/.match(lines.last) if match current = match[1].to_i if current > last last = current yield current if block_given? end end end end end return get_machine_id specified_name end def max_network_adapters 8 end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. current_nic = $1.to_i if line =~ /^nic(\d+)=".+?"$/ # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && line =~ /^VMState="(.+?)"$/ && $1.to_s != "running" return [] end # Parse out the forwarded port information # Forwarding(1)="172.22.8.201tcp32977,tcp,172.22.8.201,32977,,3777" # Forwarding(2)="tcp32978,tcp,,32978,,3777" if line =~ /^Forwarding.+?="(.+?),.+?,(.*?),(.+?),.*?,(.+?)"$/ result = [current_nic, $1.to_s, $3.to_i, $4.to_i, $2] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if output =~ /^Value: (.+?)$/ # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. value = $1.to_s return value.split("_").first end # If we can't get the guest additions version by guest property, try # to get it from the VM info itself. info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^GuestAdditionsVersion="(.+?)"$/ end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^IPV6Address:\s+(.+?)$/ info[:ipv6] = $1.to_s.strip elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^macaddress1="(.+?)"$/ end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder execute("list", "systemproperties", retryable: true).split("\n").each do |line| if line =~ /^Default machine folder:\s+(.+?)$/i return $1.to_s end end nil end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^nic(\d+)="(.+?)"$/ adapter = $1.to_i type = $2.to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif line =~ /^hostonlyadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif line =~ /^bridgeadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif output =~ /^VMState="(.+?)"$/ return $1.to_sym end nil end def read_used_ports used_ports = Hash.new{|hash, key| hash[key] = Set.new} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?" \{(.+?)\}$/ uuid = $1.to_s # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _, hostip| hostip = '*' if hostip.nil? || hostip.empty? used_ports[hostport].add?(hostip) end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end used_ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end results end def reconfig_host_only(interface) execute("hostonlyif", "ipconfig", interface[:name], "--ipv6", interface[:ipv6], retryable: true) end def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name, retryable: true) end def set_mac_address(mac) execute("modifyvm", @uuid, "--macaddress1", mac, retryable: true) end def set_name(name) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("modifyvm", @uuid, "--name", name) rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS") # We got VERR_ALREADY_EXISTS. This means that we're renaming to # a VM name that already exists. Raise a custom error. raise Vagrant::Errors::VirtualBoxNameExists, stderr: e.extra_data[:stderr] end end end def share_folders(folders) is_solaris = begin "SunOS" == read_guest_property("/VirtualBox/GuestInfo/OS/Product") rescue false end folders.each do |folder| # NOTE: Guest additions on Solaris guests do not properly handle # UNC style paths so prevent conversion (See GH-7264) if is_solaris hostpath = folder[:hostpath] else hostpath = Vagrant::Util::Platform.windows_path(folder[:hostpath]) end args = ["--name", folder[:name], "--hostpath", hostpath] args << "--transient" if folder.key?(:transient) && folder[:transient] if folder[:SharedFoldersEnableSymlinksCreate] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1", retryable: true) end # Add the shared folder execute("sharedfolder", "add", @uuid, *args, retryable: true) end end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port only by comparing the guest port read_forwarded_ports.each do |_, _, hostport, guestport| return hostport if guestport == expected_port end nil end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end end def suspend execute("controlvm", @uuid, "savestate", retryable: true) end def unshare_folders(names) names.each do |name| retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") end end end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs", retryable: true) end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) 5.times do |i| result = raw("showvminfo", uuid) return true if result.exit_code == 0 # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND, # then the vm truly does not exist. Any other error might be transient return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND") # Sleep a bit though to give VirtualBox time to fix itself sleep 2 end # If we reach this point, it means that we consistently got the # failure, do a standard vboxmanage now. This will raise an # exception if it fails again. execute("showvminfo", uuid) return true end protected def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" return false else return true end end end end end end