diff --git a/plugins/providers/virtualbox/driver/version_6_0.rb b/plugins/providers/virtualbox/driver/version_6_0.rb index 859268722..5f17b6af2 100644 --- a/plugins/providers/virtualbox/driver/version_6_0.rb +++ b/plugins/providers/virtualbox/driver/version_6_0.rb @@ -10,6 +10,95 @@ module VagrantPlugins @logger = Log4r::Logger.new("vagrant::provider::virtualbox_6_0") 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, + ] + + # Target path for disks is no longer a full path. Extract the path for the + # settings file to determine the base directory which we can then use to + # build the disk paths + result = /Suggested VM settings file name "(?.+?)"/.match(output) + if !result + @logger.warn("Failed to locate base path for disks. Using current working directory.") + base_path = "." + else + base_path = File.dirname(result[:settings_path]) + end + + @logger.info("Base path for disk import: #{base_path}") + + # 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| + path = File.join(base_path, File.basename(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 + end end end diff --git a/test/unit/plugins/providers/virtualbox/base.rb b/test/unit/plugins/providers/virtualbox/base.rb index 75016267e..d10334b56 100644 --- a/test/unit/plugins/providers/virtualbox/base.rb +++ b/test/unit/plugins/providers/virtualbox/base.rb @@ -2,3 +2,4 @@ require_relative "../../../base" require_relative "support/shared/virtualbox_driver_version_4_x_examples" +require_relative "support/shared/virtualbox_driver_version_5_x_examples" diff --git a/test/unit/plugins/providers/virtualbox/driver/version_5_0_test.rb b/test/unit/plugins/providers/virtualbox/driver/version_5_0_test.rb index 2cc124027..d094028ca 100644 --- a/test/unit/plugins/providers/virtualbox/driver/version_5_0_test.rb +++ b/test/unit/plugins/providers/virtualbox/driver/version_5_0_test.rb @@ -1,3 +1,4 @@ +require "pathname" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_5_0 do @@ -8,127 +9,84 @@ describe VagrantPlugins::ProviderVirtualBox::Driver::Version_5_0 do subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_5_0.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" + it_behaves_like "a version 5.x virtualbox driver" - describe "#shared_folders" do - let(:folders) { [{:name=>"folder", - :hostpath=>"/Users/brian/vagrant-folder", - :transient=>false, - :SharedFoldersEnableSymlinksCreate=>true}]} + describe "#import" do + let(:ovf) { double("ovf") } + let(:machine_id) { double("machine_id") } + let(:output) {<<-OUTPUT +0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% +Interpreting /home/user/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box.ovf... +OK. +Disks: + vmdisk1 85899345920 -1 http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized box-disk1.vmdk -1 -1 - let(:folders_automount) { [{:name=>"folder", - :hostpath=>"/Users/brian/vagrant-folder", - :transient=>false, - :automount=>true, - :SharedFoldersEnableSymlinksCreate=>true}]} - - let(:folders_disabled) { [{:name=>"folder", - :hostpath=>"/Users/brian/vagrant-folder", - :transient=>false, - :SharedFoldersEnableSymlinksCreate=>false}]} - - it "enables SharedFoldersEnableSymlinksCreate if true" do - expect(subprocess).to receive(:execute). - with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:notify=>[:stdout, :stderr]}). - and_return(subprocess_result(exit_code: 0)) - - expect(subprocess).to receive(:execute). - with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:notify=>[:stdout, :stderr]}). - and_return(subprocess_result(exit_code: 0)) - subject.share_folders(folders) - - end - - it "enables automount if option is true" do - expect(subprocess).to receive(:execute). - with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:notify=>[:stdout, :stderr]}). - and_return(subprocess_result(exit_code: 0)) - - expect(subprocess).to receive(:execute). - with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", "--automount", {:notify=>[:stdout, :stderr]}). - and_return(subprocess_result(exit_code: 0)) - subject.share_folders(folders_automount) - - end - - - it "disables SharedFoldersEnableSymlinksCreate if false" do - expect(subprocess).to receive(:execute). - with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:notify=>[:stdout, :stderr]}). - and_return(subprocess_result(exit_code: 0)) - subject.share_folders(folders_disabled) - - end - end - - describe "#set_mac_address" do - let(:mac) { "00:00:00:00:00:00" } - - after { subject.set_mac_address(mac) } - - it "should modify vm and set mac address" do - expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", mac, anything). - and_return(subprocess_result(exit_code: 0)) - end - - context "when mac address is falsey" do - let(:mac) { nil } - - it "should modify vm and set mac address to automatic value" do - expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", "auto", anything). - and_return(subprocess_result(exit_code: 0)) - end - end - end - - describe "#ssh_port" do - let(:forwards) { - [[1, "ssh", 2222, 22, "127.0.0.1"], - [1, "ssh", 8080, 80, ""]] +Virtual system 0: + 0: Suggested OS type: "Ubuntu_64" + (change with "--vsys 0 --ostype "; use "list ostypes" to list all possible values) + 1: Suggested VM name "precise64" + (change with "--vsys 0 --vmname ") + 2: Number of CPUs: 2 + (change with "--vsys 0 --cpus ") + 3: Guest memory: 384 MB + (change with "--vsys 0 --memory ") + 4: Network adapter: orig NAT, config 3, extra slot=0;type=NAT + 5: CD-ROM + (disable with "--vsys 0 --unit 5 --ignore") + 6: IDE controller, type PIIX4 + (disable with "--vsys 0 --unit 6 --ignore") + 7: IDE controller, type PIIX4 + (disable with "--vsys 0 --unit 7 --ignore") + 8: SATA controller, type AHCI + (disable with "--vsys 0 --unit 8 --ignore") + 9: Hard disk image: source image=box-disk1.vmdk, target path=/home/user/VirtualBox VMs/precise64/box-disk1.vmdk, controller=8;channel=0 + (change target path with "--vsys 0 --unit 9 --disk path"; + disable with "--vsys 0 --unit 9 --ignore") +OUTPUT } - before { allow(subject).to receive(:read_forwarded_ports).and_return(forwards) } - - it "should return the host port" do - expect(subject.ssh_port(22)).to eq(2222) + before do + allow(Vagrant::Util::Platform).to receive(:windows_path). + with(ovf).and_return(ovf) + allow(subject).to receive(:execute).with("import", "-n", ovf). + and_return(output) + allow(subject).to receive(:execute).with("import", ovf, any_args) + allow(subject).to receive(:get_machine_id).and_return(machine_id) end - context "when multiple matches are available" do - let(:forwards) { - [[1, "ssh", 2222, 22, "127.0.0.1"], - [1, "", 2221, 22, ""]] - } - - it "should choose localhost port forward" do - expect(subject.ssh_port(22)).to eq(2222) - end - - context "when multiple named matches are available" do - let(:forwards) { - [[1, "ssh", 2222, 22, "127.0.0.1"], - [1, "SSH", 2221, 22, "127.0.0.1"]] - } - - it "should choose lowercased name forward" do - expect(subject.ssh_port(22)).to eq(2222) - end - end + it "should return the machine id" do + expect(subject).to receive(:get_machine_id).and_return(machine_id) + expect(subject.import(ovf)).to eq(machine_id) end - context "when only ports are defined" do - let(:forwards) { - [[1, "", 2222, 22, ""]] - } - - it "should return the host port" do - expect(subject.ssh_port(22)).to eq(2222) - end + it "should return machine id using custom name" do + expect(subject).to receive(:get_machine_id).with(/.*precise64_.+/).and_return(machine_id) + expect(subject.import(ovf)).to eq(machine_id) end - context "when no matches are available" do - let(:forwards) { [] } + it "should include disk image on import" do + expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) + expect(subject).to receive(:execute) do |*args| + match = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } + expect(match).to include("disk1.vmdk") + end + expect(subject.import(ovf)).to eq(machine_id) + end - it "should return nil" do - expect(subject.ssh_port(22)).to be_nil + it "should include full path for disk image on import" do + expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) + expect(subject).to receive(:execute) do |*args| + dpath = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } + expect(Pathname.new(dpath).absolute?).to be_truthy + end + expect(subject.import(ovf)).to eq(machine_id) + end + + context "suggested name is not provided" do + before { output.sub!(/Suggested VM name/, "") } + + it "should raise an error" do + expect { subject.import(ovf) }.to raise_error(Vagrant::Errors::VirtualBoxNoName) end end end diff --git a/test/unit/plugins/providers/virtualbox/driver/version_6_0_test.rb b/test/unit/plugins/providers/virtualbox/driver/version_6_0_test.rb new file mode 100644 index 000000000..ba0804420 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/driver/version_6_0_test.rb @@ -0,0 +1,99 @@ +require "pathname" +require_relative "../base" + +describe VagrantPlugins::ProviderVirtualBox::Driver::Version_6_0 do + include_context "virtualbox" + + let(:vbox_version) { "6.0.0" } + + subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_6_0.new(uuid) } + + it_behaves_like "a version 4.x virtualbox driver" + it_behaves_like "a version 5.x virtualbox driver" + + describe "#import" do + let(:ovf) { double("ovf") } + let(:machine_id) { double("machine_id") } + let(:output) {<<-OUTPUT +0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% +Interpreting /home/user/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box.ovf... +OK. +Disks: + vmdisk1 85899345920 -1 http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized box-disk1.vmdk -1 -1 + +Virtual system 0: + 0: Suggested OS type: "Ubuntu_64" + (change with "--vsys 0 --ostype "; use "list ostypes" to list all possible values) + 1: Suggested VM name "precise64" + (change with "--vsys 0 --vmname ") + 2: Suggested VM group "/" + (change with "--vsys 0 --group ") + 3: Suggested VM settings file name "/home/user/VirtualBox VMs/precise64/precise64.vbox" + (change with "--vsys 0 --settingsfile ") + 4: Suggested VM base folder "/home/vagrant/VirtualBox VMs" + (change with "--vsys 0 --basefolder ") + 5: Number of CPUs: 2 + (change with "--vsys 0 --cpus ") + 6: Guest memory: 384 MB + (change with "--vsys 0 --memory ") + 7: Network adapter: orig NAT, config 3, extra slot=0;type=NAT + 8: CD-ROM + (disable with "--vsys 0 --unit 8 --ignore") + 9: IDE controller, type PIIX4 + (disable with "--vsys 0 --unit 9 --ignore") +10: IDE controller, type PIIX4 + (disable with "--vsys 0 --unit 10 --ignore") +11: SATA controller, type AHCI + (disable with "--vsys 0 --unit 11 --ignore") +12: Hard disk image: source image=box-disk1.vmdk, target path=box-disk1.vmdk, controller=11;channel=0 + (change target path with "--vsys 0 --unit 12 --disk path"; + disable with "--vsys 0 --unit 12 --ignore") +OUTPUT + } + + before do + allow(Vagrant::Util::Platform).to receive(:windows_path). + with(ovf).and_return(ovf) + allow(subject).to receive(:execute).with("import", "-n", ovf). + and_return(output) + allow(subject).to receive(:execute).with("import", ovf, any_args) + allow(subject).to receive(:get_machine_id).and_return(machine_id) + end + + it "should return the machine id" do + expect(subject).to receive(:get_machine_id).and_return(machine_id) + expect(subject.import(ovf)).to eq(machine_id) + end + + it "should return machine id using custom name" do + expect(subject).to receive(:get_machine_id).with(/.*precise64_.+/).and_return(machine_id) + expect(subject.import(ovf)).to eq(machine_id) + end + + it "should include disk image on import" do + expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) + expect(subject).to receive(:execute) do |*args| + match = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } + expect(match).to include("disk1.vmdk") + end + expect(subject.import(ovf)).to eq(machine_id) + end + + it "should include full path for disk image on import" do + expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) + expect(subject).to receive(:execute) do |*args| + dpath = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } + expect(Pathname.new(dpath).absolute?).to be_truthy + end + expect(subject.import(ovf)).to eq(machine_id) + end + + context "suggested name is not provided" do + before { output.sub!(/Suggested VM name/, "") } + + it "should raise an error" do + expect { subject.import(ovf) }.to raise_error(Vagrant::Errors::VirtualBoxNoName) + end + end + end +end diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_5_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_5_x_examples.rb new file mode 100644 index 000000000..2c8b142fd --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_5_x_examples.rb @@ -0,0 +1,128 @@ +shared_examples "a version 5.x virtualbox driver" do |options| + before do + raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) + end + + describe "#shared_folders" do + let(:folders) { [{:name=>"folder", + :hostpath=>"/Users/brian/vagrant-folder", + :transient=>false, + :SharedFoldersEnableSymlinksCreate=>true}]} + + let(:folders_automount) { [{:name=>"folder", + :hostpath=>"/Users/brian/vagrant-folder", + :transient=>false, + :automount=>true, + :SharedFoldersEnableSymlinksCreate=>true}]} + + let(:folders_disabled) { [{:name=>"folder", + :hostpath=>"/Users/brian/vagrant-folder", + :transient=>false, + :SharedFoldersEnableSymlinksCreate=>false}]} + + it "enables SharedFoldersEnableSymlinksCreate if true" do + expect(subprocess).to receive(:execute). + with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:notify=>[:stdout, :stderr]}). + and_return(subprocess_result(exit_code: 0)) + + expect(subprocess).to receive(:execute). + with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:notify=>[:stdout, :stderr]}). + and_return(subprocess_result(exit_code: 0)) + subject.share_folders(folders) + + end + + it "enables automount if option is true" do + expect(subprocess).to receive(:execute). + with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:notify=>[:stdout, :stderr]}). + and_return(subprocess_result(exit_code: 0)) + + expect(subprocess).to receive(:execute). + with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", "--automount", {:notify=>[:stdout, :stderr]}). + and_return(subprocess_result(exit_code: 0)) + subject.share_folders(folders_automount) + + end + + it "disables SharedFoldersEnableSymlinksCreate if false" do + expect(subprocess).to receive(:execute). + with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:notify=>[:stdout, :stderr]}). + and_return(subprocess_result(exit_code: 0)) + subject.share_folders(folders_disabled) + + end + end + + describe "#set_mac_address" do + let(:mac) { "00:00:00:00:00:00" } + + after { subject.set_mac_address(mac) } + + it "should modify vm and set mac address" do + expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", mac, anything). + and_return(subprocess_result(exit_code: 0)) + end + + context "when mac address is falsey" do + let(:mac) { nil } + + it "should modify vm and set mac address to automatic value" do + expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", "auto", anything). + and_return(subprocess_result(exit_code: 0)) + end + end + end + + describe "#ssh_port" do + let(:forwards) { + [[1, "ssh", 2222, 22, "127.0.0.1"], + [1, "ssh", 8080, 80, ""]] + } + + before { allow(subject).to receive(:read_forwarded_ports).and_return(forwards) } + + it "should return the host port" do + expect(subject.ssh_port(22)).to eq(2222) + end + + context "when multiple matches are available" do + let(:forwards) { + [[1, "ssh", 2222, 22, "127.0.0.1"], + [1, "", 2221, 22, ""]] + } + + it "should choose localhost port forward" do + expect(subject.ssh_port(22)).to eq(2222) + end + + context "when multiple named matches are available" do + let(:forwards) { + [[1, "ssh", 2222, 22, "127.0.0.1"], + [1, "SSH", 2221, 22, "127.0.0.1"]] + } + + it "should choose lowercased name forward" do + expect(subject.ssh_port(22)).to eq(2222) + end + end + end + + context "when only ports are defined" do + let(:forwards) { + [[1, "", 2222, 22, ""]] + } + + it "should return the host port" do + expect(subject.ssh_port(22)).to eq(2222) + end + end + + context "when no matches are available" do + let(:forwards) { [] } + + it "should return nil" do + expect(subject.ssh_port(22)).to be_nil + end + end + end +end