From 56b6a837a7290b83bdc97829b327c96cc4646621 Mon Sep 17 00:00:00 2001 From: jamesdobson Date: Mon, 7 Sep 2015 19:55:03 -0400 Subject: [PATCH 1/3] Removing need for master VM. --- lib/vagrant/environment.rb | 6 +- lib/vagrant/errors.rb | 8 - plugins/providers/virtualbox/action.rb | 10 +- .../virtualbox/action/create_clone.rb | 51 ---- plugins/providers/virtualbox/action/import.rb | 3 +- .../virtualbox/action/import_master.rb | 66 ----- plugins/providers/virtualbox/config.rb | 6 +- plugins/providers/virtualbox/driver/base.rb | 137 +++++++++ plugins/providers/virtualbox/driver/meta.rb | 1 + .../virtualbox/driver/version_4_3.rb | 20 -- .../virtualbox/driver/version_5_0.rb | 20 -- templates/locales/en.yml | 8 - .../virtualbox/support/shared/single-disk.ovf | 272 ++++++++++++++++++ .../virtualbox_driver_version_4_x_examples.rb | 161 +++++++++++ .../v2/virtualbox/configuration.html.md | 15 +- 15 files changed, 583 insertions(+), 201 deletions(-) delete mode 100644 plugins/providers/virtualbox/action/create_clone.rb delete mode 100644 plugins/providers/virtualbox/action/import_master.rb create mode 100644 test/unit/plugins/providers/virtualbox/support/shared/single-disk.ovf diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index d4b8db1fd13..fcea6b69f7a 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -526,11 +526,7 @@ def lock(name="global", **opts) if name != "dotlock" lock("dotlock", retry: true) do f.close - begin - File.delete(lock_path) - rescue - @logger.debug("Failed to delete lock file #{lock_path} - some other thread might be trying to acquire it -> ignoring this error") - end + File.delete(lock_path) end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 2ce133618f2..b705b362e4f 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -784,14 +784,6 @@ class VMBootTimeout < VagrantError error_key(:boot_timeout) end - class VMCloneFailure < VagrantError - error_key(:failure, "vagrant.actions.vm.clone") - end - - class VMCreateMasterFailure < VagrantError - error_key(:failure, "vagrant.actions.vm.clone.create_master") - end - class VMCustomizationFailed < VagrantError error_key(:failure, "vagrant.actions.vm.customize") end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 96bc2a8f165..83eb3aec96c 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -12,7 +12,6 @@ module Action autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) - autoload :CreateClone, File.expand_path("../action/create_clone", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) @@ -22,7 +21,6 @@ module Action autoload :ForcedHalt, File.expand_path("../action/forced_halt", __FILE__) autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Import, File.expand_path("../action/import", __FILE__) - autoload :ImportMaster, File.expand_path("../action/import_master", __FILE__) autoload :IsPaused, File.expand_path("../action/is_paused", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__) @@ -325,13 +323,7 @@ def self.action_up if !env[:result] b2.use CheckAccessible b2.use Customize, "pre-import" - - if env[:machine].provider_config.use_linked_clone - b2.use ImportMaster - b2.use CreateClone - else - b2.use Import - end + b2.use Import b2.use MatchMACAddress end end diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb deleted file mode 100644 index e26b5721fba..00000000000 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "log4r" -#require "lockfile" - -module VagrantPlugins - module ProviderVirtualBox - module Action - class CreateClone - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::action::vm::clone") - end - - def call(env) - @logger.info("Creating linked clone from master '#{env[:master_id]}'") - - env[:ui].info I18n.t("vagrant.actions.vm.clone.creating", name: env[:machine].box.name) - env[:machine].id = env[:machine].provider.driver.clonevm(env[:master_id], env[:machine].box.name, "base") do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear immediately. - env[:ui].clear_line - - # Flag as erroneous and return if clone failed - raise Vagrant::Errors::VMCloneFailure if !env[:machine].id - - # Continue - @app.call(env) - end - - def recover(env) - if env[:machine].state.id != :not_created - return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) - - # If we're not supposed to destroy on error then just return - return if !env[:destroy_on_error] - - # Interrupted, destroy the VM. We note that we don't want to - # validate the configuration here, and we don't want to confirm - # we want to destroy. - destroy_env = env.clone - destroy_env[:config_validate] = false - destroy_env[:force_confirm_destroy] = true - env[:action_runner].run(Action.action_destroy, destroy_env) - end - end - end - end - end -end diff --git a/plugins/providers/virtualbox/action/import.rb b/plugins/providers/virtualbox/action/import.rb index 6648faefc28..e56aee85534 100644 --- a/plugins/providers/virtualbox/action/import.rb +++ b/plugins/providers/virtualbox/action/import.rb @@ -12,7 +12,8 @@ def call(env) # Import the virtual machine ovf_file = env[:machine].box.directory.join("box.ovf").to_s - env[:machine].id = env[:machine].provider.driver.import(ovf_file) do |progress| + method = (env[:machine].provider_config.use_linked_clone) ? :import_multiattach : :import + env[:machine].id = env[:machine].provider.driver.send(method, ovf_file) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb deleted file mode 100644 index 61b0064eb55..00000000000 --- a/plugins/providers/virtualbox/action/import_master.rb +++ /dev/null @@ -1,66 +0,0 @@ -require "log4r" -#require "lockfile" - -module VagrantPlugins - module ProviderVirtualBox - module Action - class ImportMaster - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::action::vm::create_master") - end - - def call(env) - master_id_file = env[:machine].box.directory.join("master_id") - - env[:machine].env.lock(Digest::MD5.hexdigest(env[:machine].box.name), retry: true) do - env[:master_id] = master_id_file.read.chomp if master_id_file.file? - if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) - # Master VM already exists -> nothing to do - continue. - @logger.info("Master VM for '#{env[:machine].box.name}' already exists (id=#{env[:master_id]}) - skipping import step.") - return @app.call(env) - end - - env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) - - # Import the virtual machine - ovf_file = env[:machine].box.directory.join("box.ovf").to_s - env[:master_id] = env[:machine].provider.driver.import(ovf_file) do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear immediately. - env[:ui].clear_line - - # Flag as erroneous and return if import failed - raise Vagrant::Errors::VMImportFailure if !env[:master_id] - - @logger.info("Imported box #{env[:machine].box.name} as master vm with id #{env[:master_id]}") - - @logger.info("Creating base snapshot for master VM.") - env[:machine].provider.driver.create_snapshot(env[:master_id], "base") do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") - master_id_file.open("w+") do |f| - f.write(env[:master_id]) - end - end - - # If we got interrupted, then the import could have been - # interrupted and its not a big deal. Just return out. - if env[:interrupted] - @logger.info("Import of master VM was interrupted -> exiting.") - return - end - - # Import completed successfully. Continue the chain - @app.call(env) - end - end - end - end -end diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index 55b8356aa1d..41cb995a590 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -32,12 +32,12 @@ class Config < Vagrant.plugin("2", :config) # @return [Boolean] attr_accessor :gui - # If set to `true`, then a linked clone is created from a master - # VM generated from the specified box. + # If set to `true`, then a linked clone is created from the + # specified box and a differencing disk image for every linked clone. # # @return [Boolean] attr_accessor :use_linked_clone - + # This should be set to the name of the machine in the VirtualBox # GUI. # diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 112e97d5b17..f0c0a6f17be 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -1,4 +1,5 @@ require 'log4r' +require 'nokogiri' require 'vagrant/util/busy' require 'vagrant/util/platform' @@ -56,6 +57,29 @@ def initialize @logger.info("VBoxManage path: #{@vboxmanage_path}") end + def attach_virtual_disks(machine_id, virtual_disks) + # Find existing disks known to VirtualBox + output = execute("list", "-l", "hdds") + lines = output.split("\n") + + # Attach disks to the VM + virtual_disks.each do |disk| + params = [ + "--storagectl", disk[:controller], + "--port", disk[:port], + "--device", disk[:device], + "--type", "hdd", + "--medium", disk[:file] + ] + + if lines.index {|line| line.index(disk[:file]) != nil} == nil + params << "--mtype" << "multiattach" + end + + execute("storageattach", machine_id, *params) + end + end + # Clears the forwarded ports that have been set on the virtual machine. def clear_forwarded_ports end @@ -148,6 +172,13 @@ def export(path) def forward_ports(ports) 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 + # Halts the virtual machine (pulls the plug). def halt end @@ -159,11 +190,117 @@ def halt def import(ovf) end + # Imports the VM from an OVF file. The disk images in the box are used + # in multiattach mode, speeding the import process. + # + # @param [String] ovf Path to the OVF file. + # @return [String] UUID of the imported VM. + def import_multiattach(ovf) + virtual_disks, doc = parse_ovf(ovf) + + # Write out a new OVF with no references to disk images + ovf = File.absolute_path("box-linked-clone.ovf", File.dirname(ovf)) + f = File.open(ovf, 'w') + doc.write_xml_to(f) + f.close() + + # Import the OVF that has no disks + machine_id = import(ovf) + + attach_virtual_disks(machine_id, virtual_disks) + + return machine_id + end + # Returns the maximum number of network adapters. def max_network_adapters 8 end + # Parses the OVF. The disk images referenced by the OVF are removed + # from the returned DOM and returned as a separate return value. + # + # @param [String] ovf_file Path to the OVF file. + def parse_ovf(ovf_file) + # Parse the OVF + f = File.open(ovf_file) + doc = Nokogiri::XML(f) + f.close() + + # Extract prefixes for the namespaces we care about + ovf = rasd = vbox = "" + doc.namespaces.each do |prefix, namespace| + prefix_start = prefix.index(":") + if prefix_start != nil + prefix_start += 1 + case namespace + when "http://schemas.dmtf.org/ovf/envelope/1" + ovf = prefix[prefix_start..-1] + when "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" + rasd = prefix[prefix_start..-1] + when "http://www.virtualbox.org/ovf/machine" + vbox = prefix[prefix_start..-1] + end + end + end + + if ovf == "" or rasd == "" or vbox == "" + raise Vagrant::Errors::VMImportFailure + end + + # Create a map of disk UUIDs to the disk file paths + # Also remove the Disk, File, and VirtalHardwareSection/Item nodes associated with the disks + ovf_base_dir = File.dirname(ovf_file) + uuid_path_map = {} + disks = doc.xpath("//#{ovf}:Disk") + disks.each do |disk| + fileRef = disk["#{ovf}:fileRef"] + + # Skip empty disks + next if fileRef == nil + + uuid = disk["#{vbox}:uuid"] + diskId = disk["#{ovf}:diskId"] + + # Compute the path to the disk image and remove the associated File node + files = doc.xpath("//#{ovf}:References/#{ovf}:File[@#{ovf}:id='#{fileRef}']") + file = files.first + href = file["#{ovf}:href"] + uuid_path_map[uuid] = File.absolute_path(href, ovf_base_dir) + files.remove + + # Remove the associated VirtualHardwareSection/Item + items = doc.xpath("//#{ovf}:Item[#{rasd}:HostResource/.='/disk/#{diskId}']") + items.remove + + disk.remove + end + + # Find all StorageControllers/StorageController/AttachedDevice that point to Disks and + # remove them, taking note of the controller name to which they are attached and the + # port and device numbers where they are attached. + virtual_disks = [] + controllers = doc.xpath("//#{ovf}:StorageController") + controllers.each do |controller| + name = controller["name"] + devices = controller.xpath("#{ovf}:AttachedDevice") + devices.each do |device| + portIndex = device["port"] + deviceIndex = device["device"] + uuidNode = device.xpath("#{ovf}:Image/@uuid").first + + if uuidNode != nil + uuid = uuidNode.content[1..-2] + @logger.debug("Found virtual disk #{uuid} on '#{name}':#{portIndex}:#{deviceIndex} at #{uuid_path_map[uuid]}") + virtual_disks.push({:controller => name, :file => uuid_path_map[uuid], :port => portIndex, :device => deviceIndex}) + device.remove + end + end + end + + return virtual_disks, doc + end + # Returns a list of forwarded ports for a VM. # # @param [String] uuid UUID of the VM to read from, or `nil` if this diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 80756f9d997..8c0fef3d26f 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -97,6 +97,7 @@ def initialize(uuid=nil) :forward_ports, :halt, :import, + :import_multiattach, :read_forwarded_ports, :read_bridged_interfaces, :read_dhcp_servers, diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index b083637d19f..ef957471c3a 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -34,15 +34,6 @@ def clear_shared_folders end end - def clonevm(master_id, box_name, snapshot_name) - @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") - - machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" - execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) - - return get_machine_id machine_name - end - def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], @@ -71,10 +62,6 @@ def create_host_only_network(options) } end - def create_snapshot(machine_id, snapshot_name) - execute("snapshot", machine_id, "take", snapshot_name) - end - def delete execute("unregistervm", @uuid, "--delete") end @@ -169,13 +156,6 @@ def forward_ports(ports) execute("modifyvm", @uuid, *args) 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") end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index e2aeb35a161..e9849a17201 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -34,15 +34,6 @@ def clear_shared_folders end end - def clonevm(master_id, box_name, snapshot_name) - @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") - - machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" - execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) - - return get_machine_id machine_name - end - def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], @@ -71,10 +62,6 @@ def create_host_only_network(options) } end - def create_snapshot(machine_id, snapshot_name) - execute("snapshot", machine_id, "take", snapshot_name) - end - def delete execute("unregistervm", @uuid, "--delete") end @@ -169,13 +156,6 @@ def forward_ports(ports) execute("modifyvm", @uuid, *args) 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") end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index c5edb63a142..87373d758ca 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1618,14 +1618,6 @@ en: deleting: Clearing any previously set network interfaces... clear_shared_folders: deleting: Cleaning previously set shared folders... - clone: - importing: Importing box '%{name}' as master vm... - creating: Creating linked clone... - failure: Creation of the linked clone failed. - - create_master: - failure: |- - Failed to create lock-file for master VM creation for box %{box}. customize: failure: |- A customization command failed: diff --git a/test/unit/plugins/providers/virtualbox/support/shared/single-disk.ovf b/test/unit/plugins/providers/virtualbox/support/shared/single-disk.ovf new file mode 100644 index 00000000000..6ca38f868ac --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/support/shared/single-disk.ovf @@ -0,0 +1,272 @@ + + + + + + + List of the virtual disks used in the package + + + + Logical networks used in the package + + Logical network used by this appliance. + + + + A virtual machine + + The kind of installed guest operating system + Ubuntu_64 + Ubuntu_64 + + + Virtual hardware requirements for a virtual machine + + Virtual Hardware Family + 0 + precise64 + virtualbox-2.2 + + + 2 virtual CPU + Number of virtual CPUs + 2 virtual CPU + 1 + 3 + 2 + + + MegaBytes + 384 MB of memory + Memory Size + 384 MB of memory + 2 + 4 + 384 + + + 0 + ideController0 + IDE Controller + ideController0 + 3 + PIIX4 + 5 + + + 1 + ideController1 + IDE Controller + ideController1 + 4 + PIIX4 + 5 + + + 0 + sataController0 + SATA Controller + sataController0 + 5 + AHCI + 20 + + + true + Ethernet adapter on 'NAT' + NAT + Ethernet adapter on 'NAT' + 6 + E1000 + 10 + + + 0 + true + cdrom1 + CD-ROM Drive + cdrom1 + 7 + 3 + 15 + + + 0 + true + cdrom2 + CD-ROM Drive + cdrom2 + 8 + 4 + 15 + + + 0 + disk1 + Disk Image + disk1 + /disk/vmdisk1 + 9 + 5 + 17 + + + + Complete VirtualBox machine configuration in VirtualBox format + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb index 88f199a82df..c1bd553a4e4 100644 --- a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb @@ -3,6 +3,167 @@ raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) end + describe "attach_virtual_disks" do + context "with some disks already in use" do + before { + expect(subprocess).to receive(:execute). + with("VBoxManage", "list", "-l", "hdds", an_instance_of(Hash)). + and_return(subprocess_result(stdout: + <<-OUTPUT.gsub(/^ */, '') + UUID: e1246c7c-05dd-48c5-aa5b-5ad44ce0c13e + Parent UUID: base + State: locked read + Type: multiattach + Location: /home/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box-disk1.vmdk + Storage format: VMDK + Format variant: dynamic streamOptimized + Capacity: 81920 MBytes + Size on disk: 305 MBytes + Child UUIDs: 1616c5a2-929c-49c1-8f66-08ab44fbc091 + + UUID: 1616c5a2-929c-49c1-8f66-08ab44fbc091 + Parent UUID: e1246c7c-05dd-48c5-aa5b-5ad44ce0c13e + State: locked write + Type: normal (differencing) + Auto-Reset: off + Location: /VirtualBox VMs/vagrant_test02_1441657718454_69597/Snapshots/{1616c5a2-929c-49c1-8f66-08ab44fbc091}.vmdk + Storage format: VMDK + Format variant: differencing default + Capacity: 81920 MBytes + Size on disk: 10 MBytes + In use by VMs: vagrant_test02_1441657718454_69597 (UUID: ad89e52f-8e2b-4df7-acc3-a5dacdb0459a) + OUTPUT + ) + ) + } + + it "attaches one disk that is already in use" do + expect(subprocess).to receive(:execute). + with("VBoxManage", "storageattach", "123", + "--storagectl", "SATA Controller", + "--port", "0", + "--device", "0", + "--type", "hdd", + "--medium", "/home/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box-disk1.vmdk", + an_instance_of(Hash) + ).and_return(subprocess_result(stdout: "")) + + subject.attach_virtual_disks("123", + [ + { + :controller => "SATA Controller", + :port => "0", + :device => "0", + :file => "/home/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box-disk1.vmdk" + } + ] + ) + end + + it "attaches one disk that is not in use" do + expect(subprocess).to receive(:execute). + with("VBoxManage", "storageattach", "123", + "--storagectl", "SATA Controller", + "--port", "0", + "--device", "0", + "--type", "hdd", + "--medium", "/test-disk.vmdk", + "--mtype", "multiattach", + an_instance_of(Hash) + ).and_return(subprocess_result(stdout: "")) + + subject.attach_virtual_disks("123", + [ + { + :controller => "SATA Controller", + :port => "0", + :device => "0", + :file => "/test-disk.vmdk" + } + ] + ) + end + end + end + + describe "get_machine_id" do + before { + expect(subprocess).to receive(:execute). + with("VBoxManage", "list", "vms", an_instance_of(Hash)). + and_return(subprocess_result(stdout: output)) + } + + context "with list of VMs" do + let(:output) { + <<-OUTPUT.gsub(/^ */, '') + "Another VM" {f6845e8c-1434-4415-b280-964c86ed6fc7} + "vagrant_test02_1441657718454_69597" {ad89e52f-8e2b-4df7-acc3-a5dacdb0459a} + "vagrant_test01_1441657738990_91195" {6b9d61f1-e553-4ee9-9ac9-ff5f04614b38} + OUTPUT + } + + it "finds a VM" do + value = subject.get_machine_id("vagrant_test01_1441657738990_91195") + + expect(value).to eq("6b9d61f1-e553-4ee9-9ac9-ff5f04614b38") + end + + it "does not find a VM" do + value = subject.get_machine_id("will not be found") + + expect(value).to eq(nil) + end + end + + context "with empty output" do + let(:output) { "" } + + it "returns nil" do + value = subject.get_machine_id("will not be found") + + expect(value).to eq(nil) + end + end + end + + describe "parse_ovf" do + context "with OVF with a single disk from hashicorp/precise64" do + before { + expect(File).to receive(:open). + with(an_instance_of(String)). + and_return(File.open(File.expand_path("single-disk.ovf", File.dirname(__FILE__)))) + } + + it "should have one virtual disk" do + virtual_disks, doc = subject.parse_ovf("/path/box.ovf") + + expect(virtual_disks).to eq( + [ + { + :controller => "SATA Controller", + :file => "/path/box-disk1.vmdk", + :port => "0", + :device => "0" + } + ] + ) + end + end + + context "with empty OVF" do + before { + expect(File).to receive(:open). + with(an_instance_of(String)). + and_return(StringIO.new("")) + } + + it "should not parse correctly" do + expect { subject.parse_ovf("/path/box.ovf") }. + to raise_error Vagrant::Errors::VMImportFailure + end + end + end + describe "read_dhcp_servers" do before { expect(subprocess).to receive(:execute). diff --git a/website/docs/source/v2/virtualbox/configuration.html.md b/website/docs/source/v2/virtualbox/configuration.html.md index 5c908ab68c9..c62812129a5 100644 --- a/website/docs/source/v2/virtualbox/configuration.html.md +++ b/website/docs/source/v2/virtualbox/configuration.html.md @@ -43,10 +43,11 @@ boxes this produces a large overhead in terms of time (the import operation) and space (the new machine contains a copy of the base box's image). Using linked clones can drastically reduce this overhead. -Linked clones are based on a master VM, which is generated by importing the -base box only once the first time it is required. For the linked clones only -differencing disk images are created where the parent disk image belongs to -the master VM. +Linked clones are based on the multiattach mode for disk images in +VirtualBox. When the base box is imported, instead of copying the base box's +virtual disk image, a small differencing disk image is created. This image +is backed by the image in the base box. All writes go to the differencing +image; the image in the base box is treated as if it were read-only. ```ruby config.vm.provider "virtualbox" do |v| @@ -54,12 +55,6 @@ config.vm.provider "virtualbox" do |v| end ``` -
- Note: the generated master VMs are currently not removed - automatically by Vagrant. This has to be done manually. However, a master - VM can only be removed when there are no linked clones connected to it. -
- ## VBoxManage Customizations [VBoxManage](http://www.virtualbox.org/manual/ch08.html) is a utility that can From 6b9cbf79e7827c05323270ef3e85ae7c150eae26 Mon Sep 17 00:00:00 2001 From: jamesdobson Date: Mon, 7 Sep 2015 20:08:17 -0400 Subject: [PATCH 2/3] Added comments and removed old method references. --- plugins/providers/virtualbox/driver/base.rb | 17 +++++++++++++++++ plugins/providers/virtualbox/driver/meta.rb | 2 -- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index f0c0a6f17be..66ced881905 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -57,6 +57,19 @@ def initialize @logger.info("VBoxManage path: #{@vboxmanage_path}") end + # Attaches the specified disk images to the specified VM. + # + # The format of each virtual disk should be: + # + # { + # :controller => "SATA Controller", # Name of the controller to attach disk image + # :port => "0", # Port to attach disk image + # :device => "0", # Device to attach disk image + # :file => "/path/to/image.vmdk" # Path to the disk image file + # } + # + # @param [String] machine_id The id of the VM. + # @param [Array] virtual_disks Array of disk images to attach. def attach_virtual_disks(machine_id, virtual_disks) # Find existing disks known to VirtualBox output = execute("list", "-l", "hdds") @@ -172,6 +185,10 @@ def export(path) def forward_ports(ports) end + # Gets the UUID of the VM with the specified name. + # + # @param [String] machine_name The name of the VM. + # @return [String] UUID of the VM. def get_machine_id(machine_name) output = execute("list", "vms", retryable: true) match = /^"#{Regexp.escape(machine_name)}" \{(.+?)\}$/.match(output) diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 8c0fef3d26f..4e1eab6a3b5 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -84,10 +84,8 @@ def initialize(uuid=nil) def_delegators :@driver, :clear_forwarded_ports, :clear_shared_folders, - :clonevm, :create_dhcp_server, :create_host_only_network, - :create_snapshot, :delete, :delete_unused_host_only_networks, :discard_saved_state, From 45b12ff9be282702fc3c0f9cf8c5f078e73afd9e Mon Sep 17 00:00:00 2001 From: jamesdobson Date: Mon, 21 Sep 2015 23:13:53 -0400 Subject: [PATCH 3/3] Fix separator issue when attaching the virtual disk. --- plugins/providers/virtualbox/driver/base.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 66ced881905..1bb376c688f 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -74,6 +74,8 @@ def attach_virtual_disks(machine_id, virtual_disks) # Find existing disks known to VirtualBox output = execute("list", "-l", "hdds") lines = output.split("\n") + known_files = lines.select() { |line| line.start_with?('Location:') } + .map() { |line| File.absolute_path(line.sub(/^Location:\s*([^\s](.*))$/, '\1')) } # Attach disks to the VM virtual_disks.each do |disk| @@ -85,7 +87,7 @@ def attach_virtual_disks(machine_id, virtual_disks) "--medium", disk[:file] ] - if lines.index {|line| line.index(disk[:file]) != nil} == nil + if known_files.none? { |known_file| File.identical?(known_file, disk[:file]) } params << "--mtype" << "multiattach" end